A la découverte du framework Google Collections
Date de publication : 10 septembre 2010
Par
Thierry Leriche-Dessirier (http://www.thierryler.com)
Les Collections Java ont un peu plus d'une douzaine d'années d'existence et s'imposent comme une des
plus importantes APIs du monde Java. De nombreux framework en utilisent les fonctionnalités et les étendent.
C'est notamment le cas de Google-Collections qui ajoute des évolutions intéressantes comme les Prédicats,
les objets Multi ou Bi, les immutables, etc. Ce document est un point de départ à la découverte des éléments
clés de Google-Collections.
I. Introduction
II. Installation
III. Déclarations rapides
IV. Filtres, prédicats et associés
IV-A. Filtres
IV-B. Prédicats
IV-C. Convertisseurs
IV-D. Comparateurs
IV-E. La pagination
IV-F. et les Maps
V. Multi / Bi
V-A. MultiMaps
V-B. MultiSets
V-C. BiMaps
VI. Collections immuables (immutables)
VII. Functionnal-collections
VIII. Conclusion
IX. Annexes
IX-A. Les différentes implémentations
IX-B. pom.xml
IX-C. Classes utilisées
I. Introduction
Lorsque John Blosh 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
(1)
ou les génériques
(2), l'API
n'a quasiment pas changée. C'est dire si Java-Collections a été bien pensée.
Le modèle de conception de Java-Collections est une référence. Il repose sur trois axes majeurs dont
tout projet gagne à s'inspirer :
-
Des interfaces qui définissent les collections (3). L'expérience montre
qu'avec les collections, encore plus qu'avec les autres classes Java, les
développeurs prennent vite la bonne habitude de déclarer et d'utiliser des interfaces ;
-
Des implémentations qui fournissent des classes abstraites et concrètes. Ces implémentations respectent
les contrats définis par les interfaces, chacune à sa manière avec ses spécificités justifiant qu'on
utilise l'une ou l'autre ;
-
Des algorithmes puissants et variés qui permettent de manipuler les collections et leurs données.
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 Kevin Bourrillion
et Jared Levy qui sont allés le plus loin en créant
Google-Collections.
|
Le framework Google-Collections est désormais inclus dans le projet
Guava.
|
Le framework Google-Collections s'intéresse à des problématiques de bas-niveau et apporte un support fiable, permettant aux
développeurs de se concentrer sur les éléments importants de leurs programmes. Tout comme Java-Collections, le
framework Google-Collections propose des interfaces relativement simples, avec un nombre restreint de
méthodes par interface.
II. Installation
Pour profiter des fonctionnalités du framework Google-Collections dans un programme, le plus simple est encore
d'utiliser Maven. Il suffit d'ajouter une dépendance dans le fichier "pom.xml" de l'application.
Dépendance à Google-Collections dans le pom |
< dependency >
< groupId > com.google.collections< / groupId >
< artifactId > google-collections< / artifactId >
< version > 1.0< / version >
< / dependency >
|
|
Les habitués de Maven géreront le numéro de version plus élégamment, bien que cette façon suffise
largement pour cet article. Le code complet du fichier "pom.xml" utilisé pour cet article est fourni
en annexes.
|
Puis on lance Maven en ligne de commande
(4)
avec la commande suivante.
Commande Maven d'install |
mvn clean install site eclipse:eclipse
|
Il n'y a plus qu'à importer le projet dans Eclipse. Si le projet est déjà dans le workspace Eclipse
alors il suffit de faire un refresh.
Les dépendances dans Eclipse
III. Déclarations rapides
Le framework Google-Collections est tout aussi vaste/riche que l'est son grand frère Java-Collections.
Il est donc possible de l'aborder de différentes manières. Le parti pris de cet article est de se calquer
sur l'ordre d'écriture des éléments dans un programme standard, à commencer par les déclarations.
Une des premières choses qui saute aux yeux lorsqu'on découvre Google Collections, c'est la simplicité
avec laquelle il est possible de créer des listes (List, Set, Map, etc.) génériques complexes sans subir
les inconvénients de la syntaxe verbeuse de Java.
Voici un exemple simpliste de création de liste (Integer) où la syntaxe oblige à dupliquer des
informations finalement inutiles.
Déclaration verbeuse d'une liste d'Integer |
List< Integer> maListeClassique = new ArrayList< Integer> ();
...
maListeClassique.add (123 );
|
Dans certains cas, moins rares qu'on ne l'imagine, les déclarations Java peuvent s'allonger dans des
proportions inquiétantes. L'exemple suivant est adaptés d'un cas réel. Pour le développeur, c'est systématiquement
une source de doute et de tensions.
Déclaration très verbeuse d'une map |
Map< List< String> , Class< ? extends List< Integer> > > maGrosseMap
= new HashMap< List< String> , Class< ? extends List< Integer> > > ();
|
Avec Google Collections, les déclarations deviennent simples. Le typage ne se fait plus qu'à gauche
du signe égal.
Déclaration simplifiée à l'aide de Google-Collections |
import static com.google.common.collect.Lists.newArrayList;
...
List< Integer> maListeGoogle = newArrayList ();
|
A l'usage, cette écriture est vraiment pratique, plus succincte, plus claire, et il est difficile
de s'en passer après y avoir gouté. Les développeurs de Google-Collections se sont appliqués à généraliser
ces méthodes de création rapide pour la plupart des collections et il faut reconnaitre que c'est un travail
énorme qui, à lui seul, justifie l'adoption du framework.
Pour écrire une méthode static de création avec généric automatique, on peut s'inspirer du code suivant.
Sun ne l'a pas généralisé pour des raisons d'homogénéité (static bad...) mais c'est bien plus élégant/pratique
d'utiliser cette technique (pas seulement pour les collections) quand on en a la liberté.
Création de générique perso |
public static < T> List< T> creerUnVectorPerso () {
return new Vector< T> ();
}
...
List< Integer> maListeMaison = creerUnVectorPerso ();
|
IV. Filtres, prédicats et associés
Le framework Google-Collections fourni un ensemble de composants très pratiques pour filtrer, trier,
comparer ou convertir des collections. Les adeptes des classes anonymes vont apprécier la suite.
IV-A. Filtres
Il est fréquent de devoir filtrer des données d'une liste, issues par exemple d'une requête
générique en base ou sur un web service. A l'ancienne, une telle méthode de filtre peut s'écrire comme suit.
Filtre à l'ancienne |
static public List< Personne> filtrerHomme1 (List< Personne> personnes) {
List< Personne> result = new ArrayList< Personne> ();
for (Personne personne : personnes) {
if (personne.isHomme ()) {
result.add (personne);
}
}
return result;
}
|
Le test JUnit suivant est proposé pour tester cette première méthode de filtre. Le début du test vise
à créer un jeu de données.
Création du jeu de données à tester |
@ Before
public void doBefore () {
personnes = newArrayList ();
remplir ();
}
@param
private void remplir () {
personnes.add (new Personne (" Anne " , " Dupont " , 27 , Sexe.FEMME));
personnes.add (new Personne (" Julien " , " Lagarde " , 22 , Sexe.HOMME));
personnes.add (new Personne (" Manon " , " Ler " , 1 , Sexe.FEMME));
personnes.add (new Personne (" Mickael " , " Jordan " , 48 , Sexe.HOMME));
personnes.add (new Personne (" Paul " , " Berger " , 65 , Sexe.HOMME));
personnes.add (new Personne (" Pascal " , " Dupont " , 28 , Sexe.HOMME));
personnes.add (new Personne (" Silvie " , " Alana " , 15 , Sexe.FEMME));
personnes.add (new Personne (" Thierry " , " Ler " , 33 , Sexe.HOMME));
personnes.add (new Personne (" Zoe " , " Mani " , 7 , Sexe.FEMME));
}
|
Et la suite du test sert à valider que la méthode fonctionne bien.
Test du filtre |
@ Test
public void testTailleListe () {
assertEquals (personnes.size (), 9 );
}
@ Test
public void testFiltrerHomme1 () {
List< Personne> hommes = PersonneUtil.filtrerHomme1 (personnes);
System.out.println (hommes);
assertEquals (hommes.size (), 5 );
for (Personne homme : hommes) {
assertTrue (homme.isHomme ());
}
}
|
|
Les annotations @Before et @Test sont spécifiques aux tests. Une méthode annotée @Before sera lancée avant chaque
test. Une méthode annotée @Test correspond à un test unitaire.
|
Le code de cette première méthode de filtre n'est pas très élégant. Elle est composée de code
technique encore et encore. Pourtant, bien que ce code deviennent presqu'un pattern à force
d'utilisation, on en trouve de nombreuses variantes lors des tests de qualité. En remplacement, la
portion de code suivante est intéressante.
Filtre à l'aide de Iterables et Predicate |
static public List< Personne> filtrerHomme2 (List< Personne> personnes) {
List< Personne> result = newArrayList (Iterables.filter (personnes,
new Predicate< Personne> () {
public boolean apply (Personne personne) {
return personne.isHomme ();
}
} ));
return result;
}
|
Les éléments clés sont ici Iterables.filter et Predicate à propos duquel on dira quelques mots plus bas.
Bon évidement l'exemple utilisé est si simple qu'il est délicat de se rendre compte de la puissance des
filtres mais il suffit d'imaginer un cas réel dans un projet d'entreprise. Reste bien entendu à factoriser
les filtres et les Predicates. Et avec Java 7, on devrait avoir les closures et là...
Un petit détail qui a son importance, il est possible de préférer l'utilisation de la classe Collection2 à
la place de Iterables
Filtre à l'aide de Collections2 et Predicate |
static public List< Personne> filtrerHomme3 (List< Personne> personnes) {
List< Personne> result = newArrayList (Collections2.filter (personnes,
new Predicate< Personne> () {
public boolean apply (Personne personne) {
return personne.isHomme ();
}
} ));
return result;
}
|
En effet les deux classes fournissent la méthode filter() et semblent fournir le même service.
Mais alors quelle est la différence ? Le
Javadoc de Collections2
et le
Javadoc de Iterables
nous en disent un peu plus sur LES méthodeS filter(..)
La doc de Collection2 nous dit :
Returns the elements of unfiltered that satisfy a predicate. The returned collection is a live view of unfiltered; changes to one affect the other.
The resulting collection's iterator does not support remove(), but all other collection methods are supported. The collection's add() and addAll() methods throw an IllegalArgumentException if an element that doesn't satisfy the predicate is provided. When methods such as removeAll() and clear() are called on the filtered collection, only elements that satisfy the filter will be removed from the underlying collection.
The returned collection isn't threadsafe or serializable, even if unfiltered is.
Many of the filtered collection's methods, such as size(), iterate across every element in the underlying collection and determine which elements satisfy the filter. When a live view is not needed, it may be faster to copy Iterables.filter(unfiltered, predicate) and use the copy.
La doc de Iterable nous dit :
Returns all instances of class type in unfiltered. The returned iterable has elements whose class is type or a subclass of type. The returned iterable's iterator does not support remove().
Returns an unmodifiable iterable containing all elements of the original iterable that were of the requested type
La classe Collection2 renvoie donc une vue "live", non thread safe, des éléments filtrés tandis que
Iterable renvoie une "copy". La doc le dit elle-même, si une vue live n'est pas nécessaire alors
l'utilisation de Iterable permettra d'avoir un programme plus rapide (dans la plupart des cas).
IV-B. Prédicats
Le chapitre précédent montre comment réaliser un filtre à l'aide d'un prédicat simple (homme/femme) mais
les
prédicats
sont bien plus puissants que ça. Le code suivant donne un exemple d'utilisation des méthodes de composition.
Mélange de prédicats |
import static com.google.common.base.Predicates.and;
import static com.google.common.base.Predicates.or;
import static com.google.common.base.Predicates.in;
import static com.google.common.base.Predicates.not;
...
List< Integer> liste1 = newArrayList (1 , 2 , 3 );
List< Integer> liste2 = newArrayList (1 , 4 , 5 );
List< Integer> liste3 = newArrayList (1 , 4 , 5 , 6 );
@ Test
public void testMelange () {
boolean isFormuleOk1 = and (in (liste1), in (liste2)).apply (1 );
System.out.println (isFormuleOk1);
boolean isFormuleOk2 = and (in (liste2), in (liste3), not (in (liste1))).apply (4 );
System.out.println (isFormuleOk2);
}
|
Ce code est si simple, comparé à tout ce qu'il faudrait programmer (sans Google-Collections) pour arriver
au même résultat. Et encore l'exemple est volontairement simplifié et loin de représenter ce qui existe
dans une vraie application d'entreprise.
L'impact est encore plus flagrant lorsqu'on s'intéresse aux mécanismes de compositions sur lesquels de
nombreux développeurs se sont cassés les dents...
Composition |
import static com.google.common.base.Predicates.compose;
...
@ Test
public void testComposition () {
boolean isAddition = compose (in (liste3),
new Function< Integer, Integer> () {
public Integer apply (Integer nombre) {
return nombre + 1 ;
}
} ).apply (5 );
System.out.println (isAddition);
}
|
Une petite explication s'impose. L'utilisation de ".appli(5)" envoie la valeur "5" à la fonction,
qui l'additionne à "1" pour renvoyer la valeur "6", qui est bien dans "liste3" comme le réclame
l'instruction "in(liste3)". Quant à la méthode compose(), elle renvoie la composition d'un
prédicat (ici "in") et d'une fonction. L'ensemble est un peu délicat à prendre en main mais beaucoup
plus agréable à utiliser que s'il fallait s'en passer.
IV-C. Convertisseurs
Un point qui revient souvent dans les programmes concerne la conversion de bean, par exemple
de Form vers un DTO dans un projet Struts. Les exemples ne manquent pas. Avec l'aide de
Google-Collections, les converteurs n'utilisent pas beaucoup moins de lignes de code mais les
éléments techniques sont standardisés.
Sans Google-Collections, un converteur peut s'écrire comme suit. On note la gestion manuelle de la boucle for
qui, bien que relavivement discrète, reste bien présente.
Converter à l'ancienne |
public List< Double> convertir1 (List< Integer> liste) {
List< Double> result = new ArrayList< Double> ();
for (Integer elt : liste) {
result.add (new Double (elt));
}
return result;
}
|
Test du converter |
@ Test
public void testConverter1 () {
System.out.println (premiers);
List< Double> premiersDoubles = convertir1 (premiers);
System.out.println (premiersDoubles);
}
|
Avec Google-Collections, on se contente d'écrire le code du converteur, avec juste un peu de code de lancement.
Ici on ne s'occupe pas des boucles et autres aspets techniques.
Converter avec Google-Collections |
import static com.google.common.collect.Lists.transform;
...
public List< Double> convertir2 (List< Integer> liste) {
List< Double> result = transform (liste, new Function< Integer, Double> () {
public Double apply (Integer nombre) {
return new Double (nombre);
}
} );
return result;
}
|
La conversion d'entiers est relativement simple. Un cas réel d'une application d'entreprise ressemblerait
plus au code suivant (lui aussi simplifié).
Converter plus complexe |
public static List< Humain> convertir (List< Personne> personnes) {
List< Humain> result = transform (personnes, new Function< Personne, Humain> () {
public Humain apply (Personne personne) {
Humain humain = new Humain ();
humain.setNomComplet (personne.getPrenom () + " " + personne.getNom ());
humain.setAge (new Double (personne.getAge ()));
return humain;
}
} );
return result;
}
|
et le test |
@ Test
public void testConverterPersonnesToHumains () {
System.out.println (personnes);
List< Humain> humains = convertir (personnes);
System.out.println (humains);
}
|
IV-D. Comparateurs
Le framework fournit des méthodes intéressantes pour comparer et ordonner des objets d'une liste.
Le code parle de lui-même. Les amateurs des tris à bulles ou des sort() se feront une raison.
Tris par nom et prénom |
static public List< Personne> trierParNom (List< Personne> personnes) {
Ordering< Personne> nomOrdering = new Ordering< Personne> () {
public int compare (Personne p1, Personne p2) {
return p1.getNom ().compareTo (p2.getNom ());
}
} ;
return nomOrdering.nullsLast ().sortedCopy (personnes);
}
static public List< Personne> trierParPrenom (List< Personne> personnes) {
Ordering< Personne> prenomOrdering = new Ordering< Personne> () {
public int compare (Personne p1, Personne p2) {
return p1.getPrenom ().compareTo (p2.getPrenom ());
}
} ;
return prenomOrdering.nullsLast ().sortedCopy (personnes);
}
|
et le test |
@ Test
public void testTrierParNom () {
assertTrue (" lala " , " a " .compareTo (" b " ) < 0 );
System.out.println (personnes);
String temp = null ;
for (Personne personne : personnes) {
if (temp ! = null ) {
assertTrue (temp.compareTo (personne.getPrenom ()) <= 0 );
}
temp = personne.getPrenom ();
}
List< Personne> personnesTrieesParNom = trierParNom (personnes);
System.out.println (personnesTrieesParNom);
String nom = " a " ;
temp = null ;
for (Personne personne : personnesTrieesParNom) {
if (temp ! = null ) {
assertTrue (nom.compareTo (personne.getNom ()) <= 0 );
}
nom = personne.getNom ();
}
List< Personne> personnesTrieesParPrenom = trierParPrenom (personnesTrieesParNom);
System.out.println (personnesTrieesParPrenom);
temp = null ;
for (Personne personne : personnesTrieesParPrenom) {
if (temp ! = null ) {
assertTrue (temp.compareTo (personne.getPrenom ()) <= 0 );
}
temp = personne.getPrenom ();
}
}
|
|
Il faut noter l'utilisation de "nullsLast()" qui prend en charge le cas des objets "null" et sans
qui il est possible d'obtenir une NPE si un élément nul est présent dans la liste.
|
Il est également possible de définir des méthodes pratiques dans des Orderings, comme par exemple
les méthodes "max()" et "min()" qui permettent d'extraire les valeurs aux bornes.
Min / Max |
static private Ordering creerAgeOrdering () {
Ordering< Personne> ageOrdering = new Ordering< Personne> () {
public Personne max (Personne p1, Personne p2) {
return p1.getAge () > p2.getAge () ? p1 : p2;
}
public Personne min (Personne p1, Personne p2) {
return p1.getAge () <= p2.getAge () ? p1 : p2;
}
@ Override
public int compare (Personne p1, Personne p2) {
return 0 ;
}
} ;
return ageOrdering;
}
static public Personne trouverPlusVieux (List< Personne> personnes) {
Ordering< Personne> ageOrdering = creerAgeOrdering ();
return ageOrdering.max (personnes);
}
static public Personne trouverPlusJeune (List< Personne> personnes) {
Ordering< Personne> ageOrdering = creerAgeOrdering ();
return ageOrdering.min (personnes);
}
|
et le test |
@ Test
public void testAges () {
System.out.println (personnes);
Personne vieux = trouverPlusVieux (personnes);
System.out.println (" Le plus vieux : " + vieux);
assertEquals (" Paul " , vieux.getPrenom ());
Personne jeune = trouverPlusJeune (personnes);
System.out.println (" Le plus jeune : " + jeune);
assertEquals (" Manon " , jeune.getPrenom ());
}
|
|
La doc de Java Collections indique très précisément comment il faut faire ceci à l'ancienne.
|
Il est également possible d'utiliser des "ordonneurs" définis séparément, surtout s'ils existent
déjà dans le JDK, comme par exemple le "CASE_INSENSITIVE_ORDER" de la classe String.
Ordering tout prêt |
static public List< String> trier (List< String> liste) {
Ordering< String> ordering = Ordering.from (String.CASE_INSENSITIVE_ORDER);
return ordering.sortedCopy (liste);
}
|
et le test |
@ Test
public void testOrdering () {
List< String> noms = newArrayList ();
for (Personne personne:personnes){
noms.add (personne.getNom ());
}
System.out.println (noms);
List< String> nomsTries = PersonneUtil.trier (noms);
System.out.println (nomsTries);
String temp = null ;
for (String nom : nomsTries) {
if (temp ! = null ) {
assertTrue (temp.compareTo (nom) <= 0 );
}
temp = nom;
}
}
|
Pour créer comparateur spécifique, il suffit de s'inspirer du code suivant, qui ressemble étrangement
à une portion de classe anonyme codée plus haut.
Comparator maison |
Comparator< Personne> monComparateur = new Comparator< Personne> () {
public int compare (Personne p1, Personne p2) {
return p1.getAge () - p2.getAge ();
}
} ;
|
IV-E. La pagination
Il est fréquent de devoir paginer des listes ou plus simplement de ne traiter qu'une partie réduite d'une
grosse liste. Java permet de réaliser ce type d'opération et le framework Google-Collections rend la tâche
très facile. Le test suivant en est l'illustration.
Test de pagination |
@ Test
public void testPartition () {
System.out.println (personnes);
List< List< Personne> > partitions = Lists.partition (personnes, 5 );
int taille = 0 ;
for (List< Personne> partition : partitions) {
System.out.println (partition);
taille + = partition.size ();
}
assertTrue (taille = = personnes.size ());
}
|
IV-F. et les Maps
Les maps ne sont pas oubliées par le framework. Elles disposent bien entendu des mêmes facilités
de construction que les listes.
Ages des personnes |
import static com.google.common.collect.Maps.newHashMap;
...
Map< String, Integer> ages = newHashMap ();
|
Une telle Map permet de faire évoluer le test utilisé dans cet article. L'exemple est
volontairement (très) simple.
Ages des personnes |
@ Before
public void doBefore () {
personnes = newArrayList ();
remplirPersonnes ();
ages = newHashMap ();
remplirAges ();
}
private void remplirAges () {
for (Personne personne : personnes) {
ages.put (personne.getPrenom (), personne.getAge ());
}
}
|
Il est possible, tout comme avec les listes, d'appliquer des filtres (à l'aide de prédicats) sur les Maps.
Filtre sur Thierry et Cédric |
import static com.google.common.base.Predicates.or;
import static com.google.common.collect.Maps.filterKeys;
...
@ Test
public void testAgeDeThierryEtCedric () {
System.out.println (ages);
Map< String, Integer> thiEtCedAges = filterKeys (ages,
or (Predicates.equalTo (" Thierry " ), Predicates.equalTo (" Cédric " )));
System.out.println (thiEtCedAges);
assertTrue (thiEtCedAges.size () = = 1 );
}
|
Là encore, comme avec les listes, on peut réaliser des transformations.
Ages dans un an |
import static com.google.common.collect.Maps.transformValues;
...
@ Test
public void testTransformationDeMap () {
System.out.println (ages);
Map< String, Integer> agesDansUnAn = transformValues (ages,
new Function< Integer, Integer> () {
public Integer apply (Integer age) {
return age + 1 ;
}
} );
System.out.println (agesDansUnAn);
Integer ageZoe = ages.get (" Zoe " );
Integer ageZoeDansUnAn = agesDansUnAn.get (" Zoe " );
assertTrue (ageZoeDansUnAn = = ageZoe + 1 );
}
|
Une opération, qui revient souvent dans les programmes, est de dresser la liste des différences entre deux Maps.
Test des différences |
@ Test
public void testDifferences () {
System.out.println (ages);
ImmutableMap< String, Integer> agesCollegues = new ImmutableMap.Builder< String, Integer> ()
.put (" Paul " , 65 )
.put (" Pascal " , 28 )
.put (" Anne " , 25 )
.put (" Lucie " , 37 )
.put (" Julien " , 37 )
.build ();
System.out.println (agesCollegues);
MapDifference< String, Integer> diff = Maps.difference (ages, agesCollegues);
System.out.println (diff);
System.out.println (diff.entriesInCommon ());
}
|
|
Les ImmutableMap utilisées dans l'exemple, ci-dessus, sont expliqués plus bas.
|
Ca n'a l'air de rien mais programmer ce genre de fonction est (très) complexe. Les exemples utilisés
dans cet article, une fois encore, ne laissent pas tout entrevoir, ne montrent pas tout.
En outre les listes et les maps ne fonctionnent pas uniquement avec l'objet Personne.
V. Multi / Bi
Le framework Google-Collections introduit le principe des objets Multi et Bi, qui ajoutent des comportement
très intéressants aux listes et aux maps.
V-A. MultiMaps
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.
Dans l'exemple des âges, utilisé plus haut, c'est justement l'effet désiré puisqu'une personne ne peut
avoir qu'un seul âge. En revanche, une personne peut, par exemple, aimer plusieurs couleurs. Dans ce cas
une Map simple ne suffit pas et l'astuce classique consiste à associer une liste à la map.
Listes de couleurs |
Map< String, List< String> > couleurs = newHashMap ();
remplirCouleurs ();
...
private void remplirCouleurs () {
List< String> julienCouleurs = newArrayList ();
julienCouleurs.add (" Jaune " );
julienCouleurs.add (" Vert " );
julienCouleurs.add (" Bleu " );
couleurs.put (" Julien " , julienCouleurs);
List< String> anneCouleurs = newArrayList ();
anneCouleurs.add (" Blanc " );
anneCouleurs.add (" Rose " );
anneCouleurs.add (" Rouge " );
anneCouleurs.add (" Bleu " );
couleurs.put (" Anne " , anneCouleurs);
}
|
Ce type de construction devient vite rébarbatif et relativement longue à mettre en place, même
lorsque le développeur s'efforce de factoriser au maximum.
Ajout de couleurs |
public void ajouterCouleur (String prenom, String couleur,
Map< String, List< String> > personnesCouleurs) {
List< String> personnesCouleurs = couleurs.get (prenom);
if (desCouleurs = = null ) {
desCouleurs = new ArrayList ();
personnesCouleurs.put (prenom, desCouleurs);
}
desCouleurs.add (desCouleurs);
}
...
ajouterCouleur (" Thierry " , " Bleu " , couleurs);
ajouterCouleur (" Thierry " , " Rouge " , couleurs);
|
Ca sonne comme un pattern classique écrit encore et encore. Néanmoins, pour un
pattern, on en trouve des variantes chez tous les programmeurs alors même que la doc de l'API
Java-Collections donne un modèle de référence...
Google apporte les MultiMaps. Pour une clé donnée, elles permettrent d'avoir plusieurs valeurs. La
méthode précédente est alors simplifiée. On remarque en particulier la disparition du test
d'existence "if", que le framework gère tout seul. La "sous-liste" est automatiquement crée si besoin.
Ajout de couleurs simplifié |
public void ajouterCouleur (String prenom, String couleur, Multimap< String, String> amis) {
amis.put (prenom, couleur);
}
|
Evidement les multimaps renvoient une collection quand on cherche une clé, ce qui est justement l'objectif.
Recherche d'une clé |
public Collection< String> getCouleursAmis (String prenom, Multimap< String, String> amis) {
return amis.get (prenom);
}
|
|
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.
|
V-B. MultiSets
Les MultiSets représentent la réponse de Google-Collections à un manque de Java concernant les Sets.
En effet, les Sets, en Java, sont des listes non ordonnées qui ne contiennent pas de doublon. Ce qui est
important ici, c'est que ce soit non ordonné et sans doublon. Java propose aussi les List qui sont
ordonnées et peuvent contenir des doublons. Mais il n'y a aucune solution pour des listes ordonnées
sans doublon, et c'est justement ce à quoi correspondent les MultiSet.
Avec un multiset, il est possible d'ajouter plusieurs fois la même valeur, sans forcément que l'ajout soit
ordonné.
Construction d'une MultiSet avec Alicia en double |
Multiset< String> multiAmis = = HashMultiset.create ();
remplirMultiAmis ();
...
multiAmis.add (" Alicia " );
multiAmis.add (" Daniel " );
multiAmis.add (" Elodie " );
multiAmis.add (" Martin " );
multiAmis.add (" Alicia " );
multiAmis.add (" Thierry " );
System.out.println (multiAmis);
|
V-C. BiMaps
VI. Collections immuables (immutables)
VII. Functionnal-collections
VIII. Conclusion
IX. Annexes
IX-A. Les différentes implémentations
IX-B. pom.xml
IX-C. Classes utilisées
(1) | Arrivés très tôt. |
(2) | Une des grandes nouveautés de Java 5. |
(3) | Au sens large. |
(4) | Sous Windows, menu "Démarrer/Executer" puis taper "cmd" |
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 ©
2010 Thierry Leriche-Dessirier. Aucune reproduction, même partielle, ne peut être
faite de ce site ni 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.