À la découverte du framework Google Collections

Thierry Leriche-Dessirier

Les Collections Java ont un peu plus d'une douzaine d'années d'existence et s'imposent comme une des plus importantes API 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. Commentez ♪

- Titre : À la découverte du framework Google Collections

- Auteur : Thierry Leriche-Dessirier

- Parution : 8 novembre 2010

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

Sommaire

- I. Introduction

- II. Installation

- III. Déclarations rapides

- IV. Filtres, prédicats et associés

- IV-A. Filtres

- IV-B. Prédicats pour les listes

- 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. Des builders

- VII-A. Pour les Maps

- VIII. Des préconditions

- IX. Functional-collections

- X. Conclusion

- XI. Annexes

- XI-A. Les classes utilisées

- XI-B. Le Pom

- XI-C. Les collections

- XI-D. Imports static faciles

- XI-E. Remerciements

I - Introduction

Zip avec les sources utilisées pour cet article : tuto-google-col.zip.

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 (1) ou les génériques (2), l'API n'a quasiment pas changé. 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.

Exemple de tri d'une liste

@Test
public void testTri() {
List<String> amis = new ArrayList<String>();
amis.add("Manon");
amis.add("Claire");
amis.add("Julien");
amis.add("Thierry");
amis.add("Martin");
amis.add("Elodie");
System.out.println(amis);
// --> [Manon, Claire, Julien, Thierry, Martin, Elodie]

Collections.sort(amis);
System.out.println(amis);
// --> [Claire, Elodie, Julien, Manon, Martin, Thierry]
}

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 ce sont, 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. Toutefois, la partie Google-Collections, rapportée à l'ensemble des dépendances ramenées par Maven lorsqu'on ne demande QUE les collections, est la plus « importante » du projet. C'est d'ailleurs à elle qu'est consacré cet article.

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.

Cet article a été publié en partie dans le numéro 30 (oct-nov 2010) du magazine de Developpez.com. Il a aussi été présenté (Google-Collections et Guava) au Paris JUG en 2011.

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

Edit : Les Google-Collections ont été abandonnées en tant que projet autonome quelque temps après la publication de cet article. Les fonctionnalités sont désormais intégrées directement dans Guava et je recommande donc de n'utiliser que cette nouvelle dépendance Maven, d'autant que Guava est riche.

Dépendance à Guava (qui inclut les Google-Collections) dans le pom

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>10.0</version>
</dependency>

