Tutoriel d'introduction aux collections de Guava

Thierry Leriche-Dessirier

Guava est une bibliothèque, de chez Google, proposant de nombreux outils pour améliorer les codes des programmes Java. Elle permet, entre autres, de manipuler les collections, de jouer efficacement avec les immutables, d'éviter la gestion des beans nuls, de s'essayer à la programmation fonctionnelle, de cacher les objets, de les simplifier, et bien d'autres choses…

Dans ce deuxième article sur Guava, nous allons découvrir les fonctionnalités dédiées aux collections. Commentez

- Titre : Tutoriel d'introduction aux collections de Guava

- Auteur : Thierry Leriche-Dessirier

- Parution : 20 août 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Thierry Leriche-Dessirier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Versions des logiciels et bibliothèques utilisés

- I-B. Mises à jour

- II. Collections

- II-A. Utilitaires de collection

- II-A-1. Constructeurs statiques

- II-A-1-a. Collections vides

- II-A-1-b. Avec une capacité

- II-A-1-c. Avec des éléments

- II-A-2. Iterables

- II-A-3. Collections avec un « s » : Lists, Maps, etc.

- II-A-3-a. Lists

- II-A-3-a-i. Reverse

- II-A-3-a-ii. Partition

- II-A-3-a-iii. Characters

- II-A-3-b. Sets

- II-A-3-b-i. Unions et interceptions

- II-A-3-b-ii. Produits cartésiens

- II-A-3-b-iii. Puissance de sets

- II-A-3-c. Maps

- II-A-3-c-i. Diff

- II-B. Immutables

- II-B-1. En Java standard pour commencer

- II-B-2. La manière Guava

- II-B-3. Quand l'utiliser, et comment ?

- II-B-4. Factories/Builders

- II-C. Collections additionnelles

- II-C-1. Multimap

- II-C-2. Multiset

- II-C-3. Bimap

- II-C-4. Table

- II-C-5. RangeSet et RangeMap

- III. Conclusion

- IV. Remerciements

- V. Annexes

- V-A. Liens

- V-B. Liens personnels

I - Introduction

Cet article est le deuxième d'une série consacrée à la bibliothèque Guava :

- introduction et installation ;

- collections ;

- programmation fonctionnelle ;

- utilitaires ;

- cache et concurrence ;

- tout pour vos Strings et primitifs ;

- un peu de math ;

- hash et I/O.

I-A - Versions des logiciels et bibliothèques utilisés

Pour écrire ce document, j'ai utilisé les versions suivantes :

- Java JDK 1.6.0_24-b07 ;

- Eclipse Indigo 3.7 JEE 64b ;

- Maven 3.0.3 ;

- JUnit 4.10 ;

- Guava 14.0.

J'utilise Java 6, car Java 7 n'est pas encore très répandu en entreprise. C'est ce que je vérifie durant mes conférences lorsque je demande qui utilise Java 7 sur ses serveurs de production, mais que très peu de mains se lèvent…

I-B - Mises à jour

20/09/2013 : création

26/09/2013 : prise en compte des bonnes remarques d'Olivier sur le forum

II - Collections

L'ancien nom de Guava était « Google-Collections ». Sans surprise, la bibliothèque est en grande partie consacrée aux collections.

Lorsque Josh Bloch crée l'API Collections en 1997 pour l'intégrer à Java, il n'imagine sans doute pas l'importance qu'elle va prendre. Les collections rencontrent un succès immédiat et sont largement adoptées par la communauté Java. Il faut dire que tout (ou presque) est présent dès le lancement et que, mises à part quelques évolutions importantes comme les Iterators (très tôt), les génériques (Java 5) ou les Lambda (Java 8), l'API n'a quasiment pas changé. C'est dire si Java-Collections a été bien pensée.

Un point très important à propos de l'API Java-Collections est qu'elle est extensible. De nombreux développeurs à travers le monde en ont donc étendu les fonctionnalités. Mais c'est sans doute l'équipe de Kevin Bourrillion qui est allée le plus loin en créant Google-Collections (i.e. Guava).

II-A - Utilitaires de collection

II-A-1 - Constructeurs statiques

Une des premières choses qui saute aux yeux lorsqu'on découvre Guava, c'est la simplicité avec laquelle il est possible de créer des collections (List, Set, Map, etc.) génériques complexes sans subir les inconvénients de la syntaxe verbeuse de Java 5 (avec ses « generics »).

II-A-1-a - Collections vides

En particulier, on doit préciser le type du générique à gauche et à droite du signe « égal » en Java 5 :

Générique à gauche et à droite

List<Chien> chiens = new ArrayList<Chien>();
Map<String, Chien> chiensMap = new HashMap<String, Chien>();
Set<Chien> chienSet = new HashSet<Chien>();

Cette déclaration peut devenir bien compliquée dès qu'on s'amuse avec des types mélangés :

Avec des collections mélangées

Map<String, List<Map<Integer, Set<Chien>>>> col
= new HashMap<String, List<Map<Integer,Set<Chien>>>>();

À l'aide de Guava, on va pouvoir déclarer des collections avec une syntaxe bien plus simple en utilisant des « static facory », comme le recommande d'ailleurs Josh Bloch dans son livre « Effective Java » dès la première page :

Générique à gauche seulement

import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Sets.newHashSet;
...

List<Chien> chiens = newArrayList();
Map<String, Chien> chiensMap = newHashMap();
Set<Chien> chienSet = newHashSet();

Map<String, List<Map<Integer, Set<Chien>>>> col = newHashMap();

Le code est bien plus lisible. Et ce qui se cache derrière cet appel n'a rien de magique :

Méthode newArrayList()

public static <E> ArrayList<E> newArrayList() {
return new ArrayList<E>();
}

Dans la suite, je vous montre des exemples avec des listes pour des raisons de lisibilité, mais c'est le même principe avec les autres types de collection.

Comme vous le savez, Java 7 apporte la syntaxe « Diamant », c'est-à-dire le « Type Inference » sur la création d'instances génériques, comme l'explique F. Martini ici. Or Java 7 est encore très peu utilisé en entreprise (i.e. en Prod) et vous n'aurez donc pas l'occasion d'utiliser le code suivant avant longtemps :

Générique avec Diamant

List<Chien> chiens = new ArrayList<>();

La classe « Lists » permet la création de listes du type :

- ArrayList ;

- LinkedList.

Elle ne propose donc pas (plus) la création de « Vector », mais j'ai le sentiment que personne ne va s'en plaindre.

La classe « Maps » permet la création de maps du type :

- ConcurentMap ;

- EnumMap ;

- HashMap ;

- LinkedHashMap ;

- TreeMap.

La classe "Sets" permet la création de sets du type :

- EnumSet ;

- HashSet ;

- LinkedHashSet ;

- TreeSet.

À lire, un billet de blog intitulé « Consider static factory methods instead of constructors ».

II-A-1-b - Avec une capacité

En outre, vous savez qu'il faut spécifier une capacité (taille de liste) initiale quand c'est possible :

Création d'une liste avec une capacité initiale en Java standard

int taille = 100;
List<Chien> chiens = new ArrayList<Chien>(taille);

Avec Guava, ce n'est pas plus compliqué ; on doit toutefois employer un « static factory » spécifique :

Création d'une liste avec une capacité initiale avec Guava

import static com.google.common.collect.Lists.newArrayListWithCapacity;

int taille = 100;
List<Chien> chiens = newArrayListWithCapacity(taille);

D'ailleurs, bien souvent, on n'a qu'une idée approximative du nombre d'éléments qu'on aura dans la liste. Il vaut donc mieux prévoir un peu plus large pour éviter le coût de l'agrandissement automatique de la liste. Dans ce cas, Guava propose la méthode « newArrayListWithExpectedSize() » de création spécifique (là encore il y a un équivalent pour les autres types de collections) qui se charge de tout :

Création d'une liste avec une capacité approximative avec Guava

