Guide de l'API Vavr Collections

VAVR (anciennement Javaslang) est une bibliothèque fonctionnelle à but non lucratif pour Java 8+. Il vous permet d'écrire du code fonctionnel de type Scala en Java et sert à réduire la quantité de code et à améliorer sa qualité. Site de la bibliothèque .

Sous la coupe se trouve une traduction d'un article systématisant les informations sur l'API Vavr Collections .

Traduit par @middle_java

Dernière modification de l'article original: 15 août 2019

1. Présentation


La bibliothèque Vavr, précédemment connue sous le nom de Javaslang, est une bibliothèque fonctionnelle pour Java. Dans cet article, nous explorons sa puissante API de collecte.

Consultez cet article pour plus d'informations sur cette bibliothèque.

2. Collections persistantes


Une collection persistante, lorsqu'elle est modifiée, crée une nouvelle version de la collection sans changer la version actuelle.

La prise en charge de plusieurs versions de la même collection peut entraîner une utilisation inefficace du processeur et de la mémoire. Cependant, la bibliothèque de collections Vavr surmonte cela en partageant la structure de données entre différentes versions de la collection.

Ceci est fondamentalement différent de unmodifiableCollection() de la classe d'utilitaires Java Collections , qui fournit simplement un wrapper pour la collection de base.

Tenter de modifier une telle collection UnsupportedOperationException au lieu de créer une nouvelle version. De plus, la collection de base est toujours modifiable par un lien direct avec elle.

3. Traversable


Traversable est le type de base de toutes les collections Vavr. Cette interface définit des méthodes communes à toutes les structures de données.

Il fournit des méthodes par défaut utiles, telles que size() , get() , filter() , isEmpty() et d'autres héritées des sous-interfaces.

Nous explorons davantage la bibliothèque de la collection.

4. Seq


Commençons par les séquences.

L'interface Seq est une structure de données séquentielle. Il s'agit de l'interface parent pour List , Stream , Queue , Array , Vector et CharSeq . Toutes ces structures de données ont leurs propres propriétés uniques, dont nous parlerons ci-dessous.

4.1. Liste


List est une opération calculée énergiquement (évaluée avec impatience, est effectuée dès que les valeurs de ses opérandes sont connues) une séquence d'éléments qui étendent l'interface LinearSeq .

Les List persistantes List construites récursivement en utilisant la tête et la queue :

  • La tête est le premier élément
  • Queue - une liste contenant les éléments restants (cette liste est également formée de la tête et de la queue)

L'API List contient des méthodes d'usine statiques que vous pouvez utiliser pour créer une List . Vous pouvez utiliser la méthode static of() pour créer une instance de List partir d'un ou plusieurs objets.

Vous pouvez également utiliser la méthode static empty() pour créer une List vide et la méthode ofAll() pour créer une List de type Iterable :

 List < String > list = List.of( "Java", "PHP", "Jquery", "JavaScript", "JShell", "JAVA"); 

Regardons quelques exemples de manipulation de liste.

Nous pouvons utiliser la méthode drop() et ses variantes pour supprimer les premiers N éléments:

 List list1 = list.drop(2); assertFalse(list1.contains("Java") && list1.contains("PHP")); List list2 = list.dropRight(2); assertFalse(list2.contains("JAVA") && list2.contains("JShell")); List list3 = list.dropUntil(s - > s.contains("Shell")); assertEquals(list3.size(), 2); List list4 = list.dropWhile(s - > s.length() > 0); assertTrue(list4.isEmpty()); 

drop(int n) supprime n éléments de la liste, à partir du premier élément, tandis que dropRight() fait de même, à partir du dernier élément de la liste.

dropUntil() supprime les éléments de la liste jusqu'à ce que le prédicat soit true , tandis que dropWhile() supprime les éléments jusqu'à ce que le prédicat soit true .

Il existe également des dropRightWhile() et dropRightUntil() qui suppriment les éléments en partant de la droite.

Ensuite, take(int n) utilisé pour récupérer des éléments de la liste. Il prend n éléments de la liste puis s'arrête. Il y a aussi takeRight(int n) , qui prend des éléments de la fin de la liste:

 List list5 = list.take(1); assertEquals(list5.single(), "Java"); List list6 = list.takeRight(1); assertEquals(list5.single(), "Java"); List list7 = list.takeUntil(s - > s.length() > 6); assertEquals(list3.size(), 3); 