Puis on lance Maven depuis un terminal (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, il suffit alors de faire un refresh.

Les dépendances dans Eclipse

(cliquez ici pour aggrandir l'image)

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é d'un cas réel. Pour le développeur, c'est systématiquement une source de doutes 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;
import static com.google.common.collect.Maps.newHashMap;
...
List<Integer> maListeGoogle = newArrayList();
Map<String, Integer> maMapGoogle = newHashMap();

À l'usage, cette écriture est vraiment pratique, plus succincte, plus claire, et il est difficile de s'en passer après y avoir goûté. 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 reconnaître que c'est un travail énorme qui, à lui seul, justifie l'adoption du framework.

Dans le cadre de Java 7 et du projet Diamonds, le langage introduit une syntaxe similaire à celle proposée par Google-Collection.

Syntaxe Diamonds

List<Integer> maListeDiamonds = new ArrayList<>();
Map<String, Intger> maMapDiamonds = new HashMap<>();

Pour écrire une méthode static de création avec générique 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ériques perso

public static <T> List<T> creerUneListePerso() {
return new ArrayList<T>();
}

...
List<Integer> maListeMaison = creerUneListePerso();

On notera que l'idée des déclarations simplifiées avait déjà été proposée par Josh Bloch dans son livre « Effective Java 2 » et devrait arriver dans Java 7 (ou 8).

IV - Filtres, prédicats et associés

Le framework Google-Collections fournit 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. À 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 à constituer un jeu de données.

Création du jeu de données à tester

@Before
public void doBefore() {
personnes = newArrayList();

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

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 ; il y a trop de code technique (encore et encore). Pourtant, bien que ce code devienne presque 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 desquels on dira quelques mots plus bas. Au passage, les initiés remarqueront le pattern de programmation fonctionnelle.

Évidemment, l'exemple utilisé est si simple qu'il est difficile de se rendre compte de la puissance des filtres. Il suffit d'imaginer un cas réel, dans un projet d'entreprise, pour se convaincre de l'utilité de ces méthodes. Reste bien entendu à factoriser les filtres et les Predicates. Et avec Java 7(5), on devrait avoir les closures et là…

Un petit détail qui a son importance, il est possible d'utiliser de la classe Collections2 à 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 ? La Javadoc de Collections2 et la Javadoc de Iterables nous en disent un peu plus sur les deux 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 Collections2 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 pour les listes

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. 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é à toute la programmation nécessaire pour arriver au même résultat sans Google-Collections. 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 auxquels de nombreux développeurs ont été confrontés.

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 « .apply(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 convertisseurs n'utilisent pas beaucoup moins de lignes de code, mais les éléments techniques sont standardisés.

Sans Google-Collections, un convertisseur peut s'écrire comme suit. On note la gestion manuelle de la boucle for qui, bien que relativement discrète, reste 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 convertisseur, avec juste un peu de code de lancement. Ici on ne s'occupe pas des boucles et autres aspects 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]
}

Les programmes ont souvent besoin de convertir des listes en String, en utilisant des séparateurs. Le pattern classique ressemble au code suivant :

Conversion de liste en String classique

public static final String SEPARATEUR = ", ";

static public String asString1(List<Personne> personnes) {
int reste = personnes.size();
StringBuffer sb = new StringBuffer();
for (Personne personne : personnes) {
sb.append(personne.getPrenom()).append(" ").append(personne.getNom());
if (reste != 1) {
sb.append(SEPARATEUR);
reste--;
}
}
return sb.toString();
}

Avec ce type de code, le développeur est obligé de distinguer tous les cas spéciaux comme le dernier élément dans l'exemple ci-dessus. Le framework Google-Collections simplifie le pattern.

Conversion de liste en String avec Google-Collections

static public String asString2(List<Personne> personnes) {
StringBuilder sb = new StringBuilder();

Joiner.on(SEPARATEUR).appendTo(sb,
transform(personnes, new Function<Personne, StringBuilder>() {
public StringBuilder apply(Personne from) {
return new StringBuilder(from.getPrenom()).append(" ").append(from.getNom());
}
}));
return sb.toString();
}

et le test

@Test
public void testJoiner() {
System.out.println(personnes);

final String s = "Anne Dupont, Julien Lagarde, Manon Ler, Mickael Jordan, Pascal Dupont, "
+ "Paul Berger, Silvie Alana, Thierry Ler, Zoe Mani";

String asString1 = PersonneUtil.asString1(personnes);
System.out.println(asString1);
// --> Anne Dupont, Julien Lagarde, Manon Ler, Mickael Jordan, Pascal Dupont,
//     Paul Berger, Silvie Alana, Thierry Ler, Zoe Mani

assertEquals(s, asString1);

String asString2 = PersonneUtil.asString2(personnes);
System.out.println(asString2);
// --> Anne Dupont, Julien Lagarde, Manon Ler, Mickael Jordan, Pascal Dupont,
//     Paul Berger, Silvie Alana, Thierry Ler, Zoe Mani

assertEquals(s, asString2);
}

Dans ce cas précis, il est possible de simplifier davantage la conversion en utilisant la méthode toString() de la classe Humain.

encore plus simple

static public String asString3(List<Humain> humains) {
return Joiner.on(SEPARATEUR).join(humains);
}

et le test

String asString3 = PersonneUtil.asString3(convertir(personnes));
System.out.println(asString3);
// --> Anne Dupont, Julien Lagarde, Manon Ler, Mickael Jordan, Pascal Dupont, Paul Berger, Silvie Alana, Thierry Ler, Zoe Mani

assertEquals(s, asString3);

Les amateurs de Spring voudront certainement combiner la fonction transform avec le pattern « converter » du framework.

Fonction transform avec le converter de Spring

import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

import com.google.common.base.Function;
import com.google.common.collect.Lists;

/**
* Converter de rayon {@link Integer} en circonference {@link Double} à l'aide
* de la formule c = 2 x PI x Rayon.
*
* @author Thierry Leriche-Dessirier
*/
@Component("rayonListToCirconferenceListConverter")
public class RayonListToCirconferenceListConverter implements Converter<List<Integer>, List<Double>> {
private static final double PI = Math.PI;

public List<Double> convert(List<Integer> rayons) {
List<Double> circonferences = Lists.transform(rayons, new Function<Integer, Double>() {
public Double apply(Integer rayon) {
final double circonference = 2 * rayon * PI;
return circonference;
}
});
return circonferences;
}
}

Dans cet exemple très simple, l'objectif est de transformer une liste de rayons (typés Integer) en liste de circonférences (typées Double) à l'aide de la fonction apprise à l'école.

et le test du converter

@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners(DependencyInjectionTestExecutionListener.class)
@ContextConfiguration(locations = {"classpath*:context/applicationContext-*.xml", "classpath:converters.xml" })
public class RayonTestCase {

@Resource(name = "rayonListToCirconferenceListConverter")
Converter<List<Integer>, List<Double>> rayonListToCirconferenceListConverter;

@Test
public void testCirconferences() {

ImmutableList<Integer> rayons = ImmutableList.of(2, 3, 4, 5, 6);
System.out.println(rayons);
// --> [2, 3, 4, 5, 6]

List<Double> circonferences = rayonListToCirconferenceListConverter.convert(rayons);
System.out.println(circonferences);
// --> [12.566370614359172, 18.84955592153876, 25.132741228718345,
// 31.41592653589793, 37.69911184307752]
}
}

Un point est très important à propos de la méthode statique transform(), c'est qu'elle travaille avec des Functions. La transformation des listes n'est réellement réalisée que lorsque cela est nécessaire. En fait, on peut comparer la liste de retour à une liste de proxies.

La doc de la méthode dit : « Returns a list that applies function to each element of fromList. The returned list is a transformed view of fromList; changes to fromList will be reflected in the returned list and vice versa. [..] The function is applied lazily, invoked when needed. This is necessary for the returned list to be a view [..] To avoid lazy evaluation when the returned list doesn't need to be a view, copy the returned list into a new list of your choosing. »

Pour faire simple, ça ne sert à rien de modifier la liste de retour dans une autre partie du programme. Mais un exemple sera plus parlant.

Toto comme DTO

public class Toto {
private Integer chiffre;
private String lettre;

@Override
public String toString() {
return "Toto " + chiffre + " : " + lettre;
}

...
}

et un test où la màj ne marche pas

@Test
public void testTransform() {
// On part d'une liste toute simple.
List<Integer> chiffres = Lists.newArrayList(1, 2, 3, 4, 5);
System.out.println(chiffres);
// --> [1, 2, 3, 4, 5]

// On fait une première transformation.
List<Toto> totos = Lists.transform(chiffres, new Function<Integer, Toto>() {
public Toto apply(Integer chiffre) {
final Toto toto = new Toto();
toto.setChiffre(chiffre);
toto.setLettre("a" + chiffre);
return toto;
}
});
System.out.println(totos);
// --> [Toto 1 : a1, Toto 2 : a2, Toto 3 : a3, Toto 4 : a4, Toto 5 : a5]

// On essaie de changer directement les valeurs, mais ce n'est pas pris
// en compte car le transform est mis en attente.
for (Toto toto : totos) {
toto.setChiffre(42);
}
System.out.println(totos);
// --> [Toto 1 : a1, Toto 2 : a2, Toto 3 : a3, Toto 4 : a4, Toto 5 : a5]

for (Toto toto : totos) {
Assert.assertFalse(toto.getChiffre() == 42);
}
}

Par contre, si on chaine les transform(), ça fonctionne.

et la suite du test qui marche

// Si les transforms sont homogènes
List<Toto> totos2 = Lists.transform(totos, new Function<Toto, Toto>() {
public Toto apply(Toto toto) {
toto.setChiffre(9);
return toto;
}
});
System.out.println(totos2);
// --> [Toto 9 : a1, Toto 9 : a2, Toto 9 : a3, Toto 9 : a4, Toto 9 : a5]

for (Toto toto : totos2) {
Assert.assertFalse(toto.getChiffre() == 9);
}

On peut aussi faire comme le propose la Javadoc (« To avoid lazy evaluation when the returned list doesn't need to be a view, copy the returned list into a new list of your choosing. ») et envoyer la liste transformée dans une nouvelle liste. Ça n'ajoute que le coût du conteneur…

Cette fois, ça marche si on copie dans une nouvelle liste

List<Toto> totos3 = Lists.newArrayList(totos);
for (Toto toto : totos3) {
toto.setChiffre(42);
}
System.out.println(totos3);
// --> [Toto 42 : a1, Toto 42 : a2, Toto 42 : a3, Toto 42 : a4, Toto 42 : a5]

IV-D - Comparateurs

Le framework fournit des méthodes intéressantes pour comparer et ordonner les 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();
}
}

Il faut noter l'utilisation de « nullsLast() » qui prend en charge le cas des objets « null » et sans lequel il est possible d'obtenir une NPE (6) 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

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

La doc de Java-Collections indique très précisément comment 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 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 un 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();
...

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

Les ImmutableMap utilisées dans l'exemple, ci-dessus, sont expliqués plus bas.

Contrairement aux apparences, programmer ce genre de fonction est (très) complexe. Les exemples utilisés dans cet article, une fois encore, ne laissent pas entrevoir toutes les possibilités en matière de programmation. En outre les listes et les maps ne fonctionnent pas uniquement avec l'objet Personne.

V - Multi/Bi

Le framework Google-Collections introduit les objets Multi et Bi, qui ajoutent des comportements 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();
// ...

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

// ...

Ce type de construction devient vite rébarbatif et relativement long à 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.

Cela donne l'impression d'une utilisation répétée d'un pattern classique. 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 permettent 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);
}