import static com.google.common.collect.Lists.newArrayListWithExpectedSize;

int taille = 100;
List<Chien> chiens = newArrayListWithExpectedSize(taille);

Pour les curieux, voici le bout de code concerné :

computeArrayListCapacity

static int computeArrayListCapacity(int arraySize) {
checkArgument(arraySize >= 0); // Verifie que la taille est positive)

return Ints.saturatedCast(5L + arraySize + (arraySize / 10));
}

II-A-1-c - Avec des éléments

Mais ce qui arrive encore plus souvent, c'est qu'on crée des collections dans le but de les remplir immédiatement :

Création d'une liste avec des éléments

List<Chien> chiens = new LinkedList<Chien>();

chiens.add(new Chien("Milou"));
chiens.add(new Chien("Pluto"));
chiens.add(new Chien("Lassie"));
chiens.add(new Chien("Volt"));
chiens.add(new Chien("Rantanplan"));
chiens.add(new Chien("Idefix"));

Dans ce cas, on voudrait créer la liste en une seule instruction, ce qui est possible en Java :

Création d'une liste avec des éléments, en une ligne

List<Chien> chiens = new LinkedList<Chien>(
Arrays.asList(
new Chien("Milou"),
new Chien("Pluto"),
new Chien("Lassie"),
new Chien("Volt"),
new Chien("Rantanplan"),
new Chien("Idefix")));

Avec Guava, ça va être un poil plus simple puisqu'on peut directement passer les éléments au « factory » :

Création d'une liste avec des éléments, en une ligne avec Guava

List<Chien> chiens = newLinkedList(
new Chien("Milou"),
new Chien("Pluto"),
new Chien("Lassie"),
new Chien("Volt"),
new Chien("Rantanplan"),
new Chien("Idefix"));

II-A-2 - Iterables

Guava introduit une classe, nommé « Iterables », dont le nom me semble relativement clair. Son objectif premier est d'offrir des utilitaires pour le type « Iterable ». Comme vous allez le découvrir plus loin, la bibliothèque renvoie généralement des iterables là où on attendrait instinctivement des listes :

Le retour est un Iterable

final List<Chien> chiens1 = newArrayList(new Chien("Milou"), new Chien("Pluto"), new Chien("Lassie"));
final List<Chien> chiens2 = newArrayList(new Chien("Volt"), new Chien("Rantanplan"), new Chien("Idefix"));

final Iterable<Chien> iter = Iterables.concat(chiens1, chiens2);

L'objet « Iterable » renvoyé par la méthode « concat() » possède un « Iterator » qui traverse les éléments de tous les itérables des paramètres.

En outre la plupart des méthodes acceptent des itérables en entrée. C'est notamment le cas de la méthode « newArrayList(..) » qu'on a vu plus haut :

Un Iterable en paramètre

final Iterable<Chien> iter = ...
final List<Chien> chiens = Lists.newArrayList(iter);

Et vous l'avez compris, on peut itérer sur un itérable :

Un test plus parlant

@Test
public void testIterable() {
// Arrange
final List<Chien> chiens1 = newArrayList(new Chien("Milou"), new Chien("Pluto"), new Chien("Lassie"));
final List<Chien> chiens2 = newArrayList(new Chien("Volt"), new Chien("Rantanplan"), new Chien("Idefix"));
final int expected = 6;

// Act
final Iterable<Chien> iter = Iterables.concat(chiens1, chiens2);
for(Chien chien : iter) {
System.out.println(chien);
}
// -> Dog{name=Milou}
//    Dog{name=Pluto}
//    Dog{name=Lassie}
//    Dog{name=Volt}
//    Dog{name=Rantanplan}
//    Dog{name=Idefix}
final List<Chien> chiens = Lists.newArrayList(iter);

// Assert
Assert.assertEquals(expected, chiens.size());
}

Vous remarquez le formalisme AAA (Arrange Act Assert), utilisé dans ce test, que je vous avais déjà présenté dans le cadre de 3T (Tests en Trois Temps).

En plus de « concat(..) » (cf. exemple ci-dessus), qui est déjà pratique, « Iterables » propose de nombreuses méthodes utiles au quotidien. Une partie de ces méthodes ont des équivalents spécialisés pour les listes, maps ou sets. Je vous les présente dans les chapitres dédiés et je vous les épargne donc pour le moment.

On peut compter le nombre d'occurrences d'un élément par exemple, à l'aide de la méthode « frequency(..) » :

Le nombre d'occurrences

@Test
public void testFrequency() {
// Arrange
final List<Chien> chiens = newArrayList(
new Chien("Milou"),  // 1
new Chien("Pluto"),
new Chien("Milou"),  // 2
new Chien("Lassie"),
new Chien("Milou")); // 3
final int nbMilou = 3;

// Act
final int nb = Iterables.frequency(chiens, new Chien("Milou"));

// Assert
Assert.assertEquals(nbMilou, nb);
}

On peut prendre le premier ou le dernier élément d'une collection, ce qui n'est pas si simple avec une liste classique :

Premier ou dernier

@Test
public void testPremierDernier() {
// Arrange
final List<Chien> chiens = newArrayList(//
new Chien("Milou"), //
new Chien("Pluto"), //
new Chien("Lassie"), //
new Chien("Volt"), //
new Chien("Rantanplan"), //
new Chien("Idefix"));
final String expectedPremier = "Milou";
final String expectedDernier = "Idefix";

// Act
final Chien premier = Iterables.getFirst(chiens, new Chien("winner"));
final Chien dernier = Iterables.getLast(chiens, new Chien("looser"));

// Assert
Assert.assertEquals(expectedPremier, premier.getName());
Assert.assertEquals(expectedDernier, dernier.getName());
}

Au passage, vous remarquez que je passe un second argument à ces méthodes, pour servir de valeur par défaut si le premier ou le dernier ne sont pas trouvés (i.e. collection vide).

On peut aussi limiter le nombre d'éléments à l'aide de la méthode « limit(..) » :

Collection de taille réduite

@Test
public void testLimit() {
// Arrange
final List<Chien> chiens = newArrayList(//
new Chien("Milou"), //
new Chien("Pluto"), //
new Chien("Lassie"), //
new Chien("Volt"), //
new Chien("Rantanplan"), //
new Chien("Idefix"));
final int taille = 4;

// Act
final List<Chien> sousListe = newArrayList(Iterables.limit(chiens, taille));

// Assert
Assert.assertEquals(taille, sousListe.size());
}

Et pour finir avec les itérables, je voudrais vous présenter une fonction ultra pratique permettant de tester qu'on a bien les mêmes éléments dans deux collections :

Égalité

@Test
public void testListEgalite() {
// Arrange
final List<Chien> chiens1 = Lists.newArrayList(//
new Chien("Milou"), //
new Chien("Pluto"), //
new Chien("Lassie"), //
new Chien("Volt"), //
new Chien("Rantanplan"), //
new Chien("Idefix"));
final List<Chien> chiens2 = Lists.newArrayList(//
new Chien("Milou"), //
new Chien("Pluto"), //
new Chien("Lassie"), //
new Chien("Volt"), //
new Chien("Rantanplan"), //
new Chien("Idefix"));

// Act
final boolean areEqual = Iterables.elementsEqual(chiens1, chiens2);

// Assert
Assert.assertTrue(areEqual);
}

Ça marche aussi avec des sets mais comme cette méthode teste non seulement les éléments, mais aussi leur ordre, vous risquez d'avoir des mauvaises surprises avec les sets.

II-A-3 - Collections avec un « s » : Lists, Maps, etc.

En plus des « static factory », Guava propose des utilitaires simples pour manipuler les listes, les sets, les maps, etc.

Pour savoir quel utilitaire particulier utiliser, il suffit d'ajouter un « s » à la fin. Par exemple, pour travailler avec un « Set », il faudra utiliser « Sets ».

II-A-3-a - Lists

