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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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é :
static
int
computeArrayListCapacity
(
int
arraySize) {
checkArgument
(
arraySize >=
0
); // Verifie que la taille est positive)
return
Ints.saturatedCast
(
5
L +
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 :
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 :
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 » :
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 :
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 :
final
Iterable<
Chien>
iter =
...
final
List<
Chien>
chiens =
Lists.newArrayList
(
iter);
Et vous l'avez compris, on peut itérer sur un itérable :
@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(..) » :
@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 :
@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(..) » :
@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 :
@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(..) » :
@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(..) » :
@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 » :
@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 :
@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 :
@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 :
@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
(
));
}
@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 :
@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 :
@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 :
@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 :
@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 :
for
(
Set<
Chien>
s : power) {
System.out.println
(
"*"
+
s);
}
*[]
*[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 :
@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 :
@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 :
@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() » :
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 » :
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 :
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"
);
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 » :
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 :
@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 :
@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
(
));
}
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 :
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 :
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 :
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 :
@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 :
@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);
}
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 :
Set<
Chien>
chiens =
ImmutableSet.of
(
milou, pluto, lassie, volt, rantanplan, idefix, milou, milou, milou);
Set<
Chien>
chiens =
ImmutableSet.of
(
milou, pluto, lassie, volt, rantanplan, idefix);
Ce qu'un simple test vous permettra de vérifier :
@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 :
@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 :
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 :
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 :
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 :
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 :
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 :
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<
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 :
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<
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 :
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 :
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 :
@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.
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 :
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.
@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 :
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 :
@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
(
));
}
@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
(
));
}
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 :
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 :
@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 :
@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 :
@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<
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 :
@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 : 4 commentaires
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_articlesTutoriels
Suivez-moi sur Twitter : @thierryleriche(https://twitter.com/thierryleriche)@thierryleriche