Il est aussi possible d'initialiser la liste à l'aide de newArrayList(..)

Écriture plus concise

// Luc
List<String> lucCouleurs = newArrayList("Violet", "Rouge", "Vert", "Noir", "Orange", "Gris");
couleurs.put("Luc", lucCouleurs);

Évidemment 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 Lists 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, sans forcément que l'ajout soit ordonné.

Construction d'une MultiSet avec 3 en double et 5 mal placé

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 (cf. ci-dessous), ce n'est pas lié au MultiSet.

Le framework propose également quelques fonctions associées pratiques et dont le besoin revient souvent.

Test des fonctionnalités associées

@Test
public void testPremiers() {

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

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

System.out.println(premiers.count(3));
// --> 2

assertTrue(premiers.count(3) == 2);
assertTrue(premiers.count(5) == 1);
}

V-C - BiMaps

Un peu à l'opposé des MultiMaps, le framework Google-Collections propose les BiMaps qui garantissent non seulement l'unicité des clés (comme une Map classique), mais aussi l'unicité des valeurs. Cette bimap peut donc s'utiliser dans les deux sens puisque les valeurs uniques sont vues comme des clés : clé-valeur-clé…

Test des fonctionnalités associées

BiMap<String, String> plaquesVoiture = HashBiMap.create();
plaquesVoiture.put("Thierry", "1234 AB 91");
plaquesVoiture.put("Marie", "4369 ERD 75");
plaquesVoiture.put("Julien", "549 DF 87");

System.out.println(plaquesVoiture);
// --> {Thierry=1234 AB 91, Marie=4369 ERD 75, Julien=549 DF 87}
System.out.println(plaquesVoiture.inverse());
// --> {1234 AB 91=Thierry, 4369 ERD 75=Marie, 549 DF 87=Julien}