II-A-3-a-i - Reverse

Quand j'étais à l'école, je m'amusais à renverser les listes. Cela peut avoir beaucoup d'applications pratiques. Bien que ce soit relativement simple à programmer, surtout avec les listes chaînées (doublement en Java). C'est toujours sympa lorsque la bibliothèque le fait pour vous. Et c'est justement ce que permet de faire la méthode « reverse(..) » :

Renverser une liste

@Test
public void testReverse() {

// Arrange
final List<Chien> chiens = newArrayList(
new Chien("Milou"),
new Chien("Pluto"),
new Chien("Lassie"),
new Chien("Volt"),
new Chien("Rantanplan"),
new Chien("Idefix"));
System.out.println(chiens); // -> [Chien [name=Milou], Chien [name=Pluto], Chien [name=Lassie], Chien [name=Volt], Chien [name=Rantanplan], Chien [name=Idefix]]
final String expected = "Idefix";

// Act
final List<Chien> chiens2 = Lists.reverse(chiens);
System.out.println(chiens2); // -> [Chien [name=Idefix], Chien [name=Rantanplan], Chien [name=Volt], Chien [name=Lassie], Chien [name=Pluto], Chien [name=Milou]]
final Chien idefix = chiens2.get(0);

// Assert
Assert.assertEquals(expected, idefix.getName());
}

II-A-3-a-ii - Partition

Une autre fonctionnalité qu'on doit souvent programmer est le partitionnement des listes. Cela sert par exemple à diviser l'affichage d'une liste en plusieurs pages. Avec Guava, on va faire appel à la méthode « partition(..) » :

Partitionner une liste

@Test
public void testPartition() {
// Arrange
final List<Chien> chiens = newArrayList(
new Chien("Milou"),
new Chien("Pluto"),
new Chien("Lassie"),
new Chien("Volt"),
new Chien("Rantanplan"),
new Chien("Idefix"));
final int taille = 5;
final int nbPagesAttendu = 2;
final int taillePage0Attendu = 5;
final int taillePage1Attendu = 1;

// Act
List<List<Chien>> partition = Lists.partition(chiens, taille);

// Assert
Assert.assertEquals(nbPagesAttendu, partition.size());
Assert.assertEquals(taillePage0Attendu, partition.get(0).size());
Assert.assertEquals(taillePage1Attendu, partition.get(1).size());
}

II-A-3-a-iii - Characters

Il est aussi possible de décomposer des strings en liste de caractères, à l'aide de la méthode « charactersOf(..) ». Le split découpe en tableau et non en liste ce qui n'est pas toujours très pratique à manipuler. Par contre, ça évite d'utiliser le wrapper object Character au lieu de « char » :

Partitionner une liste

@Test
public void testCharacters() {
// Arrange
final String texte = "Bonjour";
final Character expected = 'B';

// Act
List<Character> caracteres = Lists.charactersOf(texte);

// Assert
Assert.assertEquals(expected, caracteres.get(0));
}

II-A-3-b - Sets

II-A-3-b-i - Unions et interceptions

Encore une fois, Guava permet d'effectuer facilement des opérations relativement complexes et pourtant courantes. Le cas des unions, pour commencer, est certainement le plus simple :

Unions

@Test
public void testUnion() {
// Arrange
final Set<Chien> chiens1 = ImmutableSet.of(new Chien("Milou"), new Chien("Pluto"), new Chien("Lassie"), new Chien("Volt"));
// -> [Chien [name=Milou], Chien [name=Pluto], Chien [name=Lassie], Chien [name=Volt]]
final Set<Chien> chiens2 = ImmutableSet.of(new Chien("Pluto"), new Chien("Rantanplan"), new Chien("Idefix"));
// -> [Chien [name=Pluto], Chien [name=Rantanplan], Chien [name=Idefix]]
final int expected = 6;

// Act
final Set<Chien> union = Sets.union(chiens1, chiens2);
// -> [Chien [name=Milou], Chien [name=Pluto], Chien [name=Lassie], Chien [name=Volt], Chien [name=Rantanplan], Chien [name=Idefix]]

// Assert
Assert.assertEquals(expected, union.size());
}

Un « ImmutableSet » est tout simplement un « set immutable ». On aurait tout aussi bien pu utiliser un « set standard ».

Ici, le chien « Pluto », qui était dans les deux sets, n'est qu'une seule fois dans l'union.

On peut demander l'inverse, à savoir l'intersection des sets :

Intersections

@Test
public void testIntersection() {
// Arrange
final Set<Chien> chiens1 = ImmutableSet.of(new Chien("Milou"), new Chien("Pluto"), new Chien("Lassie"), new Chien("Volt"));
final Set<Chien> chiens2 = ImmutableSet.of(new Chien("Pluto"), new Chien("Rantanplan"), new Chien("Idefix"));
final int expected = 1;

// Act
final Set<Chien> intersection = Sets.intersection(chiens1, chiens2);
// -> [Chien [name=Pluto]]

// Assert
Assert.assertEquals(expected, intersection.size());
}

À l'opposé, on peut avoir les différences sur les listes. Par contre il faut bien comprendre qu'on va demander les éléments qui sont dans la liste 1, mais pas dans la liste 2. L'ordre des paramètres est donc important :

Diff 1

@Test
public void testDiff1() {
// Arrange
final Set<Chien> chiens1 = ImmutableSet.of(new Chien("Milou"), new Chien("Pluto"), new Chien("Lassie"), new Chien("Volt"));
final Set<Chien> chiens2 = ImmutableSet.of(new Chien("Pluto"), new Chien("Rantanplan"), new Chien("Idefix"));
final int expected = 3;

// Act
final Set<Chien> diff = Sets.difference(chiens1, chiens2);
// -> [Chien [name=Milou], Chien [name=Lassie], Chien [name=Volt]]

// Assert
Assert.assertEquals(expected, diff.size());
}

Diff 2

@Test
public void testDiff2() {
// Arrange
final Set<Chien> chiens1 = ImmutableSet.of(new Chien("Milou"), new Chien("Pluto"), new Chien("Lassie"), new Chien("Volt"));
final Set<Chien> chiens2 = ImmutableSet.of(new Chien("Pluto"), new Chien("Rantanplan"), new Chien("Idefix"));
final int expected = 2;

// Act
final Set<Chien> diff = Sets.difference(chiens2, chiens1);
// -> [Chien [name=Rantanplan], Chien [name=Idefix]]

// Assert
Assert.assertEquals(expected, diff.size());
}