Enfin, takeUntil() prend des éléments de la liste jusqu'à ce que le prédicat devienne true . Il existe une variante de takeWhile() qui prend également un argument de prédicat.

De plus, l'API a d'autres méthodes utiles, par exemple même distinct() , qui renvoie une liste d'éléments avec des doublons supprimés, ainsi que distinctBy() , qui accepte Comparator pour déterminer l'égalité.

Il est très intéressant qu'il y ait aussi intersperse() , qui insère un élément entre chaque élément de la liste. Cela peut être très pratique pour les opérations avec String :

 List list8 = list .distinctBy((s1, s2) - > s1.startsWith(s2.charAt(0) + "") ? 0 : 1); assertEquals(list3.size(), 2); String words = List.of("Boys", "Girls") .intersperse("and") .reduce((s1, s2) - > s1.concat(" " + s2)) .trim(); assertEquals(words, "Boys and Girls"); 

Vous voulez diviser la liste en catégories? Et pour cela il y a une API:

 Iterator < List < String >> iterator = list.grouped(2); assertEquals(iterator.head().size(), 2); Map < Boolean, List < String >> map = list.groupBy(e - > e.startsWith("J")); assertEquals(map.size(), 2); assertEquals(map.get(false).get().size(), 1); assertEquals(map.get(true).get().size(), 5); 

La méthode group(int n) divise List en groupes de n éléments chacun. La méthode groupdBy() prend une Function qui contient la logique de fractionnement de la liste et renvoie une Map avec deux éléments: true et false .

La true clé est mappée sur la List éléments satisfaisant à la condition spécifiée dans Function . La false clé correspond à la List éléments qui ne remplissent pas cette condition.

Comme prévu, lors de la modification de la List , la List origine ne change pas réellement. Au lieu de cela, la nouvelle version de List toujours renvoyée.

Nous pouvons également interagir avec List utilisant la sémantique de la pile - extraire des éléments selon le principe «dernier entré, premier sorti» (LIFO). En ce sens, des méthodes API telles que peek() , pop() et push() existent pour manipuler la pile:

 List < Integer > intList = List.empty(); List < Integer > intList1 = intList.pushAll(List.rangeClosed(5, 10)); assertEquals(intList1.peek(), Integer.valueOf(10)); List intList2 = intList1.pop(); assertEquals(intList2.size(), (intList1.size() - 1)); 

La fonction pushAll() est utilisée pour insérer une plage d'entiers sur la pile, et la fonction peek() est utilisée pour récupérer l'élément head de la pile. Il existe également une méthode peekOption() qui peut encapsuler le résultat dans un objet Option .

Il existe d'autres méthodes intéressantes et vraiment utiles dans l'interface List qui sont documentées en détail dans les documents Java .

4.2. File d'attente


La Queue immuable stocke les éléments, vous permettant de les récupérer selon le principe FIFO (premier entré, premier sorti).

Queue intérieur se compose de deux listes liées: la List avant et la List arrière. La List avant contient les éléments qui sont supprimés de la file d'attente et la List arrière contient les éléments mis en file d'attente.

Cela vous permet de mettre les opérations de mise en file d'attente et de retrait de la file d'attente à la complexité O (1) . Lorsque la List se termine dans la List avant lorsqu'elle est supprimée de la file d'attente, la List arrière List inversée et devient la nouvelle List avant.

Créons une file d'attente:

 Queue < Integer > queue = Queue.of(1, 2); Queue < Integer > secondQueue = queue.enqueueAll(List.of(4, 5)); assertEquals(3, queue.size()); assertEquals(5, secondQueue.size()); Tuple2 < Integer, Queue < Integer >> result = secondQueue.dequeue(); assertEquals(Integer.valueOf(1), result._1); Queue < Integer > tailQueue = result._2; assertFalse(tailQueue.contains(secondQueue.get(0))); 

La fonction dequeue() supprime l'élément head de Queue et renvoie Tuple2<T, Q> . Le premier élément du tuple est l'élément head retiré de la file d'attente, le deuxième élément du tuple est le reste des éléments Queue .

