IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

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.

       Version PDF (Miroir)   Version hors-ligne (Miroir)
Viadeo Twitter Facebook Share on Google+        



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.

info 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>  
idea 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.

Dépendances dans Eclipse
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();
}

/**
 * Remplir la liste avec des personnes. Les personnes sont ajoutées par
 * ordre alphabetique sur le prénom.
 * 
 * @param liste
 */
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));
	// Pascal Dupont est le mari d'Anne Dupont
	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);
	// --> [Julien, Mickael, Pascal, Paul, Thierry]

	assertEquals(hommes.size(), 5);

	for (Personne homme : hommes) {
		assertTrue(homme.isHomme());
	}
}				
info 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); // --> true
	boolean isFormuleOk2 = and(in(liste2), in(liste3), not(in(liste1))).apply(4);
	System.out.println(isFormuleOk2); // --> true
}
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); // --> true		
}		
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);
	// --> [1, 2, 3, 5, 7, 11, 13]
	
	List<Double> premiersDoubles = convertir1(premiers);
	System.out.println(premiersDoubles);
	// --> [1.0, 2.0, 3.0, 5.0, 7.0, 11.0, 13.0]	
}
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())); // Integer --> Double
			return humain;
		}
	});
	return result;
}
et le test
@Test
public void testConverterPersonnesToHumains() {
	System.out.println(personnes);
	// --> [Anne, Julien, Manon, Mickael, Pascal, Paul, Silvie, Thierry, Zoe]
	
	List<Humain> humains = convertir(personnes);
	System.out.println(humains);
	// --> [Anne Dupont, Julien Lagarde, Manon Ler, Mickael Jordan, Pascal Dupont, Paul Berger, Silvie Alana, Thierry Ler, Zoe Mani]
}		

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);
	// --> true

	System.out.println(personnes);
	// Déjà triée par prénom
	// --> [Anne, Julien, Manon, Mickael, Pascal, Paul, Silvie, Thierry,
	// Zoe]
	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);
	// --> [Silvie, Paul, Anne, Pascal, Mickael, Julien, Manon, Thierry,
	// Zoe]
	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);
	// --> [Anne, Julien, Manon, Mickael, Pascal, Paul, Silvie, Thierry,
	// Zoe]
	temp = null;
	for (Personne personne : personnesTrieesParPrenom) {
		if (temp != null) {
			assertTrue(temp.compareTo(personne.getPrenom()) <= 0);
		}
		temp = personne.getPrenom();
	}
}
idea 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);

	// Paul a 65 ans.
	Personne vieux = trouverPlusVieux(personnes);
	System.out.println("Le plus vieux : " + vieux);
	// --> Le plus vieux : Paul

	assertEquals("Paul", vieux.getPrenom());

	// Manon a 1 an.
	Personne jeune = trouverPlusJeune(personnes);
	System.out.println("Le plus jeune : " + jeune);
	// --> Le plus jeune : Manon

	assertEquals("Manon", jeune.getPrenom());
}			
info 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);
	// --> [Dupont, Lagarde, Ler, Jordan, Dupont, Berger, Alana, Ler, Mani]
	
	List<String> nomsTries = PersonneUtil.trier(noms);
	System.out.println(nomsTries);
	// --> [Alana, Berger, Dupont, Dupont, Jordan, Lagarde, Ler, Ler, Mani]
	
	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);
	// --> [Anne, Julien, Manon, Mickael, Pascal, Paul, Silvie, Thierry, Zoe]
	
	List<List<Personne>> partitions =  Lists.partition(personnes, 5);
	int taille = 0;
	for(List<Personne> partition : partitions) {
		System.out.println(partition);
		taille += partition.size();
	}
	// --> [Anne, Julien, Manon, Mickael, Pascal]
	// --> [Paul, Silvie, Thierry, Zoe]
	
	assertTrue(taille == personnes.size()); // --> true
}

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

/**
 * Exemple simple et un peu maladroit.
 */
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);
	// --> {Zoe=7, Paul=65, Mickael=48, Manon=1, Thierry=33, Julien=22, Silvie=15, Pascal=28, Anne=27}

	Map<String, Integer> thiEtCedAges = filterKeys(ages,
			or(Predicates.equalTo("Thierry"), Predicates.equalTo("Cédric")));
	System.out.println(thiEtCedAges);
	// --> {Thierry=33}
	
	assertTrue(thiEtCedAges.size() == 1); // --> true
}				
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);
	// --> {Zoe=7, Paul=65, Mickael=48, Manon=1, Thierry=33, Julien=22, Silvie=15, Pascal=28, Anne=27}

	Map<String, Integer> agesDansUnAn = transformValues(ages,
			new Function<Integer, Integer>() {
				public Integer apply(Integer age) {
					return age + 1;
				}
			});
	System.out.println(agesDansUnAn);
	// --> {Zoe=8, Paul=66, Mickael=49, Manon=2, Thierry=34, Julien=23, Silvie=16, Pascal=29, Anne=28}

	Integer ageZoe = ages.get("Zoe");
	Integer ageZoeDansUnAn = agesDansUnAn.get("Zoe");
	assertTrue(ageZoeDansUnAn == ageZoe + 1); // --> true
}	
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);
	// --> {Zoe=7, Paul=65, Mickael=48, Manon=1, Thierry=33, Julien=22, Silvie=15, Pascal=28, Anne=27}
	
	ImmutableMap<String, Integer> agesCollegues = new ImmutableMap.Builder<String, Integer>()
			.put("Paul", 65)
			.put("Pascal", 28)
			/* Anne Triche sur son âge */
			.put("Anne", 25)
			.put("Lucie", 37)
			.put("Julien", 37)
			.build();

	System.out.println(agesCollegues);
	// --> {Paul=65, Pascal=28, Anne=25, Lucie=37, Julien=37}

	MapDifference<String, Integer> diff = Maps.difference(ages,	agesCollegues);
	System.out.println(diff);
	// --> not equal: only on left={Zoe=7, Manon=1, Mickael=48, Thierry=33, Silvie=15}: 
	// only on right={Lucie=37}: value differences={Julien=(22, 37), Anne=(27, 25)}
	
	System.out.println(diff.entriesInCommon());
	// --> {Paul=65, Pascal=28}
}
info 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() {
	// Julien
	List<String> julienCouleurs = newArrayList();
	julienCouleurs.add("Jaune");
	julienCouleurs.add("Vert");
	julienCouleurs.add("Bleu");
	couleurs.put("Julien", julienCouleurs);
	
	// Anne
	List<String> anneCouleurs = newArrayList();
	anneCouleurs.add("Blanc");
	anneCouleurs.add("Rose");
	anneCouleurs.add("Rouge");
	anneCouleurs.add("Bleu");
	couleurs.put("Anne", anneCouleurs);
	
	// etc.
}
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); 
// etc.
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);
}
idea 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);
// --> [Alicia x 2, Thierry, Martin, Elodie, Daniel]

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



               Version PDF (Miroir)   Version hors-ligne (Miroir)

(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"

Valid XHTML 1.0 TransitionalValid CSS!

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.