Guide de l’API Collections dans Vavr

Guide de l'API Collections dans Vavr

1. Vue d'ensemble

La bibliothèque Vavr, anciennement appelée Javaslang, est une bibliothèque fonctionnelle pour Java. Dans cet article, nous explorons ses puissantes API de collections.

Pour obtenir plus d'informations sur cette bibliothèque, veuillez lirethis article.

2. Collections persistantes

Une collection persistante, une fois modifiée, produit une nouvelle version de la collection tout en préservant la version actuelle.

La maintenance de plusieurs versions d'une même collection peut entraîner une utilisation inefficace du processeur et de la mémoire. Cependant, la bibliothèque de collections Vavr résout ce problème en partageant la structure de données entre différentes versions d'une collection.

Ceci est fondamentalement différent deunmodifiableCollection() de Java de la classe utilitaireCollections, qui fournit simplement un wrapper autour d'une collection sous-jacente.

Tenter de modifier une telle collection entraîneUnsupportedOperationException au lieu de créer une nouvelle version. De plus, la collection sous-jacente est toujours modifiable par sa référence directe.

3. Traversable

Traversable est le type de base de toutes les collections Vavr - cette interface définit les méthodes qui sont partagées entre toutes les structures de données.

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

Explorons davantage la bibliothèque des collections.

4. Seq

Nous commencerons par des séquences.

L'interfaceSeq représente des structures de données séquentielles. C'est l'interface parente pourList,Stream,Queue,Array,Vector etCharSeq. Toutes ces structures de données ont leurs propres propriétés uniques que nous allons explorer ci-dessous.

4.1. List

UnList est une séquence d'éléments évalués avec impatience étendant l'interfaceLinearSeq.

LesLists persistants sont formés récursivement à partir d'une tête et d'une queue:

  • Tête - le premier élément

  • Queue - une liste contenant les éléments restants (cette liste est également formée d'une tête et d'une queue)

Il existe des méthodes de fabrique statiques dans l'APIList qui peuvent être utilisées pour créer unList. Nous pouvons utiliser la méthode statiqueof() pour créer une instance deList à partir d'un ou plusieurs objets.

Nous pouvons également utiliser lesempty() statiques pour créer unList vide etofAll() pour créer unList à partir d'un typeIterable:

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

Jetons un coup d'œil à quelques exemples de manipulation de listes.

Nous pouvons utiliser lesdrop() et ses variantes pour supprimer les premiers élémentsN:

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 le nombre d'éléments den de la liste à partir du premier élément tandis quedropRight() fait de même à partir du dernier élément de la liste.

dropUntil() continue de supprimer des éléments de la liste jusqu'à ce que le prédicat soit évalué à vrai tandis quedropWhile() continue de supprimer des éléments tant que le prédicat est vrai.

Il y a aussidropRightWhile() etdropRightUntil() qui commencent à supprimer des éléments de la droite.

Ensuite,take(int n) est utilisé pour récupérer des éléments d'une liste. Il prend le nombre d'éléments dendans la liste, puis s'arrête. Il y a aussi untakeRight(int n) qui commence à prendre des éléments à la fin de la liste:

List list5 = list.take(1);
assertEquals(list5.single(), "Java");

List list6 = list.takeRight(1);
assertEquals(list6.single(), "JAVA");

List list7 = list.takeUntil(s -> s.length() > 6);
assertEquals(list7.size(), 3);

Enfin,takeUntil() continue de prendre des éléments de la liste jusqu'à ce que le prédicat soit vrai. Il existe une variantetakeWhile() qui accepte également un argument de prédicat.

De plus, il existe d'autres méthodes utiles dans l'API, par exemple, en fait, ledistinct() qui renvoie une liste d'éléments non dupliqués ainsi que ledistinctBy() qui accepte unComparator pour déterminer l'égalité.

Très intéressant, il y a aussi leintersperse() qui insère un élément entre chaque élément d'une liste. Cela peut être très pratique pour les opérationsString:

List list8 = list
  .distinctBy((s1, s2) -> s1.startsWith(s2.charAt(0) + "") ? 0 : 1);
assertEquals(list8.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 une liste en catégories? Eh bien, il existe également une API pour cela:

Iterator> iterator = list.grouped(2);
assertEquals(iterator.head().size(), 2);

Map> 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);

Legroup(int n) divise unList en groupes d'élémentsn chacun. LegroupdBy() accepte unFunction qui contient la logique de division de la liste et renvoie unMap avec deux entrées -true etfalse.

La clétrue correspond à unList d'éléments qui satisfont la condition spécifiée dans leFunction; la cléfalse correspond à unList d'éléments qui ne le font pas.

Comme prévu, lors de la mutation d'unList, leList d'origine n'est pas réellement modifié. Au lieu de cela, une nouvelle version desList est toujours renvoyée.

Nous pouvons également interagir avec unList en utilisant la sémantique de la pile - récupération des éléments du dernier entré-premier sorti (LIFO). Dans cette mesure, il existe des méthodes API pour manipuler une pile telles quepeek(),pop() etpush():

List intList = List.empty();

List intList1 = intList.pushAll(List.rangeClosed(5,10));

assertEquals(intList1.peek(), Integer.valueOf(10));

List intList2 = intList1.pop();
assertEquals(intList2.size(), (intList1.size() - 1) );

La fonctionpushAll() est utilisée pour insérer une plage d'entiers sur la pile, tandis que lepeek() est utilisé pour obtenir la tête de la pile. Il y a aussi lespeekOption() qui peuvent envelopper le résultat dans un objetOption.

Il existe d'autres méthodes intéressantes et vraiment utiles dans l'interfaceList qui sont bien documentées dans lesJava docs.

4.2. Queue

UnQueue immuable stocke des éléments permettant une extraction premier entré premier sorti (FIFO).

UnQueue se compose en interne de deux listes chaînées, une avantList et une arrièreList. L'avantList contient les éléments qui sont retirés de la file d'attente et l'arrièreList contient les éléments qui sont mis en file d'attente.

Cela permet aux opérationsenqueue etdequeue de s'exécuter dans O (1). Lorsque lesListavant à court d'éléments, lesList’savant et arrière sont échangés et lesListarrière sont inversés.

Créons une file d'attente:

Queue queue = Queue.of(1, 2);
Queue secondQueue = queue.enqueueAll(List.of(4,5));

assertEquals(3, queue.size());
assertEquals(5, secondQueue.size());

Tuple2> result = secondQueue.dequeue();
assertEquals(Integer.valueOf(1), result._1);

Queue tailQueue = result._2;
assertFalse(tailQueue.contains(secondQueue.get(0)));

La fonctiondequeue supprime l'élément head desQueue et renvoie unTuple2<T, Q>. Le tuple contient l'élément head qui a été supprimé comme première entrée et les éléments restants desQueue comme deuxième entrée.

Nous pouvons utiliser lescombination(n) pour obtenir toutes les combinaisons d'élémentsN possibles dans lesQueue:

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

Encore une fois, nous pouvons voir que lesQueue d'origine ne sont pas modifiés lors de la mise en file d'attente / de la suppression des éléments.

4.3. Stream

UnStream est une implémentation d'une liste chaînée paresseuse et est assez différent dejava.util.stream. Contrairement àjava.util.stream, le VavrStream stocke des données et évalue paresseusement les éléments suivants.

Disons que nous avons unStream d'entiers:

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

L'impression du résultat des.toString() sur la console n'affichera queStream(2, ?). Cela signifie que c'est seulement la tête desStream qui a été évaluée alors que la queue n'a pas été évaluée.