Nous pouvons utiliser la combination(n) pour obtenir toutes les N combinaisons possibles d'éléments dans la Queue :

 Queue < Queue < Integer >> queue1 = queue.combinations(2); assertEquals(queue1.get(2).toCharSeq(), CharSeq.of("23")); 

Encore une fois, la Queue origine ne change pas lors de l'ajout / suppression d'éléments de la file d'attente.

4.3. Stream


Stream est une implémentation d'une liste liée paresseusement qui est significativement différente de java.util.stream . Contrairement à java.util.stream , Stream Vavr stocke les données et calcule paresseusement les éléments suivants.
Disons que nous avons des entiers Stream :

 Stream < Integer > s = Stream.of(2, 1, 3, 4); 

Lors de l'impression du résultat de s.toString() dans la console, seul Stream (2 ,?) sera affiché. Cela signifie que seul l'élément de tête de Stream été calculé, alors que les éléments de queue ne l'ont pas été.

Appeler s.get(3) puis afficher le résultat de s.tail() renvoie Stream (1, 3, 4 ,?) . Au contraire, si vous n'appelez pas d'abord s.get(3) - ce qui fera que Stream calculera le dernier élément - seul Stream (1 ,?) sera le résultat de s.tail() ) . Cela signifie que seul le premier élément de queue a été calculé.

Ce comportement peut améliorer les performances et permet à Stream d'être utilisé pour représenter des séquences qui sont (théoriquement) infiniment longues.
Stream dans Vavr est immuable et peut être Empty ou Cons . Cons composent de l'élément de tête et de la queue calculée paresseusement du Stream . Contrairement à List , Stream stocke que l'élément head en mémoire. Les éléments de queue sont calculés selon les besoins.

Créons un Stream de 10 entiers positifs et calculons la somme des nombres pairs:

 Stream < Integer > intStream = Stream.iterate(0, i - > i + 1) .take(10); assertEquals(10, intStream.size()); long evenSum = intStream.filter(i - > i % 2 == 0) .sum() .longValue(); assertEquals(20, evenSum); 

Contrairement à l'API Stream de Java 8, Stream dans Vavr est une structure de données pour stocker une séquence d'éléments.

Par conséquent, il a des méthodes telles que get() , append() , insert() et autres pour manipuler ses éléments. drop() , distinct() et certaines autres méthodes décrites précédemment sont également disponibles.

Enfin, montrons rapidement tabulate() dans Stream . Cette méthode retourne un Stream longueur n contenant des éléments qui sont le résultat de l'application de la fonction:

 Stream < Integer > s1 = Stream.tabulate(5, (i) - > i + 1); assertEquals(s1.get(2).intValue(), 3); 

Nous pouvons également utiliser zip() pour créer un Stream partir de Tuple2<Integer, Integer> , qui contient des éléments formés en combinant deux Stream :

 Stream < Integer > s = Stream.of(2, 1, 3, 4); Stream < Tuple2 < Integer, Integer >> s2 = s.zip(List.of(7, 8, 9)); Tuple2 < Integer, Integer > t1 = s2.get(0); assertEquals(t1._1().intValue(), 2); assertEquals(t1._2().intValue(), 7); 

4.4. Array


Array est une séquence indexée immuable qui fournit un accès aléatoire efficace. Il est basé sur un tableau d' objets Java. Il s'agit essentiellement d'un wrapper Traversable pour un tableau d'objets de type T

Vous pouvez créer une instance d' Array à l'aide de la méthode static of() . De plus, vous pouvez créer une plage d'éléments à l'aide des méthodes statiques range() et rangeBy() . La méthode rangeBy() possède un troisième paramètre qui vous permet de déterminer l'étape.

Les méthodes range() et rangeBy() créeront des éléments, en commençant uniquement de la valeur initiale à la valeur finale moins un. Si nous devons inclure la valeur finale, nous pouvons utiliser rangeClosed() ou rangeClosedBy() :

 Array < Integer > rArray = Array.range(1, 5); assertFalse(rArray.contains(5)); Array < Integer > rArray2 = Array.rangeClosed(1, 5); assertTrue(rArray2.contains(5)); Array < Integer > rArray3 = Array.rangeClosedBy(1, 6, 2); assertEquals(list3.size(), 3); 