Il est même possible de demander une différence symétrique (équivalent d'un XOR), pour avoir les éléments qui ne sont pas dans les deux sets :

Différence symétrique

@Test
public void testSymetricDiff() {
// Arrange
final Set<Chien> chiens1 = ImmutableSet.of(new Chien("Milou"), new Chien("Pluto"), new Chien("Lassie"), new Chien("Volt"));
final Set<Chien> chiens2 = ImmutableSet.of(new Chien("Pluto"), new Chien("Rantanplan"), new Chien("Idefix"));
final int expected = 5;

// Act
final Set<Chien> diff = Sets.symmetricDifference(chiens1, chiens2);
// -> [Chien [name=Milou], Chien [name=Lassie], Chien [name=Volt], Chien [name=Rantanplan], Chien [name=Idefix]]

// Assert
Assert.assertEquals(expected, diff.size());
}

II-A-3-b-ii - Produits cartésiens

Un produit cartésien associe deux à deux les éléments issus de deux sets. C'est assez simple à fabriquer à l'aide de boucles imbriquées par exemple, mais c'est mieux quand c'est la bibliothèque qui s'en occupe :

Produit cartésien

@Test
public void testCartesien() {
// Arrange
final Set<Chien> chiens1 = ImmutableSet.of(new Chien("Milou"), new Chien("Pluto"), new Chien("Lassie"), new Chien("Volt"));
final Set<Chien> chiens2 = ImmutableSet.of(new Chien("Pluto"), new Chien("Rantanplan"), new Chien("Idefix"));
final int expected = 12; // 12 = 4 x 3

// Act
final Set<List<Chien>> cartesian = Sets.cartesianProduct(chiens1, chiens2);
// -> [[Chien [name=Milou], Chien [name=Pluto]], [Chien [name=Milou], Chien [name=Rantanplan]],
//     [Chien [name=Milou], Chien [name=Idefix]], [Chien [name=Pluto], Chien [name=Pluto]],
//     [Chien [name=Pluto], Chien [name=Rantanplan]], [Chien [name=Pluto], Chien [name=Idefix]],
//     [Chien [name=Lassie], Chien [name=Pluto]], [Chien [name=Lassie], Chien [name=Rantanplan]],
//     [Chien [name=Lassie], Chien [name=Idefix]], [Chien [name=Volt], Chien [name=Pluto]],
//     [Chien [name=Volt], Chien [name=Rantanplan]], [Chien [name=Volt], Chien [name=Idefix]]]

// Assert
Assert.assertEquals(expected, cartesian.size());
}

Bien entendu, on peut faire participer plus de deux sets au produit cartésien :

Produit cartésien avec trois sets

@Test
public void testCartesienTriple() {
// Arrange
final Set<Chien> chiens1 = ImmutableSet.of(new Chien("Milou"), new Chien("Pluto"), new Chien("Lassie"), new Chien("Volt"));
final Set<Chien> chiens2 = ImmutableSet.of(new Chien("Pluto"), new Chien("Rantanplan"), new Chien("Idefix"));
final Set<Chien> chiens3 = ImmutableSet.of(new Chien("Volt"), new Chien("Medor"));
final int expected = 24; // 24 = 4 x 3 x 2

// Act
final Set<List<Chien>> cartesian = Sets.cartesianProduct(chiens1, chiens2, chiens3);
// -> [[Chien [name=Milou], Chien [name=Pluto]], [Chien [name=Milou], Chien [name=Rantanplan]], [Chien [name=Milou], Chien [name=Idefix]],
//     [Chien [name=Pluto], Chien [name=Pluto]], [Chien [name=Pluto], Chien [name=Rantanplan]], [Chien [name=Pluto], Chien [name=Idefix]],
//     [Chien [name=Lassie], Chien [name=Pluto]], [Chien [name=Lassie], Chien [name=Rantanplan]], [Chien [name=Lassie], Chien [name=Idefix]],
//     [Chien [name=Volt], Chien [name=Pluto]], [Chien [name=Volt], Chien [name=Rantanplan]], [Chien [name=Volt], Chien [name=Idefix]]]

// Assert
Assert.assertEquals(expected, cartesian.size());
}

II-A-3-b-iii - Puissance de sets

Le concept de puissance est un peu difficile à appréhender. Il correspond à l'ensemble des combinaisons que l'on peut fabriquer avec les éléments d'un set. Je suis certain que vous vous êtes déjà cassé les dents à programmer ça alors dites merci à Guava de le faire dorénavant pour vous :

Puissance

@Test
public void testPuissance() {
// Arrange
final Set<Chien> chiens = ImmutableSet.of(new Chien("Milou"), new Chien("Pluto"), new Chien("Lassie"), new Chien("Volt"));
final int expected = 16;

// Act
final Set<Set<Chien>> power = Sets.powerSet(chiens);

// Assert
Assert.assertEquals(expected, power.size());
}

Comme c'est un peu délicat, je vous ai fait une sortie console pour bien comprendre comment sont constituées les combinaisons, qui incluent en particulier un set vide :

Print

for(Set<Chien> s : power) {
System.out.println("*" + s);
}

Print

*[]
*[Chien [name=Milou]]
*[Chien [name=Pluto]]
*[Chien [name=Milou], Chien [name=Pluto]]
*[Chien [name=Lassie]]
*[Chien [name=Milou], Chien [name=Lassie]]
*[Chien [name=Pluto], Chien [name=Lassie]]
*[Chien [name=Milou], Chien [name=Pluto], Chien [name=Lassie]]
*[Chien [name=Volt]]
*[Chien [name=Milou], Chien [name=Volt]]
*[Chien [name=Pluto], Chien [name=Volt]]
*[Chien [name=Milou], Chien [name=Pluto], Chien [name=Volt]]
*[Chien [name=Lassie], Chien [name=Volt]]
*[Chien [name=Milou], Chien [name=Lassie], Chien [name=Volt]]
*[Chien [name=Pluto], Chien [name=Lassie], Chien [name=Volt]]
*[Chien [name=Milou], Chien [name=Pluto], Chien [name=Lassie], Chien [name=Volt]]

Bien entendu, il ne faut pas oublier que ce sont des sets. Les éléments ne sont donc pas en double.

II-A-3-c - Maps

II-A-3-c-i - Diff

Le « diff » sur des maps s'apparente aux unions des sets. La grosse différence vient du fait que les maps travaillent sur des « entries » (couples clé-valeur) et non sur de simples valeurs. La première opération qu'on peut effectuer est de déterminer les entrées en commun :

Entrées en commun

@Test
public void testCommonEntries() {
// Arrange
final Map<String, Chien> chiens1 = ImmutableMap.of(
"M", new Chien("Milou"),
"P", new Chien("Pluto"),
"L", new Chien("Lassie"));
final Map<String, Chien> chiens2 = ImmutableMap.of(
"V", new Chien("Volt"),
"M", new Chien("Medor"),
"P", new Chien("Pluto"),
"I", new Chien("Idefix"));

final int nbCommon = 1;

// Act
final MapDifference<String, Chien> diff = Maps.difference(chiens1, chiens2);
final Map<String, Chien> common = diff.entriesInCommon();
// -> {P=Chien [name=Pluto]}

// Assert
Assert.assertEquals(nbCommon, common.size());
}

Quand une clé est présente dans les deux maps, la fonction sait se souvenir des valeurs associées :

Entrées similaires

@Test
public void testSimilarEntries() {
// Arrange
final Map<String, Chien> chiens1 = ImmutableMap.of(
"M", new Chien("Milou"),
"P", new Chien("Pluto"),
"L", new Chien("Lassie"));
final Map<String, Chien> chiens2 = ImmutableMap.of(
"V", new Chien("Volt"),
"M", new Chien("Medor"),
"P", new Chien("Pluto"),
"I", new Chien("Idefix"));

final int nbSimilar = 1;

// Act
final MapDifference<String, Chien> diff = Maps.difference(chiens1, chiens2);
final Map<String, ValueDifference<Chien>> similar = diff.entriesDiffering();
// -> {M=(Chien [name=Milou], Chien [name=Medor])}

// Assert
Assert.assertEquals(nbSimilar, similar.size());
}

À l'opposé, il est simple de déterminer les entrées présentes uniquement dans l'une ou l'autre des deux maps :

Only on left/right

@Test
public void testOnlyOnLeftRight() {
// Arrange
final Map<String, Chien> chiens1 = ImmutableMap.of(
"M", new Chien("Milou"),
"P", new Chien("Pluto"),
"L", new Chien("Lassie"));
final Map<String, Chien> chiens2 = ImmutableMap.of(
"V", new Chien("Volt"),
"M", new Chien("Medor"),
"P", new Chien("Pluto"),
"I", new Chien("Idefix"));

final int nbLeft = 1;
final int nbRight = 2;

// Act
final MapDifference<String, Chien> diff = Maps.difference(chiens1, chiens2);
Map<String, Chien> onlyOnLeft = diff.entriesOnlyOnLeft();
// -> {L=Chien [name=Lassie]}
Map<String, Chien> onlyOnRight = diff.entriesOnlyOnRight();
// -> {V=Chien [name=Volt], I=Chien [name=Idefix]}

// Assert
Assert.assertEquals(nbLeft, onlyOnLeft.size());
Assert.assertEquals(nbRight, onlyOnRight.size());
}

II-B - Immutables

À mes yeux, les immutables constituent un des plus gros/importants apports de Guava. Pour faire simple, un immutable est une liste (ou plus généralement une collection) constante.

Si vous vous intéressez à la programmation fonctionnelle, il est important de comprendre ce chapitre.

Pour paraphraser le Wiki de Guava, les immutables ont de nombreux avantages, à commencer par :

- une plus grande résistance face aux bibliothèques tierces qu'on ne contrôle pas, dont on ne sait pas très bien ce qu'elles font sans forcément que ce soit un problème de confiance, pour lesquelles on utilisera des stratégies de défense ;

- elles sont « thread-safe » ;

- elles n'ont pas à supporter les mécanismes de mutation, ce qui permet de gagner en temps, performances et espace ;

- et ce sont surtout des constantes, qui resteront des constantes à la différence des unmodifiables (cf. ci-dessous).

II-B-1 - En Java standard pour commencer

En Java, il est déjà « possible » de créer des collections dites « unmodifiable » (notez que je ne dis pas « immutables ») à l'aide de méthodes comme « unmodifiableSet() » :

Création d'un Set unmodifiable avec les premiers nombres premiers

Set<Integer> temp = new LinkedHashSet<Integer>(
Arrays.asList(1, 2, 3, 5, 7));
Set<Integer> primes = Collections.unmodifiableSet(temp);

Dans l'exemple ci-dessus, je crée la liste (enfin le Set pour être exact) des premiers nombres premiers. Cette collection a toutes les raisons d'être non modifiable, c'est-à-dire une constante.

Oui oui, comme le dit Wikipedia (ici), les nombres zéro et un ne sont ni premiers ni composés.

On aurait pu aussi reprendre l'exemple des chiens, présenté dans le chapitre sur les « static factory » :

Création d'un Set unmodifiable de chien

Set<Chien> temp = new LinkedHashSet<Chien>(
Arrays.asList(
new Chien("Milou"),
new Chien("Pluto"),
new Chien("Lassie"),
new Chien("Volt"),
new Chien("Rantanplan"),
new Chien("Idefix")));
Set<Chien> chiens = Collections.unmodifiableSet(temp);

Vous remarquez bien entendu comme c'est compliqué. On doit en premier passer par un tableau, le transformer en liste, puis en set dans une variable temporaire, et enfin fabriquer une « vue » non modifiable… C'est trop lourd.

Mais, surtout, je voudrais attirer votre attention sur un point qui me semble essentiel. Pour cela, revenons à la « vue » unmodifiable et sortons la déclaration de nos chiens :

Les chiens

Chien milou = new Chien("Milou");
Chien pluto = new Chien("Pluto");
Chien lassie = new Chien("Lassie");
Chien volt = new Chien("Volt");
Chien rantanplan = new Chien("Rantanplan");
Chien idefix = new Chien("Idefix");

Création d'un Set unmodifiable de chien

Set<Chien> temp = new LinkedHashSet<Chien>(
Arrays.asList(milou, pluto,lassie, volt, rantanplan, idefix));
Set<Chien> chiens = Collections.unmodifiableSet(temp);

Pour bien marquer le coup, je vais tout marquer en « final » :

Création d'un Set unmodifiable de chien final

final Set<Chien> temp = new LinkedHashSet<Chien>(
Arrays.asList(milou, pluto,lassie, volt, rantanplan, idefix));
final Set<Chien> chiens = Collections.unmodifiableSet(temp);

À ce stade, comme je l'explique en conférence, le développeur dispose d'une « vue » unmodifiable et se pense donc à l'abri. C'est d'ailleurs pour cela qu'il s'est infligé autant de code barbare. En réalité, ce que vous avez devant les yeux, c'est sans doute l'une des erreurs les plus importantes que vous pouvez faire en Java.

En effet, vous avez bien une « vue » qu'on ne peut plus modifier. Pour le vérifier, écrivons un petit test unitaire :

Exception lors de la modification

@Test(expected = UnsupportedOperationException.class)
public void testUnmodifiableAvecErreur() {

final Set<Chien> temp = new LinkedHashSet<Chien>(
Arrays.asList(milou, pluto, lassie, volt, rantanplan, idefix));
final Set<Chien> chiens = Collections.unmodifiableSet(temp);

chiens.add(new Chien("Belle")); // UOE
}

Le développeur se sent donc en confiance pour fournir cette « vue » au reste du code. La collection initiale venait peut-être elle-même d'une autre couche applicative dont le développeur n'avait pas le contrôle total. Et là, c'est le drame, car, s'il est vrai que la « vue » ne peut pas être modifiée, qu'en est-il de la collection temporaire ? Testons cela :

Tout se passe bien...

@Test
public void testUnmodifiableSansErreur() {

Set<Chien> temp = new LinkedHashSet<Chien>(
Arrays.asList(milou, pluto, lassie, volt, rantanplan, idefix));
final Set<Chien> chiens = Collections.unmodifiableSet(temp);

System.out.println("Chiens : " + chiens);
assertEquals(6, chiens.size());

temp.add(new Chien("Belle"));
System.out.println("Chiens : " + chiens);
assertEquals(7, chiens.size());
}

Sortie console

Chiens : [Chien [name=Milou], Chien [name=Pluto], Chien [name=Lassie], Chien [name=Volt], Chien [name=Rantanplan], Chien [name=Idefix] ]
Chiens : [Chien [name=Milou], Chien [name=Pluto], Chien [name=Lassie], Chien [name=Volt], Chien [name=Rantanplan], Chien [name=Idefix], Chien [name=Belle] ]

Eh oui, ça passe (au delta du « final » que j'ai enlevé pour des raisons de compilation, mais ça correspond au cas décrit) sans râler. Et finalement, la « vue » unmodifiable ne fait que casser les pieds au pauvre développeur et ne le protège en réalité de rien.

II-B-2 - La manière Guava

Avec Guava, il suffit d'indiquer que l'on souhaite un set immutable. Reprenons donc l'exemple des premiers :

Création d'un Set immutable avec les premiers nombres premiers

Set<Integer> primes = ImmutableSet.of(1, 2, 3, 5, 7);

Je crois que ça saute aux yeux que c'est plus simple. Et si on reprend l'exemple avec les chiens pour vraiment comparer, c'est encore plus flagrant :

Création d'un Set immutable de chien

Set<Chien> chien = ImmutableSet.of(
new Chien("Milou"),
new Chien("Pluto"),
new Chien("Lassie"),
new Chien("Volt"),
new Chien("Rantanplan"),
new Chien("Idefix"));

Et si les chiens sont déjà créés :

Ou même

Set<Chien> chien = ImmutableSet.of(milou, pluto, lassie, volt, rantanplan, idefix);

Il n'y a pas photo ; c'est plus simple. Revenons à cette histoire de « vue » qui ne protège de rien. Cette fois, nous utilisons les immutables de Guava :

Exception sur l'ajout

@Test(expected = UnsupportedOperationException.class)
public void testImmutableEnErreur() {

Set<Chien> chiens = ImmutableSet.of(milou, pluto, lassie, volt,    rantanplan, idefix);

System.out.println("Chiens : " + chiens);

chiens.add(new Chien("Belle")); // UOE
System.out.println("Chiens : " + chiens);
}

Sans surprise, ça lance une exception. Mais recentrons-nous sur le même cas, c'est-à-dire avec une liste qui viendrait d'ailleurs :

Avec une liste qui vient d'autre part...

@Test
public void testImmutableListeFournie() {

List<Chien> chiensVenusDautrePart = Lists.newArrayList(milou, pluto, lassie, volt, rantanplan, idefix);

Set<Chien> chiens = ImmutableSet.copyOf(chiensVenusDautrePart);

System.out.println("Chiens : " + chiens);

chiensVenusDautrePart.add(new Chien("Belle"));
System.out.println("Chiens : " + chiens);
}

Sortie console

Chiens : [Chien [name=Milou], Chien [name=Pluto], Chien [name=Lassie], Chien [name=Volt], Chien [name=Rantanplan], Chien [name=Idefix] ]
Chiens : [Chien [name=Milou], Chien [name=Pluto], Chien [name=Lassie], Chien [name=Volt], Chien [name=Rantanplan], Chien [name=Idefix] ]

Ce qu'il faut bien voir ici, c'est que l'ajout à la liste initiale s'est effectué sans erreur sans que ça se répercute sur la collection immutable. Pour le coup, on a donc bien une constante.

Un autre point que vous auriez pu remarquer, c'est que je pars d'une liste pour avoir un set immutable à la fin, ceci à l'aide de la méthode « copyOf() » qui va prendre la liste pour en faire une « vraie » copie et mettre dans le set. Notez à quel point c'est facile.

La méthode « copyOf() » est relativement intelligente. Vous allez vous en servir à chaque fois que vous aurez besoin d'une stratégie de défense (ici une copie de défense). Si on lui passe une collection standard, elle transforme ça en immutable en faisant une copie. Si au contraire, on lui passe une collection déjà immutable, elle renvoie la collection (dans le bon type) directement. Eh oui, on ne va pas faire une copie d'une constante, ça serait dommage…

Au passage, puisqu'on s'amuse avec des sets depuis quelques paragraphes, notez que Guava sait repérer les doublons et les gère sans erreur. Ainsi les deux lignes suivantes sont équivalentes :

Avec doublons

Set<Chien> chiens = ImmutableSet.of(
milou, pluto, lassie, volt, rantanplan, idefix, milou, milou, milou);

Sans doublon

Set<Chien> chiens = ImmutableSet.of(
milou, pluto, lassie, volt, rantanplan, idefix);

Ce qu'un simple test vous permettra de vérifier :

Vérification

@Test
public void testImmutableAvecDoublon() {

Set<Chien> chiens = ImmutableSet.of(
milou, pluto, lassie, volt, rantanplan, idefix, milou, milou, milou);

Assert.assertEquals(6, chiens.size());
}

En revanche, la méthode n'accepte pas d'objet null :

Exception sur l'ajout d'un objet null

@Test
public void testImmutableAvecNull() {

Set<Chien> chiens = ImmutableSet.of(
milou, pluto, lassie, volt, rantanplan, idefix, null); // NPE
}

En fait, si vous avez besoin d'une collection immutable pouvant éventuellement contenir un item nul, le mieux est de regarder du côté de « Optional » (cf. plus bas).

II-B-3 - Quand l'utiliser, et comment ?

On se demande depuis très longtemps quand utiliser des collections immutables, en partant du principe que les collections Java sont mutables par défaut. En réalité, c'est un peu prendre le problème à l'envers. La vraie question qu'on devrait se poser serait plutôt « quand a-t-on besoin d'utiliser des collections mutables ? »

Un exemple vous parlera plus qu'une longue explication. Mon application fait des requêtes en base de données. Ça ramène une liste de résultats que je vais afficher. À aucun moment je ne vais modifier la liste. Pourquoi aurais-je donc besoin d'une liste mutable, avec toutes les contraintes que cela entraîne au niveau de la mémoire, de la synchronisation, etc. ? Une liste immutable est bien plus simple et, surtout, beaucoup plus sure.

Voici un autre exemple, que j'ai déjà en partie abordé. Dans un programme, il arrive souvent qu'on utilise des techniques de défense, comme les copies. On ne sait pas forcément très bien où ont traîné les collections et on ne sait pas mieux ce que vont en faire les autres parties du code auxquelles on les fournit. En outre, on a vu que la solution des « unmodifiables » est en vérité une fausse bonne idée.

Illustrons cela en revenant à la classe « Chien » au constructeur de laquelle on veut passer une liste de couleurs préférées, ce qui donne à peu près le code suivant :

Chien

public class Chien {
private String name;
...
private List<String> colors;

public Chien(String name, List<String> colors) {
this.name = name;
...
this.colors = Collections.unmodifiableList( new ArrayList<String>(colors));
}

public List<String> getColors() {
return colors;
}

Avec Guava, on va faire plusieurs choses. D'abord, même si ça avait été possible, on ne va plus travailler avec des interfaces « List », mais avec « ImmutableList » directement. L'idée est de faire passer un message clair au reste du programme en annonçant la couleur (sans jeu de mots) : cette liste sera une constante… Ça en choquera certains, à commencer par moi, mais c'est vraiment une bonne pratique dans ce cas. Et c'est justement le cas dans lequel on doit le faire (de ne pas utiliser l'interface), car on indique un comportement spécifique.

Ensuite, on va donc utiliser la méthode « copyOf() » dont on a parlé plus haut, ce qui va donner :

Chien

public class Chien {
private String name;
...
private ImmutableList<String> colors;

public Chien(String name, List<String> colors) {
this.name = name;
...
this.colors = ImmutableList.copyOf(colors);
}

public ImmutableList<String> getColors() {
return colors;
}

Ce n'est pas toujours une bonne pratique d'utiliser « copyOf() ». En effet, cette méthode réalise une copie qui prend de la place en mémoire, encombre donc d'autant le GC, et consomme du temps. Si vous travaillez avec des collections en lesquelles vous avez confiance, alors autant vous contenter d'une « vue » unmodifiable, qui ne fera pas de copie, surtout si ladite collection est grosse…

Vous pouvez aussi utiliser la méthode « asList() », présente dans toutes les classes immutables et bien pratique :

As list

ImmutableSet<Chien> chiens = ImmutableSet.of(
milou, pluto, lassie, volt, rantanplan, idefix);

List<Chien> list = chiens.asList();

II-B-4 - Factories/Builders

La méthode statique « of() » vient avec un ensemble de surcharges :

Of()

ImmutableSet.of(E e1)
ImmutableSet.of(E e1, E e2)
ImmutableSet.of(E e1, E e2, E e3)
ImmutableSet.of(E e1, E e2, E e3, E e4)
ImmutableSet.of(E e1, E e2, E e3, E e4, E e5)
ImmutableSet.of(E e1, E e2, E e3, E e4, E e5, E e6, E...)

Vous noterez « l'overload » à partir du septième élément, mais j'imagine que ça vous est familier si vous êtes arrivé jusqu'ici. Mais il y a surtout une méthode sans argument :

Of vide

ImmutableSet.of()

À quoi peut donc bien servir une méthode statique qui crée une collection vide, qui sera immutable et par conséquent une constante qui n'évoluera pas ?… Tout simplement à créer une collection vide et constante. Mais à quoi cela sert-il ? Ça peut servir, par exemple, à dire qu'une requête en base de données ne renvoie rien, sans que cela soit une erreur. Je trouve cela mieux que de renvoyer une valeur nulle, ou pire…

Guava aime aussi particulièrement les « builders » et en propose naturellement pour les immutables. Voici ce que cela donne en reprenant l'exemple des chiens :

Builder

Set<Chien> chiens = ImmutableSet.<Chien>builder()
.addAll(chiensVenusDautrePart)
.add(new Chien("Belle"))
.build();

Vous pouvez deviner que c'est assez sympa à utiliser, par exemple pour fusionner des collections diverses, tout en ajoutant des éléments au passage.

Guava propose des équivalents immutables pour toutes les collections « intéressantes » du JDK. Il suffit généralement de préfixer le nom de la collection cible par « Immutable » :

- ImmutableCollection pour Collection ;

- ImmutableList pour List ;

- ImmutableSet pour Set ;

- ImmutableSortedSet pour SortedSet/NavigableSet ;

- ImmutableMap pour Map ;

- ImmutableSortedMap pour SortedMap.

Mais Guava propose aussi des versions immutables des collections additionnelles (cf. plus bas) où, là encore, il suffit de préfixer le nom par « Immutable » :

- ImmutableMultiset pour Multiset ;

- ImmutableSortedMultiset pour SortedMultiset ;

- ImmutableMultimap pour Multimap ;

- ImmutableListMultimap pour ListMultimap ;

- ImmutableSetMultimap pour SetMultimap ;

- ImmutableBiMap pour BiMap ;

- ImmutableClassToInstanceMap pour ClassToInstanceMap ;

- ImmutableTable pour Table.

À lire, un billet de blog intitulé « Java Unmodifiables Vs Guava Immutables ».

II-C - Collections additionnelles

Guava propose de nouveaux types de collections, évidemment compatibles avec les types classiques, qui ajoutent des fonctionnalités vraiment utiles et font passer des messages clairs dans les programmes.

II-C-1 - Multimap

Les maps sont utiles pour travailler avec des ensembles de clé-valeur. Mais les maps ont une limitation importante : on ne peut associer qu'une seule valeur à une clé donnée.

Ce qu'on voudrait, c'est associer une liste à une clé dans la map et c'est justement ce qu'est une multimap. Concrètement, une multimap est une map de listes ou plus généralement une map de collections.

En Java standard, c'est relativement simple à faire. Prenons par exemple les couleurs préférées de nos chiens :

Map de liste en Java

Map<Chien, List<String>> favoriteColors = new HashMap<Chien, List<String>>();

Chien milou = new Chien("Milou");

List<String> milouFavoriteColors = new ArrayList<String>();
milouFavoriteColors.add("Jaune");
milouFavoriteColors.add("Rouge");
milouFavoriteColors.add("Bleu");

favoriteColors.put(milou, milouFavoriteColors);

Ce n'est pas très complexe. Par contre c'est un peu casse-pied quand il s'agit de modifier les valeurs, si bien qu'on met ça comme on peut dans une classe utilitaire au début du projet et on n'y revient plus. En général, ça ressemble au code suivant :

Ajout d'une couleur

public void addColor(Map<Chien, List<String>> map, Chien chien, String color) {

if (color == null || color.isEmpty()) {
throw new IllegalArgumentException("La couleur ne peut pas etre nulle ou vide.");
}

List<String> colors = map.get(chien);
if (colors == null) {
colors = new ArrayList<String>();
map.put(chien, colors);
}
colors.add(color);
}

Ça se complique… Et là je ne vous parle même pas de supprimer une couleur et encore moins d'assurer le multithread.

Avec Guava, on va utiliser directement une « Multimap » qui prendra en charge tous les aspects techniques. Cela fera également passer un message clair au reste du programme », ceci n'est pas une simple map mais une map de collections. C'est important :

Multimap de Guava

Multimap<Chien, String> multimap = HashMultimap.create();

Chien milou = new Chien("Milou");

multimap.put(milou, "Jaune");
multimap.put(milou, "Rouge");
multimap.put(milou, "Bleu");

Au passage, vous remarquez que je définis une multimap de Chien et de String, sans faire référence à une liste, ce qui rend le code d'autant plus lisible.

Contrairement à Map.get(clé) qui retourne null si la clé n'est pas trouvée, MultiMap.get(clé) renvoie une collection vide si la clé n'est pas trouvée. Il faut dire que les gens de Google n'aiment pas vraiment les valeurs nulles en retour.

Quand on récupère une collection à partir de sa clé, celle-ci reste connectée à la multimap, comme le prouve l'exemple suivant :

Modification de la collection

HashMultimap<Chien, String> multimap = HashMultimap.create();

Chien milou = new Chien("Milou");

multimap.put(milou, "Jaune");
multimap.put(milou, "Rouge");
multimap.put(milou, "Bleu");

// Ajout d'un element dans le set
Set<String> colors = multimap.get(milou);
colors.add("Blanc");

// On voit que la multimap est mofifiee
Set<String> colors2 = multimap.get(milou);

// Assert
Assert.assertEquals(4, colors2.size());

II-C-2 - Multiset

Les MultiSets représentent la réponse de Guava à un manque de Java concernant les Sets. En effet, les sets, en Java, sont des listes sans ordre significatif qui ne contiennent pas de doublon. Ce qui est important ici, c'est que ce soit sans ordre significatif et sans doublon. Java propose aussi les listes qui sont ordonnées et peuvent contenir des doublons. Mais il n'y a aucune solution pour des listes non ordonnées avec doublon, et c'est justement ce à quoi correspondent les MultiSets.

Avec un multiset, il est possible d'ajouter plusieurs fois la même valeur :

Construction d'une MultiSet avec 3 en double

Multiset<Integer> premiers = HashMultiset.create();
premiers.add(2);
premiers.add(3);
premiers.add(7);
premiers.add(11);
premiers.add(3);
premiers.add(5);

System.out.println(premiers);
// -> [2, 3 x 2, 5, 7, 11]

Comment comprendre ce résultat ? Le chiffre « 3 » ajouté en double, a été enregistré [..] en double, d'où le résultat « 3 x 2 ». Quant au fait que la liste renvoyée par « premiers.elementSet() » soit ordonnée, ce n'est pas lié au MultiSet.

Comme vous l'avez sans doute déjà compris, les multisets retiennent des informations sur les éléments ajoutés plusieurs fois :

Compte le nombre d'occurrences

@Test
public void testCountElement3() {

// Arrange
final int target = 3;
final int nb = 2;

// Act
Multiset<Integer> multiset = HashMultiset.create();
multiset.add(2);
multiset.add(3);
multiset.add(7);
multiset.add(11);
multiset.add(3);
multiset.add(5);
// -> [2, 3 x 2, 5, 7, 11]

// Assert
}

II-C-3 - Bimap

Un peu à l'opposé des MultiMaps, Guava propose les BiMaps qui garantissent non seulement l'unicité des clés (comme une Map classique), mais aussi celle des valeurs. Cette bimap peut s'utiliser dans les deux sens puisque les valeurs uniques sont vues comme des clés : clé-valeur-clé. C'est donc une map bijective, d'où le nom.

Tatouages, forcément uniques, des chiens

BiMap<String, Chien> tatouages = HashBiMap.create();

tatouages.put("ABC123", new Chien("Milou"));
tatouages.put("ZXW987", new Chien("Pluto"));
// -> {ZXW987=Dog{name=Pluto}, ABC123=Dog{name=Milou}}

Comme la map est bijective, on peut donc l'inverser, de sorte que les clés deviennent les valeurs et que les valeurs deviennent les clés. Je précise que la méthode « inverse() » renvoie une vue sans modifier la Bimap d'origine :

Inversion

tatouages.inverse();
// -> {Dog{name=Pluto}=ZXW987, Dog{name=Milou}=ABC123}

Je vous garantis que ce n'est pas une partie de plaisir de programmer ça proprement en Java standard.

Pour le reste, une bimap fonctionne comme une map classique. Il est possible de changer la valeur associée à une clé, mais pas d'avoir deux clés avec la même valeur.

Exception sur une valeur en double

@Test(expected = IllegalArgumentException.class)
public void testDouble() {

BiMap<String, Chien> tatouages = HashBiMap.create();

tatouages.put("ABC123", new Chien("Milou"));
tatouages.put("ZXW987", new Chien("Pluto"));
tatouages.put("Milou", new Chien("Milou")); // IAE

}

II-C-4 - Table

On peut voir les Tables Guava comme des maps utilisant une combinaison de deux clés, ce qui peut être assez pratique :

Ville Age Chien en Table

Table<String, Integer, Chien> villeAgeChiens = HashBasedTable.create();

villeAgeChiens.put("Paris", 5, new Chien("Milou"));
villeAgeChiens.put("Paris", 12, new Chien("Pluto"));
villeAgeChiens.put("Marseille", 5, new Chien("Lassie"));
villeAgeChiens.put("New York", 7, new Chien("Volt"));
villeAgeChiens.put("Berlin", 5, new Chien("Idefix"));

Cette combinaison permet donc de travailler de la même manière qu'avec un tableau, mais en utilisant la puissance des maps. On peut par exemple rechercher les valeurs par combinaison, mais aussi par ligne ou par colonne :

En ligne

@Test
public void testLignes() {

// Arrange
final int nb = 2;

// Act
Table<String, Integer, Chien> villeAgeChiens = HashBasedTable.create();

villeAgeChiens.put("Paris", 5, new Chien("Milou"));
villeAgeChiens.put("Paris", 12, new Chien("Pluto"));
villeAgeChiens.put("Marseille", 5, new Chien("Lassie"));
villeAgeChiens.put("New York", 7, new Chien("Volt"));
villeAgeChiens.put("Berlin", 5, new Chien("Idefix"));

final Map<Integer, Chien> rows = villeAgeChiens.row("Paris");
// -> keys: 5, 12

// Assert
Assert.assertEquals(nb, rows.size());
}

Sélection d'une ligne

En colonne

@Test
public void testColonnes() {

// Arrange
final int nb = 3;

// Act
Table<String, Integer, Chien> villeAgeChiens = HashBasedTable.create();

villeAgeChiens.put("Paris", 5, new Chien("Milou"));
villeAgeChiens.put("Paris", 12, new Chien("Pluto"));
villeAgeChiens.put("Marseille", 5, new Chien("Lassie"));
villeAgeChiens.put("New York", 7, new Chien("Volt"));
villeAgeChiens.put("Berlin", 5, new Chien("Idefix"));

final Map<String, Chien> cols = villeAgeChiens.column(5);
// -> keys: Paris, Marseille, Berlin

// Assert
Assert.assertEquals(nb, cols.size());
}

Sélection d'une colonne

II-C-5 - RangeSet et RangeMap

Un rangeset est un set constitué d'intervalles. L'intérêt de cette collection est qu'elle est capable de lier les intervalles qui sont connectés. On ajoute ces intervalles au rangeset en précisant s'ils sont ouverts ou fermés :

Intervalles

RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 5)); // i.e. [1, 5] = 1, 2, 3, 4, 5
rangeSet.add(Range.openClosed(5, 9)); // i.e. ]5, 9] = 6, 7, 8, 9
rangeSet.add(Range.open(15, 20)); // i.e. ]15, 20[ = 16, 17, 18, 19

System.out.println(rangeSet);
// -> {[1,9](15,20)}

On parlera un peu plus en détail des intervalles dans un prochain épisode.

La notation anglaise des intervalles n'est pas la même que la française. L'intervalle « [1, 9] » est équivalent en anglais et en français. En revanche, l'intervalle « (15, 20) » se traduit par « ]15, 20[«  en français, soit « [16, 19] » pour des entiers.

Il est donc possible de dire si une valeur particulière fait bien partie de l'union des intervalles :

Dans l'union ?

@Test
public void testRange() {

// Arrange
final int target7 = 7;
final int target10 = 10;

// Act
final RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 5)); // i.e. [1, 5] = 1, 2, 3, 4, 5
rangeSet.add(Range.openClosed(5, 9)); // i.e. ]5, 9] = 6, 7, 8, 9
rangeSet.add(Range.open(15, 20)); // i.e. ]15, 20[ = 16, 17, 18, 19
// -> {[1,9](15,20)}

final boolean isIn7 = rangeSet.contains(target7);
final boolean isIn10 = rangeSet.contains(target10);

// Assert
Assert.assertTrue(isIn7);
Assert.assertFalse(isIn10);
}

On peut simplement demander si tout un intervalle est dedans :

Tout dedans ?

@Test
public void testContainsRange() {

// Arrange
final int min = 7;
final int max = 8;

// Act
final RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 5)); // i.e. [1, 5] = 1, 2, 3, 4, 5
rangeSet.add(Range.openClosed(5, 9)); // i.e. ]5, 9] = 6, 7, 8, 9
rangeSet.add(Range.open(15, 20)); // i.e. ]15, 20[ = 16, 17, 18, 19
// -> {[1,9](15,20)}