L'appel des.get(3) et l'affichage ultérieur du résultat des.tail() renvoieStream(1, 3, 4, ?). Au contraire, sans invoquers.get(3) first qui amène leStream à évaluer le dernier élément - le résultat des.tail() ne sera queStream(1, ?). Cela signifie que seul le premier élément de la queue a été évalué.

Ce comportement peut améliorer les performances et permet d'utiliserStream pour représenter des séquences qui sont (théoriquement) infiniment longues.

VavrStream est immuable et peut êtreEmpty ouCons. UnCons se compose d'un élément head et d'une queueStream calculée paresseusement. Contrairement à unList, pour unStream, seul l'élément head est conservé en mémoire. Les éléments de queue sont calculés à la demande.

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

Stream 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 Java 8Stream, leStream de Vavr est une structure de données permettant de stocker une séquence d'éléments.

Ainsi, il a des méthodes commeget(),append(),insert() et d'autres pour manipuler ses éléments. Lesdrop(),distinct() et certaines autres méthodes considérées précédemment sont également disponibles.

Enfin, démontrons rapidement lestabulate() dans unStream. Cette méthode retourne unStream de longueurn, qui contient des éléments résultant de l'application d'une fonction:

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

Nous pouvons également utiliser leszip() pour générer unStream deTuple2<Integer, Integer>, qui contient des éléments formés en combinant deuxStreams:

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

Stream> s2 = s.zip(List.of(7,8,9));
Tuple2 t1 = s2.get(0);

assertEquals(t1._1().intValue(), 2);
assertEquals(t1._2().intValue(), 7);

4.4. Array

UnArray est une séquence indexée immuable qui permet un accès aléatoire efficace. Il est soutenu par un Javaarray d'objets. Il s'agit essentiellement d'un wrapperTraversable pour un tableau d'objets de typeT.

Nous pouvons instancier unArray en utilisant la méthode statiqueof(). Nous pouvons également générer des éléments de plage en utilisant les méthodes statiquesrange() etrangeBy(). LerangeBy() a un troisième paramètre qui nous permet de définir le pas.

Les méthodesrange() etrangeBy() ne généreront que des éléments à partir de la valeur de début à la valeur de fin moins un. Si nous devons inclure la valeur finale, nous pouvons utiliser lesrangeClosed() ourangeClosedBy():

Array rArray = Array.range(1, 5);
assertFalse(rArray.contains(5));

Array rArray2 = Array.rangeClosed(1, 5);
assertTrue(rArray2.contains(5));

Array rArray3 = Array.rangeClosedBy(1,6,2);
assertEquals(rArray3.size(), 3);

Manipulons les éléments par index:

Array intArray = Array.of(1, 2, 3);
Array newArray = intArray.removeAt(1);

assertEquals(3, intArray.size());
assertEquals(2, newArray.size());
assertEquals(3, newArray.get(1).intValue());

Array array2 = intArray.replace(1, 5);
assertEquals(array2.get(0).intValue(), 5);

4.5. Vector

UnVector est une sorte d'intervalleArray etList fournissant une autre séquence indexée d'éléments qui permet à la fois un accès aléatoire et une modification en temps constant:

Vector intVector = Vector.range(1, 5);
Vector 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 exprimer une séquence de caractères primitifs. Il s'agit essentiellement d'un wrapperString avec l'ajout d'opérations de collecte.

Pour créer unCharSeq:

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. Set

Dans cette section, nous développons diverses implémentations deSet dans la bibliothèque de collections. La particularité de la structure de donnéesSet est qu’elle n’autorise pas les valeurs en double.

Il existe, cependant, différentes implémentations deSet - leHashSet étant celui de base. LesTreeSet n'autorisent pas les éléments en double et peuvent être triés. LeLinkedHashSet conserve l'ordre d'insertion de ses éléments.

Examinons de plus près ces implémentations une par une.

5.1. HashSet

HashSet a des méthodes de fabrique statiques pour créer de nouvelles instances - dont certaines ont été explorées précédemment dans cet article - commeof(),ofAll() et des variations des méthodesrange().