En Java standard, pour arriver au même résultat, on est soit obligé de gérer deux maps en même temps (une pour les clés et une pour les valeurs) soit de gaspiller son temps à faire des recherches dans les entries.

Pour le reste, une bimap fonctionne comme une map classique.

Test classique du get

@Test
public void testVoitures() {
String plaque = plaquesVoiture.get("Thierry");
System.out.println(plaque);
// --> 1234 AB 91

assertTrue(plaque.equals("1234 AB 91")); // --> true
}

Il est possible de changer la valeur associée à une clé, mais pas d'avoir deux clés avec la même valeur.

Test de plantage

@Test
@Test(expected = IllegalArgumentException.class)
public void testVoitures2() {
System.out.println(plaquesVoiture);
// --> {Thierry=1234 AB 91, Marie=4369 ERD 75, Julien=549 DF 87}

plaquesVoiture.put("Thierry", "456 AB 75");
System.out.println(plaquesVoiture);
// --> {Thierry=456 AB 75, Julien=549 DF 87, Marie=4369 ERD 75}

plaquesVoiture.put("Luc", "456 AB 75");
// --> IllegalArgumentException
}

VI - Collections immuables (immutables)

Le framework Google-Collections introduit un concept fort (et un poil complexe) d'immutabilité. Il définit un ensemble de collections immuables qui de par leur nature simplifient le code (pas besoin de se poser de question sur les risques de changement) et augmentent les performances (fini les synchro). Le contrat principal de ces collections immuables est que le framework garantit le principe d'immuabilité.

Les classes du framework ne possèdent pas de constructeur public afin d'empêcher l'héritage et ainsi d'éviter qu'une classe fille puisse casser l'immutabilité. C'est vraiment un contrat de service fort. Bien que ce ne soit pas l'objectif premier, l'immutabilité permet des gains importants en termes de performances. Les gens de Google annoncent notamment un gain de mémoire d'un facteur de l'ordre de 2 ou 3.

Pour comprendre comment cela fonctionne, il faut revenir aux bases de Java. Pour définir une liste « non modifiable » (notez que je ne dis pas « immutable » ici) il fallait écrire le code suivant :

Création d'une liste non modifiable en Java

public static final Set<Integer> NOMBRES_PREMIERS;
static {
Set<Integer> premiers = new LinkedHashSet<Integer>();
premiers.add(1);
premiers.add(2);
premiers.add(3);
premiers.add(5);
NOMBRES_PREMIERS = Collections.unmodifiableSet(premiers);
}

L'idée est de créer une première liste dans laquelle on place des valeurs puis de la transformer à l'aide de unmodifiableSet(). La variable est ici déclarée en static final pour définir une « constante »…

À la place, on peut utiliser une seule ligne de code, mais au prix de l'utilisation d'un bon nombre de classes et d'appels imbriqués.

Création de la liste en une seule ligne

public static final Set<Integer> NOMBRES_PREMIERS2
= Collections.unmodifiableSet(new LinkedHashSet<Integer>(Arrays.asList(1, 2, 3, 5, 7)));

À l'aide du framework, on va pouvoir créer la liste avec une simple instruction (plus lisible).

Création de la liste avec une commande simplifiée

public static final ImmutableSet<Integer> NOMBRES_PREMIERS3
= ImmutableSet.of(1, 2, 3, 5, 7);

Ici la constante NOMBRES_PREMIERS3 n'utilise pas un simple Set, mais un ImmutableSet pour garantir l'immutabilité et bien faire passer le message.

Quand un programme reçoit un Set d'un autre programme, il ne peut jamais être certain que la liste soit réellement immuable. Il doit donc toujours complexifier ses algorithmes pour en tenir compte. Ou alors il fait confiance à la liste [..] jusqu'au jour où le contrat de service de constance (indiqué généralement dans la Javadoc) ne sera plus respecté. Au contraire, en passant un objet immutable comme ImmutableSet, le programme garantit (puisque les implémentations ne possèdent pas de constructeur public) que la liste est « réellement » immutable et, ainsi, les autres programmes peuvent lui faire confiance : une vraie constante.

Pour que ce soit un peu plus clair, imaginons un programme A qui reçoit une liste (indiquée comme modifiable ou non) d'un programme B. Le programme A n'a pas la main sur la liste et ne sait pas si le programme B risque ou non de la modifier (même si B promet que non) alors une stratégie courante est que le programme A fasse une « copie de défense ».

Copie de défense dans la classe Personne