final boolean isIn = rangeSet.encloses(Range.closed(min, max)); // Ici [7, 8]

// Assert
Assert.assertTrue(isIn);
}

Il est non seulement possible d'ajouter des intervalles, mais aussi d'en enlever, tout ou partie :

Hop un trou

@Test
public void testRemove() {

// Arrange
final int target = 7;

// Act
final RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 5)); // i.e. [1, 5] = 1, 2, 3, 4, 5
rangeSet.add(Range.openClosed(5, 9)); // i.e. ]5, 9] = 6, 7, 8, 9
rangeSet.add(Range.open(15, 20)); // i.e. ]15, 20[ = 16, 17, 18, 19
// -> {[1,9](15,20)}

rangeSet.remove(Range.closedOpen(6, 8)); // i.e. [6, 8[ = 6, 7
// -> {[1,6)[8,9](15,20)}

final boolean isIn = rangeSet.contains(target);

// Assert
Assert.assertFalse(isIn);
}

Une RangeMap, quant à elle, est une map dont les clés sont des intervalles (forcément disjoints) :

RangeMap

RangeMap<Integer, Chien> rangemap = TreeRangeMap.create();
rangemap.put(Range.closed(1, 3), new Chien("Milou")); // i.e. [1, 3]
rangemap.put(Range.open(7, 10), new Chien("Pluto")); // i.e. ]7, 10[ = [8, 9]
// -> [[1,3]=Dog{name=Milou}, (7,10)=Dog{name=Pluto}]