Nous pouvons obtenir la différence entre deux ensembles en utilisant la méthodediff(). De plus, les méthodesunion() etintersect() renvoient l'ensemble d'unions et l'ensemble d'intersections des deux ensembles:

HashSet set0 = HashSet.rangeClosed(1,5);
HashSet set1 = HashSet.rangeClosed(3, 6);

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 set = HashSet.of("Red", "Green", "Blue");
HashSet newSet = set.add("Yellow");

assertEquals(3, set.size());
assertEquals(4, newSet.size());
assertTrue(newSet.contains("Yellow"));

L'implémentation deHashSet est soutenue par unHash array mapped trie (HAMT), qui offre des performances supérieures par rapport à unHashTable ordinaire et sa structure le rend approprié pour sauvegarder une collection persistante.

5.2. TreeSet

UnTreeSet immuable est une implémentation de l'interfaceSortedSet. Il stocke unSet d'éléments triés et est implémenté à l'aide d'arbres de recherche binaires. Toutes ses opérations sont exécutées dans le temps O (log n).

Par défaut, les éléments d'unTreeSet sont triés dans leur ordre naturel.

Créons unSortedSet en utilisant l’ordre de tri naturel:

SortedSet set = TreeSet.of("Red", "Green", "Blue");
assertEquals("Blue", set.head());

SortedSet intSet = TreeSet.of(1,2,3);
assertEquals(2, intSet.average().get().intValue());

Pour ordonner les éléments de manière personnalisée, passez une instance deComparator lors de la création d'unTreeSet. Nous pouvons également générer une chaîne à partir des éléments de l'ensemble:

SortedSet 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 contiennent également une implémentation immuable deBitSet. L'interfaceBitSet étend l'interfaceSortedSet. BitSet peut être instancié à l'aide de méthodes statiques dansBitSet.Builder.

Comme d'autres implémentations de la structure de donnéesSet,BitSet ne permet pas d'ajouter des entrées en double à l'ensemble.

Il hérite des méthodes de manipulation de l'interfaceTraversable. Notez qu'il est différent desjava.util.BitSet de la bibliothèque Java standard. Les données deBitSet ne peuvent pas contenir les valeurs deString.

Voyons comment créer une instanceBitSet en utilisant la méthode de fabriqueof():

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

Nous utilisons lestakeUntil() pour sélectionner les quatre premiers éléments deBitSet. L'opération a renvoyé une nouvelle instance. Notez que letakeUntil() est défini dans l'interfaceTraversable, qui est une interface parent deBitSet.

D'autres méthodes et opérations illustrées ci-dessus, qui sont définies dans l'interfaceTraversable, sont également applicables àBitSet.

6. Map

Une carte est une structure de données clé-valeur. LesMap de Vavr sont immuables et ont des implémentations pourHashMap,TreeMap etLinkedHashMap.

En règle générale, les contrats de carte n'autorisent pas les clés en double, bien qu'il puisse y avoir des valeurs en double mappées à différentes clés.

6.1. HashMap

UnHashMap est une implémentation d'une interfaceMap immuable. Il stocke les paires clé-valeur en utilisant le code de hachage des clés.

LesMap de Vavr utilisentTuple2 pour représenter des paires clé-valeur au lieu d'un typeEntry traditionnel:

Map> 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());

Semblable àHashSet, une implémentationHashMap est soutenue par un trie mappé de tableau de hachage (HAMT) résultant en un temps constant pour presque toutes les opérations.

Nous pouvons filtrer les entrées de carte par clés, en utilisant la méthodefilterKeys() ou par valeurs, en utilisant la méthodefilterValues(). Les deux méthodes acceptent unPredicate comme argument:

Map map1
  = HashMap.of("key1", "val1", "key2", "val2", "key3", "val3");

Map fMap
  = map1.filterKeys(k -> k.contains("1") || k.contains("2"));