Travaillons avec les éléments en utilisant l'index:

 Array < Integer > intArray = Array.of(1, 2, 3); Array < Integer > newArray = intArray.removeAt(1); assertEquals(3, intArray.size()); assertEquals(2, newArray.size()); assertEquals(3, newArray.get(1).intValue()); Array < Integer > array2 = intArray.replace(1, 5); assertEquals(s1.get(0).intValue(), 5); 

4.5. Vecteur


Vector est un croisement entre Array et List , fournissant une autre séquence indexée d'éléments, permettant à la fois un accès aléatoire et une modification en temps constant:

 Vector < Integer > intVector = Vector.range(1, 5); Vector < Integer > newVector = intVector.replace(2, 6); assertEquals(4, intVector.size()); assertEquals(4, newVector.size()); assertEquals(2, intVector.get(1).intValue()); assertEquals(6, newVector.get(1).intValue()); 

4.6. Charseq


CharSeq est un objet de collection pour représenter une séquence de caractères primitifs. Il s'agit essentiellement d'un wrapper pour String avec l'ajout d'opérations de collecte.

Pour créer CharSeq vous devez procéder comme suit.

 CharSeq chars = CharSeq.of("vavr"); CharSeq newChars = chars.replace('v', 'V'); assertEquals(4, chars.size()); assertEquals(4, newChars.size()); assertEquals('v', chars.charAt(0)); assertEquals('V', newChars.charAt(0)); assertEquals("Vavr", newChars.mkString()); 

5. Définir


Cette section présente les différentes implémentations de Set dans la bibliothèque de collections. Une caractéristique unique de la structure de données Set est qu'elle n'autorise pas les valeurs en double.

Il existe différentes implémentations de Set . Le principal est HashSet . TreeSet n'autorise pas les éléments en double et peut être trié. LinkedHashSet préserve l'ordre d'insertion des éléments.

Examinons ces implémentations de plus près.

5.1. Hashset


HashSet a des méthodes d'usine statiques pour créer de nouvelles instances. Nous avons étudié certaines d'entre elles plus haut dans cet article, par exemple les ofAll() et les variations des méthodes range() .

La différence entre les deux ensembles peut être obtenue en utilisant la méthode diff() . De plus, les méthodes union() et intersect() renvoient l'union et l'intersection de deux ensembles :

 HashSet < Integer > set0 = HashSet.rangeClosed(1, 5); HashSet < Integer > set0 = HashSet.rangeClosed(1, 5); assertEquals(set0.union(set1), HashSet.rangeClosed(1, 6)); assertEquals(set0.diff(set1), HashSet.rangeClosed(1, 2)); assertEquals(set0.intersect(set1), HashSet.rangeClosed(3, 5)); 

Nous pouvons également effectuer des opérations de base, telles que l'ajout et la suppression d'éléments:

 HashSet < String > set = HashSet.of("Red", "Green", "Blue"); HashSet < String > newSet = set.add("Yellow"); assertEquals(3, set.size()); assertEquals(4, newSet.size()); assertTrue(newSet.contains("Yellow")); 

L'implémentation HashSet est basée sur le trieur de tableaux Hash (HAMT) , qui offre des performances supérieures par rapport à HashTable et sa structure le rend approprié pour prendre en charge les collections persistantes.

5.2. Treeset


TreeSet est une implémentation de l'interface SortedSet . Il stocke un ensemble d'éléments triés et est implémenté à l'aide d'arbres de recherche binaires. Toutes ses opérations sont effectuées pendant le temps O (log n) .

Par défaut, les éléments TreeSet sont triés dans leur ordre naturel.
Créons un SortedSet utilisant un ordre de tri naturel:

 SortedSet < String > set = TreeSet.of("Red", "Green", "Blue"); assertEquals("Blue", set.head()); SortedSet < Integer > intSet = TreeSet.of(1, 2, 3); assertEquals(2, intSet.average().get().intValue()); 

Pour organiser les éléments de manière personnalisée, transmettez une instance de Comparator lors de la création du TreeSet . Vous pouvez également créer une chaîne à partir d'un ensemble d'éléments:

 SortedSet < String > reversedSet = TreeSet.of(Comparator.reverseOrder(), "Green", "Red", "Blue"); assertEquals("Red", reversedSet.head()); String str = reversedSet.mkString(" and "); assertEquals("Red and Green and Blue", str); 