On l'utilise comme une map classique, mais avec la notion d'intervalle :

Recherche dans la rangemap

@Test
public void testRangemap() {

// Arrange
final int target = 2;
final String name = "Milou";

// Act
RangeMap<Integer, Chien> rangemap = TreeRangeMap.create();
rangemap.put(Range.closed(1, 3), new Chien("Milou")); // i.e. [1, 3]
rangemap.put(Range.open(7, 10), new Chien("Pluto")); // i.e. ]7, 10[ = [8, 9]

final Chien chien = rangemap.get(target);

// Assert
Assert.assertEquals(name, chien.getName());
}

Il y a quelques autres méthodes mineures que je vous laisse découvrir par vous-même. Vous me direz si vous leur avez trouvé une application dans vos projets.

III - Conclusion

Nous venons de découvrir l'une des parties les plus importantes de Guava, à savoir les collections. N'hésitez pas à consulter les autres épisodes de cette série pour découvrir les fonctionnalités fantastiques de la bibliothèque.

Vos retours nous aident à améliorer nos publications. N'hésitez donc pas à commenter cet article sur le forum : Commentez

IV - Remerciements

D'abord j'adresse mes remerciements à l'équipe Guava, chez Google, pour avoir développé une bibliothèque aussi utile et pour la maintenir. Je n'oublie pas tous les contributeurs qui participent notamment sur le forum Guava.

Plus spécifiquement en ce qui concerne cet article, je tiens à remercier l'équipe de Developpez.com et plus particulièrement Bernard Le Roux, Ricky81, Mickael Baron, Yann Caron, Logan et Claude Leloup.

V - Annexes

V-A - Liens

Guava : https://code.google.com/p/guava-libraries/

Article « Simplifier le code de vos beans Java à l'aide de Commons Lang, Guava et Lombok » :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/simplifier-code-guava-lombok/

Blog sur Guava : https://blog.developpez.com/guava/

Article « J2SE 1.5 Tiger » par Lionel Roux :
https://lroux.developpez.com/article/java/tiger/

Article « Présentation de Java SE 7 » par F. Martini (adiGuba) :
https://adiguba.developpez.com/tutoriels/java/7/

V-B - Liens personnels

Retrouvez ma page et mes autres articles sur Developpez.com à l'adresse
https://thierry-leriche-dessirier.developpez.com/#page_articles

Suivez-moi sur Twitter : @thierryleriche(https://twitter.com/thierryleriche)