assertFalse(fMap.containsKey("key3"));

Map fMap2
  = map1.filterValues(v -> v.contains("3"));
assertEquals(fMap2.size(), 1);
assertTrue(fMap2.containsValue("val3"));

Nous pouvons également transformer les entrées de la carte en utilisant la méthodemap(). Transformons, par exemple,map1 enMap<String, Integer>:

Map 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

UnTreeMap immuable est une implémentation de l'interfaceSortedMap. Semblable àTreeSet, une instanceComparator est utilisée pour trier les éléments d'unTreeMap.

Montrons la création d'unSortedMap:

SortedMap 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 deTreeMap sont triées dans l'ordre naturel des clés. On peut cependant spécifier unComparator qui sera utilisé pour le tri:

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

Comme avecTreeSet, une implémentation deTreeMap est également modélisée en utilisant un arbre, donc ses opérations sont de temps O (log n). Lemap.get(key) renvoie unOption qui encapsule une valeur à la clé spécifiée dans la carte.

7. Interopérabilité avec Java

L'API de collecte est entièrement interopérable avec le cadre de collecte de Java. Voyons comment cela se fait dans la pratique.

7.1. Conversion de Java en Vavr

Chaque implémentation de collection dans Vavr a une méthode de fabrique statiqueofAll() qui prend unjava.util.Iterable. Cela nous permet de créer une collection Vavr à partir d'une collection Java. De même, une autre méthode d'usineofAll() prend directement un JavaStream.

Pour convertir un JavaList en unList immuable:

java.util.List javaList = java.util.Arrays.asList(1, 2, 3, 4);
List vavrList = List.ofAll(javaList);

java.util.stream.Stream javaStream = javaList.stream();
Set vavrSet = HashSet.ofAll(javaStream);

Une autre fonction utile est lecollector() qui peut être utilisé en conjonction avecStream.collect() pour obtenir une collection Vavr:

List 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. Conversion de Vavr en Java

L'interface deValue dispose de nombreuses méthodes pour convertir un type Vavr en type Java. Ces méthodes sont au formattoJavaXXX().

Prenons quelques exemples:

Integer[] array = List.of(1, 2, 3)
  .toJavaArray(Integer.class);
assertEquals(3, array.length);

java.util.Map 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 8Collectors pour collecter des éléments des collections Vavr:

java.util.Set javaSet = List.of(1, 2, 3)
  .collect(Collectors.toSet());

assertEquals(3, javaSet.size());
assertEquals(1, javaSet.toArray()[0]);

7.3. Vues de la collection Java

Sinon, la bibliothèque fournit ce que l'on appelle des vues de collection qui fonctionnent mieux lors de la conversion en collections Java. Les méthodes de conversion de la section précédente parcourent tous les éléments pour créer une collection Java.

Les vues, en revanche, implémentent des interfaces Java standard et des appels de méthodes de délégation à la collection Vavr sous-jacente.

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

L'appel de méthodes de mutation sur une vue immuable entraîne unUnsupportedOperationException.

Regardons un exemple:

@Test(expected = UnsupportedOperationException.class)
public void givenVavrList_whenViewConverted_thenException() {
    java.util.List 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 javaList = List.of(1, 2, 3)
  .asJavaMutable();
javaList.add(4);

assertEquals(4, javaList.get(3).intValue());

8. Conclusion

Dans ce didacticiel, nous avons découvert diverses structures de données fonctionnelles fournies par l'API Collection de Vavr. Il existe des méthodes d’API plus utiles et plus productives que vous pouvez trouver dans les collectionsJavaDoc de Vavr et lesuser guide.

Enfin, il est important de noter que la bibliothèque définit égalementTry,Option,Either etFuture qui étendent l'interfaceValue et implémentent par conséquent Java Interface deIterable. Cela implique qu'ils peuvent se comporter comme une collection dans certaines situations.

Le code source complet de tous les exemples de cet article se trouve àover on Github.