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.
@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.
<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.
<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.
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.
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.
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.
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.
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.
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é.
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 :
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.
@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
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.
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
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.
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.
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.
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
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.
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é).
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;
}
@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 :
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.
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
(
);
}
@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.
static
public
String asString3
(
List<
Humain>
humains) {
return
Joiner.on
(
SEPARATEUR).join
(
humains);
}
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.
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.
@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.
public
class
Toto {
private
Integer chiffre;
private
String lettre;
@Override
public
String toString
(
) {
return
"Toto "
+
chiffre +
" : "
+
lettre;
}
...
}
@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.
// 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…
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.
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);
}
@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.
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);
}
@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.
static
public
List<
String>
trier
(
List<
String>
liste) {
Ordering<
String>
ordering =
Ordering.from
(
String.CASE_INSENSITIVE_ORDER);
return
ordering.sortedCopy
(
liste);
}
@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<
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
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.
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.
@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.
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.
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
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.
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.
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.
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(..)
// 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).
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é.
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
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é…
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
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
@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 :
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.
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).
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 ».
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.
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.
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.
@Test
(
expected=
UnsupportedOperationException.class
)
public
void
testSingleton1
(
) {
Set<
String>
single =
Collections.singleton
(
"Thierry"
);
single.add
(
"Manon"
); // --> UnsupportedOperationException
}
Le framework propose également des maps immutables.
public
static
final
ImmutableMap<
String, Integer>
AGES
=
ImmutableMap.of
(
"Jean"
, 32
, "Paul"
, 12
, "Lucie"
, 37
, "Marie"
, 17
);
@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.
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.
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
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
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.
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;
}
@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.
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;
}
@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.
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.
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
;
}
}
@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 »
<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.
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.
static
public
List<
Personne>
filtrerEnChaine2
(
List<
Personne>
personnes) {
Iterable<
Personne>
fi =
Iterables.filter
(
Iterables.filter
(
personnes, predicatePrenomTropLong),
predicateNomAvecDu);
return
newArrayList
(
fi);
}
@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.
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
}
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;
}
}
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
}
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;
}
}
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▲
<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 :
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.
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.
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.