5.3. Bitset


Les collections Vavr ont également une implémentation BitSet immuable. L'interface BitSet étend l'interface BitSet . BitSet peut être créé à l'aide de méthodes statiques dans BitSet.Builder .
Comme avec d'autres implémentations de la structure de données Set , BitSet ne vous permet pas d'ajouter des enregistrements en double à un ensemble.

Il hérite des méthodes de manipulation de l'interface Traversable . Notez qu'il est différent de java.util.BitSet de la bibliothèque Java standard. BitSet données BitSet ne peuvent pas contenir de valeurs de String .

Envisagez de créer une instance de BitSet à l'aide de la méthode d'usine of() :

 BitSet < Integer > bitSet = BitSet.of(1, 2, 3, 4, 5, 6, 7, 8); BitSet < Integer > bitSet1 = bitSet.takeUntil(i - > i > 4); assertEquals(list3.size(), 4); 

Pour sélectionner les quatre BitSet éléments BitSet nous avons utilisé la commande takeUntil() . L'opération a renvoyé une nouvelle instance. Notez que la méthode takeUntil() est définie dans l'interface Traversable , qui est l'interface parent de BitSet .

Les autres méthodes et opérations décrites ci-dessus définies dans l'interface Traversable s'appliquent également à BitSet .

6. Carte


Map est une structure de données de valeur-clé. Map dans Vavr est immuable et comporte des implémentations pour HashMap , TreeMap et LinkedHashMap .

En règle générale, les contrats de carte n'autorisent pas les clés en double, tandis que les valeurs en double mappées sur différentes clés peuvent l'être.

6.1. Hashmap


HashMap est une implémentation de l'interface Map immuable. Il stocke les paires clé-valeur à l'aide d'un hachage de clés.

Map dans Vavr utilise Tuple2 pour représenter des paires clé-valeur au lieu du type d' Entry traditionnel:

 Map < Integer, List < Integer >> map = List.rangeClosed(0, 10) .groupBy(i - > i % 2); assertEquals(2, map.size()); assertEquals(6, map.get(0).get().size()); assertEquals(5, map.get(1).get().size()); 

Comme HashSet , l'implémentation de HashMap basée sur le trieur de tableaux Hash (HAMT) , ce qui conduit à un temps constant pour presque toutes les opérations.
Les éléments de carte peuvent être filtrés par clé à l'aide de la méthode filterKeys() ou par valeur à l'aide de la méthode filterValues() . Les deux méthodes prennent Predicate comme argument:

 Map < String, String > map1 = HashMap.of("key1", "val1", "key2", "val2", "key3", "val3"); Map < String, String > fMap = map1.filterKeys(k - > k.contains("1") || k.contains("2")); assertFalse(fMap.containsKey("key3")); Map < String, String > map1 = map1.filterValues(v - > v.contains("3")); assertEquals(list3.size(), 1); assertTrue(fMap2.containsValue("val3")); 

Vous pouvez également transformer des éléments de carte à l'aide de la méthode map() . Par exemple, convertissons map1 en Map<String, Integer> :

 Map < String, Integer > map2 = map1.map( (k, v) - > Tuple.of(k, Integer.valueOf(v.charAt(v.length() - 1) + ""))); assertEquals(map2.get("key1").get().intValue(), 1); 

6.2. Treemap


Immutable TreeMap est une implémentation de l'interface SortedMap . Comme avec TreeSet , une instance personnalisée de Comparator utilisée pour personnaliser le tri des éléments TreeMap .
SortedMap la création de SortedMap :

 SortedMap < Integer, String > map = TreeMap.of(3, "Three", 2, "Two", 4, "Four", 1, "One"); assertEquals(1, map.keySet().toJavaArray()[0]); assertEquals("Four", map.get(4).get()); 

Par défaut, les entrées TreeMap sont triées dans l'ordre des clés naturelles. Cependant, vous pouvez spécifier le Comparator à utiliser pour le tri:

 TreeMap < Integer, String > treeMap2 = TreeMap.of(Comparator.reverseOrder(), 3, "three", 6, "six", 1, "one"); assertEquals(treeMap2.keySet().mkString(), "631"); 