public class Personne {

private String prenom;
// ...

private Set<String> couleursPreferees;

public Set<String> getCouleursPreferees() {
return couleursPreferees;
}

public void setCouleursPreferees(Set<String> couleursPreferees) {
this.couleursPreferees =
Collections.unmodifiableSet(new LinkedHashSet<String>(couleursPreferees));
}

// ...

La technique est lourde. Heureusement Google-Collections simplifie considérablement ce code, au prix du changement du type de l'attribut « couleursPreferees », mais pas du setter.

et avec une liste immutable

private ImmutableSet<String> couleursPrefereesImmutables;

public Set<String> getCouleursPreferees() {
// return couleursPreferees;
return couleursPrefereesImmutables;
}

public void setCouleursPreferees(Set<String> couleursPreferees) {
// this.couleursPreferees = Collections.unmodifiableSet(
// new LinkedHashSet<String>(couleursPreferees));
this.couleursPrefereesImmutables = ImmutableSet.copyOf(couleursPreferees);
}

L'idée n'est pas seulement de remplacer un Collections.unmodifiableSet(..) par un ImmutableSet.copyOf(..), car cela n'a d'intérêt que dans l'économie de quelques caractères dans le cas standard. Au contraire, s'il s'avère que la liste « couleursPreferees » était déjà immutable, alors le framework sait le détecter et, dans ce cas, il (ie. le copyOf) se contente de la laisser passer.

Au passage, notons que la classe Collections (de Java-Collections) possède la méthode singleton() qui permet de créer un Set immutable avec un seul élément relativement simplement.

Création d'un Set avec un seul élément

Set<String> single = Collections.singleton("Thierry");

Une tentative d'ajout d'un nouvel élément à ce « singleton immutable » se traduit par le lancement d'une exception.

L'ajout d'un élément provoque une exception

@Test (expected=UnsupportedOperationException.class)
public void testSingleton1() {
Set<String> single = Collections.singleton("Thierry");
single.add("Manon"); // --> UnsupportedOperationException
}

Le framework propose également des maps immutables.

Une map immutable

public static final ImmutableMap<String, Integer> AGES
= ImmutableMap.of("Jean", 32, "Paul", 12, "Lucie", 37, "Marie", 17);

et un test d'immutabilité

@Test(expected = UnsupportedOperationException.class)
public void testModifDesAges() {
System.out.println(AGES);
// --> {Jean=32, Paul=12, Lucie=37, Marie=17}

AGES.put("Toto", 40);
// --> UnsupportedOperationException
}

L'instruction AGES.put() lance une exception (UnsupportedOperationException), car la map est immutable. Cela provient du framework Google-Collections indépendamment du fait que AGES soit déclarée « final », car ce mot clé agit sur la référence et non sur son contenu.

On pourrait croire que ImmutableMap.of() est une ellipse, mais pas du tout. Les créateurs de Google-Collections sont même profondément contre. Ils ont créé tous les constructeurs nécessaires pour initialiser les maps avec jusqu'à cinq couples clé-valeur.

Pour construire des grosses maps, il est nécessaire d'utiliser un builder.

Utilisation du builder pour une map plus grosse

public static final ImmutableMap<String, Integer> AGES2
= new ImmutableMap.Builder<String, Integer>()
.put("Jean", 32)
.put("Paul", 12)
.put("Lucie", 37)
.put("Marie", 17)
.put("Bernard", 17)
.put("Toto", 17)
.put("Loulou", 17)
.build();

VII - Des builders

En plus des « builders » déjà présentés (ci-dessus), le framework Google-Collections propose tout un ensemble de classe permettant de construire très simplement des objets complexes.

VII-A - Pour les Maps

Le builder de ImmutableMap présenté dans la section précédente est sans doute l'un des plus intéressants du framework. Toutefois voici quelques exemples de builder spécifiques aux Maps qui méritent quelques secondes d'attention.

Commençons par les Maps concurrentes dont l'utilisation est relativement complexe et la création encore plus. La classe MapMaker simplifie le travail du programmeur. Il est possible, comme dans l'exemple suivant, d'indiquer directement le niveau de concurrence possible.

Création facile d'une ConcurrentMap

ConcurrentMap<String, Integer> scores
= new MapMaker()
.concurrencyLevel(10) /* 10 concurrents max */
.makeMap();

Le builder permet de spécifier tout un ensemble de paramètres comme les stratégies soft ou weak sur les clés et les valeurs. Il permet également d'indiquer une période après laquelle les valeurs disparaissent automatiquement de la map à l'aide de la méthode expiration()

Test de données périmées

@Test
public void testExpiration1() {

final int nombreJoueurs = 10;
final int nombreSpectateurs = 10;
ConcurrentMap<String, Integer> scores = new MapMaker()
.concurrencyLevel(nombreJoueurs + nombreSpectateurs).softKeys()
.weakValues().expiration(5, TimeUnit.SECONDS).makeMap();

final String joueur1 = "Thierry";
scores.put(joueur1, 12);

Integer score1 = scores.get(joueur1);
System.out.println(score1); // --> 12
Assert.assertEquals(new Integer(12), score1); // --> true

sleep(3000); // environ 3 secondes depuis l'ajout dans la map

Integer score1b = scores.get(joueur1);
System.out.println(score1b); // --> 12
Assert.assertNotNull(score1b); // --> true

sleep(3000); // environ 6 secondes depuis l'ajout dans la map

Integer score1c = scores.get(joueur1);

System.out.println(score1c); // --> null
Assert.assertNull(score1c); // --> true
}

Le chrono commence dès l'ajout dans la map et est relancé à chaque modification d'une valeur (déterminée par sa clé)

Test de données périmées avec modifs

@Test
public void testExpiration2() {

final int nombreJoueurs = 10;
final int nombreSpectateurs = 10;
ConcurrentMap<String, Integer> scores = new MapMaker()
.concurrencyLevel(nombreJoueurs + nombreSpectateurs).softKeys()
.weakValues().expiration(5, TimeUnit.SECONDS).makeMap();

final String joueur1 = "Thierry";
scores.put(joueur1, 12);

Integer score1 = scores.get(joueur1);
System.out.println(score1); // --> 12
Assert.assertEquals(new Integer(12), score1); // --> true

sleep(3000); // environ 3 secondes depuis l'ajout dans la map

Integer score1b = scores.get(joueur1);
System.out.println(score1b); // --> 12
Assert.assertNotNull(score1b); // --> true

// Changement de la valeur
scores.put(joueur1, 13);

sleep(3000); // environ 6 secondes depuis l'ajout dans la map mais seulement 3 depuis la maj.

Integer score1c = scores.get(joueur1);

System.out.println(score1c); // --> 13
Assert.assertNotNull(score1c); // --> true
}

VIII - Des préconditions

Les créateurs du framework Google-Collections nous offrent un miniframework de préconditions. Les fonctionnalités offertes sont relativement simples à coder soi-même (7), mais apportent de vraies plus-values, dont l'immense avantage d'être partagées.

Des frameworks comme JUnit ou même Java proposent des mécanismes d'assertion. L'instruction « if » est elle-même l'un des plus simples. Quasiment tous les programmes doivent tester les valeurs qu'on leur passe et agir en conséquence. Un cas fréquent est celui qui oblige une méthode à lancer une exception lorsqu'un paramètre est nul.

Addition avec lancement d'exception si un param est invalide

public Integer additionner1(Integer a, Integer b) {
if (a == null || b == null) {
throw new IllegalArgumentException("Le parem a ou b est null");
}
return a + b;
}

et le test

@Test(expected = IllegalArgumentException.class)
public void testAdditioner1() {
Integer a = 1;
Integer b = 2;
Integer result1 = additionner1(a, b);
System.out.println(result1);

Integer c = 1;
Integer d = null;
Integer result2 = additionner1(c, d);
// --> IllegalArgumentException : Le parem a ou b est null
System.out.println(result2);
}

Cette pratique est plus que classique, mais elle pollue le code. Heureusement Google permet d'écrire des codes plus concis.

Addition avec check

import static com.google.common.base.Preconditions.checkNotNull;

public Integer additionner2(Integer a, Integer b) {
checkNotNull(a, "Le param a est null");
checkNotNull(b, "Le param b est null");
return a + b;
}

et le test

@Test(expected = NullPointerException.class)
public void testAdditioner2() {
Integer a = 1;
Integer b = 2;
Integer result1 = additionner2(a, b);
System.out.println(result1);

Integer c = 1;
Integer d = null;
Integer result2 = additionner2(c, d);
// --> NullPointerException : Le parem b est null
System.out.println(result2);
}

Les fonctions comme checkNotNull() renvoient la valeur transmise ou lancent une exception. Ce mécanisme est d'autant plus intéressant dans un constructeur ou un setter.

check dans un setter

private String prenom;
private int age;

public void setPrenom(String prenom) {
this.prenom = checkNotNull(prenom);
}

public void setAge(int age) {
checkArgument(0 <= age, "Un age ne peut pas être négatif");
this.age = age;
}

Le framework propose tout un ensemble des préconditions dont checkArgument() pour vérifier une expression booléenne, checkElementIndex() ou checkState() pour vérifier un état, etc.

Test si la voiture est à l'arrêt dans la méthode demarrer()

class Voiture {
private boolean running;

public boolean isRunning() {
return running;
}

public void demarrer() {
Preconditions.checkState(!isRunning(), "La voiture roule déjà");
running = true;
System.out.println("Vroum vroum...");
}

public void stop() {
running = false;
}
}

et le test

@Test(expected = IllegalStateException.class)
public void testDemarrer() {
Voiture clio = new Voiture();
clio.demarrer();
// --> Vroum vroum...

clio.demarrer();
// --> IllegalStateException : La voiture roule déjà
}

IX - Functional-collections

Le framework Functional-collections, hébergé chez Google (ici), permet d'aller plus loin.

Pour installer Functional-collections, il suffit d'ajouter une dépendance au pom, comme pour Google-Collections puis de relancer un « mvn clean install »

Ajout dans le pom.xml

<dependency>
<groupId>com.googlecode.functional-collections</groupId>
<artifactId>functional-collections</artifactId>
<version>1.1.8</version>
</dependency>

Le framework ajoute la capacité de chainer des appels, par exemple, sur des filtres. Pour cela, il suffit de les enchainer les uns à la suite des autres.

Des filtres chainés

private static Predicate predicatePrenomTropLong = new Predicate<Personne>() {
public boolean apply(Personne input) {
return input.getPrenom().length() <= 5;
}
};

private static Predicate predicateNomAvecDu = new Predicate<Personne>() {
public boolean apply(Personne input) {
return input.getNom().startsWith("Du");
}
};

static public List<Personne> filtrerEnChaine(List<Personne> personnes) {

FunctionalIterable<Personne> fi = FunctionalIterables.make(personnes)
.filter(predicatePrenomTropLong)
.filter(predicateNomAvecDu);

return newArrayList(fi);
}

Le code est simplifié (plus facile à écrire et à lire) en comparaison avec celui de Google-Collections. En effet, avec ce dernier, il faut encapsuler les appels de méthodes.

Les mêmes filtres chainés (version Google), mais moins faciles à lire

static public List<Personne> filtrerEnChaine2(List<Personne> personnes) {
Iterable<Personne> fi = Iterables.filter(Iterables.filter(
personnes, predicatePrenomTropLong),
predicateNomAvecDu);
return newArrayList(fi);
}

et le test

@Test
public void filtrerEnChaine() {
System.out.println(personnes);
// --> [Anne, Julien, Manon, Mickael, Pascal, Paul, Silvie, Thierry, Zoe]

List<Personne> personnesFiltrees = PersonneUtil.filtrerEnChaine(personnes);
System.out.println(personnesFiltrees);
// --> [Anne]

assertTrue(personnesFiltrees.size() == 1);

List<Personne> personnesFiltrees2 = PersonneUtil.filtrerEnChaine2(personnes);
System.out.println(personnesFiltrees2);

assertTrue(personnesFiltrees2.size() == 1);
}

X - Conclusion

Ce petit aperçu de Google-Collections parle de lui-même et pourtant il y aurait encore beaucoup à découvrir. Toutefois les avantages du framework sautent aux yeux. Quand on y a goûté, il est bien difficile de s'en passer, mais qui le voudrait ?

D'un point de vue technique, les avantages à utiliser le framework Google-Collections sont substantiels, mais c'est davantage du point de vue de la gestion de projets que les avantages deviennent importants. En effet, le framework apporte un support fiable et simple aux collections. Les équipes peuvent se reposer dessus tout en ayant des patterns d'utilisations standardisés. En outre il n'est pas rare d'entendre les développeurs remercier le brillant (8) architecte qui a eu la bonne idée d'ajouter la dépendance dans le pom.

XI - Annexes

XI-A - Les classes utilisées

Les sources utilisées pour cet article sont disponibles dans le zip tuto-google-col.zip. Les principaux éléments sont fournis ci-dessous.

Personne.java

import com.google.common.collect.ImmutableSet;
// ... d'autres imports plus classiques

public class Personne {

private String prenom;
private String nom;
private Integer age;
private Sexe sexe;

private Set<String> couleursPreferees;
private ImmutableSet<String> couleursPrefereesImmutables;

public Set<String> getCouleursPreferees() {
return couleursPreferees;
}

/**
* Copie de défense
*
* @param couleursPreferees
*/
public void setCouleursPreferees(Set<String> couleursPreferees) {
this.couleursPreferees = Collections
.unmodifiableSet(new LinkedHashSet<String>(couleursPreferees));
}

public Set<String> getCouleursPreferees2() {
// return couleursPreferees;
return couleursPrefereesImmutables;
}

/**
* Copie de défense simple avec Google-Collections.
*
* @param couleursPreferees
*/
public void setCouleursPreferees2(Set<String> couleursPreferees) {
// this.couleursPreferees = Collections.unmodifiableSet(
// new LinkedHashSet<String>(couleursPreferees));

this.couleursPrefereesImmutables = ImmutableSet
.copyOf(couleursPreferees);
}

public Personne(String prenom, String nom, Integer age, Sexe sexe) {
this.prenom = prenom;
this.nom = nom;
this.age = age;
this.sexe = sexe;
}

public boolean isHomme() {
return sexe == Sexe.HOMME;
}

public boolean isFemme() {
return sexe == Sexe.FEMME;
}

@Override
public String toString() {
return prenom;
}

// Le reste (getters et setters classiques) dans le zip
}

Sexe.java

public enum Sexe {

HOMME("homme", 1),
FEMME("femme", 2);

final private String label;
final private Integer code;

Sexe(String label, Integer code) {
this.label = label;
this.code = code;
}
}

Humain.java

public class Humain {
private String nomComplet;
private Double age;

public Humain() {
//
}

public Humain(String nomComplet, Double age) {
this.nomComplet = nomComplet;
this.age = age;
}

@Override
public String toString() {
return nomComplet;
}

// Le reste (getters et setters classiques) dans le zip
}

ConverterUtil.java

import static com.google.common.collect.Lists.transform;
import com.google.common.base.Function;
// ... d'autres imports plus classiques

public class ConverterUtil {

public static List<Double> convertir1(List<Integer> liste) {
List<Double> result = new ArrayList<Double>();
for (Integer elt : liste) {
result.add(new Double(elt));
}
return result;
}

public static 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;
}

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

ConverterUtil.java

import static com.google.common.collect.Lists.newArrayList;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.googlecode.functionalcollections.FunctionalIterable;
import com.googlecode.functionalcollections.FunctionalIterables;
// ... d'autres imports plus classiques

public class PersonneUtil {

private static Predicate predicatePrenomTropLong = new Predicate<Personne>() {
public boolean apply(Personne input) {
return input.getPrenom().length() <= 5;
}
};

private static Predicate predicateNomAvecDu = new Predicate<Personne>() {
public boolean apply(Personne input) {
return input.getNom().startsWith("Du");
}
};

static public List<Personne> filtrerEnChaine(List<Personne> personnes) {
FunctionalIterable<Personne> fi = FunctionalIterables.make(personnes)
.filter(predicatePrenomTropLong)
.filter(predicateNomAvecDu);

return newArrayList(fi);
}

static public List<Personne> filtrerEnChaine2(List<Personne> personnes) {
Iterable<Personne> fi = Iterables.filter(
Iterables.filter(personnes, predicatePrenomTropLong),
predicateNomAvecDu);
return newArrayList(fi);
}

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

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

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

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

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

static public List<String> trier(List<String> liste) {
Ordering<String> ordering = Ordering
.from(String.CASE_INSENSITIVE_ORDER);
return ordering.sortedCopy(liste);
}
}

XI-B - Le Pom

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.thi.article.developpez.googlecol</groupId>
<artifactId>tuto-google-col</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>tuto-google-col</name>
<url>http://www.thierryler.com</url>

<properties>
<version.junit>4.8.1</version.junit>
<version.googlecol>1.0</version.googlecol>
<version.funccol>1.1.8</version.funccol>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${version.junit}</version>
</dependency>

<dependency>
<groupId>com.google.collections</groupId>
<artifactId>google-collections</artifactId>
<version>${version.googlecol}</version>
</dependency>

<dependency>
<groupId>com.googlecode.functional-collections</groupId>
<artifactId>functional-collections</artifactId>
<version>${version.funccol}</version>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.google.collections</groupId>
<artifactId>google-collections</artifactId>
</dependency>

<dependency>
<groupId>com.googlecode.functional-collections</groupId>
<artifactId>functional-collections</artifactId>
</dependency>
</dependencies>

<build>
<finalName>Tuto Google Collections</finalName>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.1</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

XI-C - Les collections

Parmi les immutables, on note surtout les classes suivantes :

- ImmutableCollection (abstract)

- - ImmutableList (abstract)

- - ImmutableMultiset

- - ImmutableSet (abstract)

- - + ImmutableSortedSet (abstract)

- ImmutableMap (abstract)

- - ImmutableBiMap (abstract)

- - ImmutableSortedMap

Parmi les multi, on note surtout les classes suivantes :

- Multiset (interface)

- - ConcurrentHashMultiset --> Set en concurences (multithread)

- - EnumMultiset --> pour y mettre des enums

- - ForwardingMultiset (abstract)

- - HashMultiset --> equals() + hashCode()

- - ImmutableMultiset

- - LinkedHashMultiset

- - TreeMultiset --> comparateurs

- Multimap (interface)

- - ListMultimap (interface)

- - + ArrayListMultimap

- - + ImmutableListMultimap

- - + LinkedListMultimap

- - SetMultimap (interface)

- - + HashMultimap

- - + ImmutableSetMultimap

- - + LinkedHashMultimap

- - SortedSetMultimap (interface)

- - + TreeMultimap (aussi dans la hiérarchie de SetMultimap

- - ForwardingMultimap (abstract)

- - ImmutableMultimap (abstract)

Parmi les Bi, on note surtout les classes suivantes :

- EnumBiMap (interface)

- EnumHashBiMap (interface)

- HashBiMap (interface)

- ImmutableBiMap (interface)

XI-D - Imports static faciles

Les développeurs se plaignent souvent à propos des imports static qui sont pénibles à ajouter aux classes dans Eclipse. Grâce à Google-Collections, ils ont donc de quoi se plaindre. Ce qui est un peu rébarbatif, c'est de devoir soit importer toutes les méthodes statiques à l'aide du joker (étoile), soit de les choisir une par une, mais ce n'est pas toujours très aisé, surtout lorsqu'on ne maîtrise pas complètement les bibliothèques externes utilisées. En outre Eclipse ne propose pas de complétion avec import automatique sur les méthodes statiques. Heureusement, des astuces existent ; en voici quelques-unes.

La première méthode consiste à lancer une complétion sur la classe contenant la méthode statique cible, par exemple la classe Lists pour la méthode newArrayList(). La complétion est alors disponible pour la méthode. Toutefois le code s'en retrouve un peu pollué, ce qui donne quelque chose comme suit :

Code pollué par la classe

List<String> liste = Lists.newArrayList();

Il suffit de cliquer avec le bouton droit de la souris sur « newArrayList » et de choisir le menu « Source/Add import » pour que « Lists » disparaisse et que l'import static soit ajouté en haut de la classe.

Une seconde méthode, légèrement plus longue à mettre en œuvre, mais à ne faire qu'une fois, consiste à ajouter un nouveau raccourci de complétion à Eclipse. Pour cela il faut aller dans le menu « Window/Preference » puis dans « Java/Editor/Template » puis cliquer sur le bouton …« New… » et saisir les valeurs comme indiqué dans la capture d'écran suivante.

Ajout d'un template dans Eclipse

(cliquez ici pour aggrandir l'image)

Durant la frappe, Eclipse ouvre des popups pour aider à la saisie et proposer les fonctions adaptées.

Il devient alors possible d'utiliser directement la complétion (Ctrl + Espace) sur la méthode statique comme le montre la capture suivante.

La complétion de newArrayList en action

(cliquez ici pour aggrandir l'image)

XI-E - Remerciements

Je tiens à remercier, en tant qu'auteur de cet article sur Google-Collections, toutes les personnes qui m'ont aidé et soutenu durant la phase d'écriture. Je pense tout d'abord à mes collègues qui subissent mes questions au quotidien, mais aussi à mes contacts et amis du web, dans le domaine de l'informatique ou non, qui m'ont fait part de leurs remarques et critiques. Bien entendu, je n'oublie pas l'équipe de developpez.com qui m'a guidé dans la rédaction de cet article et m'a aidé à le corriger et le faire évoluer. Plus particulièrement j'envoie mes remerciements à Monia, Pottiez et Eric.

Retour à l'article

Retour à l'article

Retour à l'article

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

(5)ou 8 selon les plans d'Oracle

(6)NPE : NullPointerException

(7)On peut dire la même chose de la plupart des frameworks

(8)S'il ne l'est pas, il le devient aux yeux de tous