Comme dans le cas de TreeSet , l'implémentation TreeMap est également créée à l'aide de l'arbre, par conséquent, ses opérations ont le temps O (log n) . La map.get(key) renvoie Option , qui contient la valeur de la clé de carte spécifiée.

7. Compatibilité Java


L'API Vavr Collection est entièrement compatible avec Java Collection Framework. Voyons comment cela se fait dans la pratique.

7.1. Convertir de Java à Vavr


Chaque implémentation de collection dans Vavr possède une ofAll() fabrique statique ofAll() qui accepte java.util.Iterable . Cela vous permet de créer une collection Vavr à partir d'une collection Java . De même, une autre méthode d'usine ofAll() accepte directement Java Stream .

Pour convertir une List Java en une List immuable:

 java.util.List < Integer > javaList = java.util.Arrays.asList(1, 2, 3, 4); List < Integer > vavrList = List.ofAll(javaList); java.util.stream.Stream < Integer > javaStream = javaList.stream(); Set < Integer > vavrSet = HashSet.ofAll(javaStream); 

Une autre fonction utile est collector() , qui peut être utilisée en conjonction avec Stream.collect() pour obtenir la collection Vavr:

 List < Integer > vavrList = IntStream.range(1, 10) .boxed() .filter(i - > i % 2 == 0) .collect(List.collector()); assertEquals(4, vavrList.size()); assertEquals(2, vavrList.head().intValue()); 

7.2. Convertir de Vavr à Java


L'interface Value possède de nombreuses méthodes de conversion d'un type Vavr vers un type Java . Ces méthodes ont le format toJavaXXX() .

Prenons quelques exemples:

 Integer[] array = List.of(1, 2, 3) .toJavaArray(Integer.class); assertEquals(3, array.length); java.util.Map < String, Integer > map = List.of("1", "2", "3") .toJavaMap(i - > Tuple.of(i, Integer.valueOf(i))); assertEquals(2, map.get("2").intValue()); 

Nous pouvons également utiliser Java 8 Collectors pour collecter des éléments des collections Vavr:

 java.util.Set < Integer > javaSet = List.of(1, 2, 3) .collect(Collectors.toSet()); assertEquals(3, javaSet.size()); assertEquals(1, javaSet.toArray()[0]); 

7.3. Vues des collections Java


De plus, la bibliothèque propose des vues de collection qui fonctionnent mieux lorsqu'elles sont converties en collections Java. Les méthodes de transformation de la section précédente itèrent sur (itèrent) tous les éléments pour créer une collection Java.

Les vues, quant à elles, implémentent des interfaces Java standard et délèguent les appels de méthode à la collection de base Vavr.

Au moment d'écrire ces lignes, seule la vue List est prise en charge. Chaque collection séquentielle a deux méthodes: l'une pour créer une représentation immuable, l'autre pour mutable.

L'appel de méthodes pour modifier une vue immuable UnsupportedOperationException .

Regardons un exemple:

 @Test(expected = UnsupportedOperationException.class) public void givenVavrList_whenViewConverted_thenException() { java.util.List < Integer > javaList = List.of(1, 2, 3) .asJava(); assertEquals(3, javaList.get(2).intValue()); javaList.add(4); } 

Pour créer une vue immuable:

 java.util.List < Integer > javaList = List.of(1, 2, 3) .asJavaMutable(); javaList.add(4); assertEquals(4, javaList.get(3).intValue()); 

8. Conclusions


Dans ce didacticiel, nous avons découvert les différentes structures de données fonctionnelles fournies par l'API Vavr Collections. Il existe également des méthodes API utiles et productives qui peuvent être trouvées dans la documentation Java et le Guide de l' utilisateur des collections Vavr.

Enfin, il est important de noter que la bibliothèque définit également Try , Option , Either et Future , qui étendent l'interface Value et, par conséquent, implémentent l'interface Java Iterable . Cela signifie que dans certaines situations, ils peuvent se comporter comme des collections.

Le code source complet de tous les exemples de cet article se trouve sur Github .

Matériaux supplémentaires:
habr.com/en/post/421839
www.baeldung.com/vavr

Traduit par @middle_java

Source: https://habr.com/ru/post/fr474402/


All Articles