I. Introduction▲
Cet article est le quatrième d'une série consacrée à la bibliothèque Guava :
- introduction et installation ;
- collections ;
- programmation fonctionnelle ;
- utilitaires ;
- cache et concurrence ;
- tout pour vos Strings et primitifs ;
- un peu de math ;
- hash et I/O.
I-A. Versions des logiciels et bibliothèques utilisées▲
Pour écrire ce document, j'ai utilisé les versions suivantes ;
- Java JDK 1.6.0_24-b07 ;
- Eclipse Indigo 3.7 JEE 64b ;
- Maven 3.0.3 ;
- JUnit 4.10 ;
- Guava 14.0.
J'utilise Java 6, car Java 7 n'est pas encore très répandu en entreprise. C'est ce que je vérifie durant mes conférences lorsque je demande qui utilise Java 7 sur ses serveurs de production, mais que très peu de mains se lèvent…
I-B. Mises à jour▲
9 octobre 2013 : création
II. Utilitaires▲
II-A. Extention de Object▲
II-A-1. equals at hashCode▲
Je suis prêt à parier que vous utilisez les fonctionnalités de génération de code d'Eclipse lorsque vous avez à écrire les méthodes « equals(..) » et « hashCode() ». C'est même un automatisme, car vous pouvez vous débarrasser de cette affaire en quelques clics. Le code produit n'est pourtant pas des plus heureux. Voyons ce que ça donne sur notre objet « Chien » en ne prenant en compte que l'attribut « name » pour commencer :
@Override
public
boolean
equals
(
Object obj) {
if
(
this
==
obj)
return
true
;
if
(
obj ==
null
)
return
false
;
if
(
getClass
(
) !=
obj.getClass
(
))
return
false
;
Chien other =
(
Chien) obj;
if
(
name ==
null
) {
if
(
other.name !=
null
)
return
false
;
}
else
if
(!
name.equals
(
other.name))
return
false
;
return
true
;
}
@Override
public
int
hashCode
(
) {
final
int
prime =
31
;
int
result =
1
;
result =
prime *
result +
((
name ==
null
) ? 0
: name.hashCode
(
));
return
result;
}
Ça peut aller pour « hashCode() », mais la méthode « equals(..) » est carrément illisible alors qu'on ne s'occupe que d'un seul attribut. Voyons ce que cela donne en ajoutant des attributs :
@Override
public
boolean
equals
(
Object obj) {
if
(
this
==
obj)
return
true
;
if
(
obj ==
null
)
return
false
;
if
(
getClass
(
) !=
obj.getClass
(
))
return
false
;
Chien other =
(
Chien) obj;
if
(
birthday ==
null
) {
if
(
other.birthday !=
null
)
return
false
;
}
else
if
(!
birthday.equals
(
other.birthday))
return
false
;
if
(
fullName ==
null
) {
if
(
other.fullName !=
null
)
return
false
;
}
else
if
(!
fullName.equals
(
other.fullName))
return
false
;
if
(
lof ==
null
) {
if
(
other.lof !=
null
)
return
false
;
}
else
if
(!
lof.equals
(
other.lof))
return
false
;
if
(
name ==
null
) {
if
(
other.name !=
null
)
return
false
;
}
else
if
(!
name.equals
(
other.name))
return
false
;
if
(
race ==
null
) {
if
(
other.race !=
null
)
return
false
;
}
else
if
(!
race.equals
(
other.race))
return
false
;
if
(
sex !=
other.sex)
return
false
;
return
true
;
}
@Override
public
int
hashCode
(
) {
final
int
prime =
31
;
int
result =
1
;
result =
prime *
result +
((
birthday ==
null
) ? 0
: birthday.hashCode
(
));
result =
prime *
result +
((
fullName ==
null
) ? 0
: fullName.hashCode
(
));
result =
prime *
result +
((
lof ==
null
) ? 0
: lof.hashCode
(
));
result =
prime *
result +
((
name ==
null
) ? 0
: name.hashCode
(
));
result =
prime *
result +
((
race ==
null
) ? 0
: race.hashCode
(
));
result =
prime *
result +
((
sex ==
null
) ? 0
: sex.hashCode
(
));
return
result;
}
On voit que ça empire. C'est même la catastrophe alors qu'on est loin d'avoir employé tous les attributs disponibles. Autant dire que vous n'irez jamais mettre les mains dans ce code et que ça va exploser au moindre bogue.
Au passage, savez-vous à quoi correspond le nombre « 31 » dans la méthode « hashCode() » ? Cette valeur a une grande signification… Voici ce qu'en dit Joshua Bloch dans son livre « Effective Java » : « The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance. Modern VMs do this sort of optimization automatically. » Un collègue de Developpez.com m'en a gentiment proposé une traduction : « 31 a été choisi, car il s'agit d'un nombre premier impair. S'il avait été pair et que la multiplication avait provoqué un débordement, des informations auraient été perdues, puisque multiplier par 2 revient à faire un décalage (de bits). L'avantage d'utiliser un nombre premier est moins clair, mais c'est la tradition. L'une des propriétés intéressantes de 31 est que la multiplication peut être remplacée par un décalage et une soustraction pour de meilleures performances. Cette optimisation est prise en compte de manière automatique par les VM modernes. »
À l'aide de Guava, on va pouvoir réellement simplifier tout cela, tout en faisant bien attention à ce que les deux méthodes continuent de respecter des contrats similaires :
@Override
public
boolean
equals
(
Object obj) {
if
(
obj ==
null
||
!(
obj instanceof
Chien)) {
return
false
;
}
final
Chien other =
(
Chien) obj;
return
Objects.equal
(
birthday, other.birthday)
&&
Objects.equal
(
fullName, other.fullName)
&&
Objects.equal
(
lof, other.lof)
&&
Objects.equal
(
name, other.name)
&&
Objects.equal
(
race, other.race)
&&
Objects.equal
(
sex, other.sex);
}
Je crois qu'il n'y a pas photo…
Bien que cela soit assez flagrant pour la méthode « equals(..) », ça me parait encore plus important pour « hashCode() » :
@Override
public
int
hashCode
(
) {
return
Objects.hashCode
(
birthday, fullName, lof, name, race, sex);
}
Il n'y a carrément plus de question à se poser… C'est magique.
II-A-2. toString▲
Comme pour les deux méthodes précédentes, je parie que vous générez vos « toString() » à l'aide de quelques clics sous Eclipse.
@Override
public
String toString
(
) {
return
"Chien [name="
+
name //
+
", fullName="
+
fullName //
+
", birthday="
+
birthday //
+
", sex="
+
sex //
+
", race="
+
race //
+
", id="
+
id
+
", lof="
+
lof //
+
", weight="
+
weight //
+
", size="
+
size //
+
", colors="
+
colors +
"]"
;
}
Même si on a vu pire, il faut bien reconnaître que ce n'est pas top. Et encore, j'ai formaté le code pour qu'il soit lisible… La version Guava est un poil meilleure :
@Override
public
String toString
(
) {
return
Objects.toStringHelper
(
"Dog"
)
.add
(
"name"
, name)
.add
(
"fullName"
, fullName)
.add
(
"birthday"
, birthday)
.add
(
"sex"
, sex)
.add
(
"race"
, race)
.add
(
"id"
, id)
.add
(
"lof"
, lof)
.add
(
"weight"
, weight)
.add
(
"size"
, size)
.add
(
"colors"
, colors)
.toString
(
);
}
Au fait, ce pattern s'appela « combinator framework » et il est inspiré des monades dans le paradigme fonctionnel. Le retour de la fonction est l'argument de la fonction suivante et ainsi de suite. Un exemple de combinator framework est jparsec.
II-B. Comparaisons▲
Milou, le chien, est coquet. Il veut se comparer aux autres chiens. Pour cela, il doit d'abord implémenter l'interface « Comparable » qui demande d'écrire la méthode « compareTo(..) ». Je crois que vous avez déjà assez souffert avec cette affaire, dans votre vie de développeur, donc je vous épargne la version en Java pur. Voici ce que ça donne avec Guava :
public
class
Chien implements
Comparable<
Chien>
{
...
@Override
public
int
compareTo
(
Chien other) {
return
ComparisonChain.start
(
)
.compare
(
name, other.name)
.compare
(
fullName, other.fullName)
.compare
(
birthday, other.birthday)
.compare
(
sex, other.sex)
.result
(
);
}
Vous remarquez d'abord que cela n'occupe que quelques lignes contre des dizaines en Java standard. Notez aussi que je ne vérifie pas la nullité éventuelle des attributs, car la bibliothèque s'en occupe. Enfin, grâce à la structure d'appel, il est très simple de vérifier qu'on n'a pas inversé l'argument de gauche avec celui de droite. En effet, on se demande toujours dans quel sens prendre cette méthode. Et qui n'a jamais inversé « name.compareTo(other.name) » et « other.name.compareTo(name) » par inattention, provoquant ainsi un bogue hyper difficile à trouver ?…
II-C. Stop Watch▲
Lorsqu'on souhaite chronométrer le temps que prend un bloc de code à s'exécuter, il n'y a pas beaucoup de solutions en Java. D'une façon ou d'une autre, on doit utiliser la date du système qu'on note avant et après. Une simple soustraction donne la durée :
final
long
start =
System.currentTimeMillis
(
);
...
// Ici le bloc dont on veut mesurer la duree.
...
final
long
end =
System.currentTimeMillis
(
);
final
lond duree =
end -
start;
Et si on veut des durées intermédiaires, il suffit de noter autant de fois que besoin la date système :
final
long
start =
System.currentTimeMillis
(
);
...
final
long
date1 =
System.currentTimeMillis
(
);
...
final
long
date2 =
System.currentTimeMillis
(
);
...
final
lond duree1 =
date1 -
start;
final
long
duree2 =
date2 -
date1;
...
Ce n'est pas très compliqué, mais on sent bien qu'on peut faire mieux. Pour cela, à l'aide de Guava, on va utiliser le StopWatch :
final
Stopwatch sw =
new
Stopwatch
(
);
sw.start
(
);
...
// Ici le bloc dont on veut mesurer la duree.
...
final
long
duree =
sw.elapsedMillis
(
);
sw.stop
(
);
Si on veut les durées intermédiaires, il suffit de faire appel à « reset() » et de relancer :
final
Stopwatch sw =
new
Stopwatch
(
);
sw.start
(
);
...
final
long
duree1 =
sw.elapsedMillis
(
);
sw.reset
(
); // Stoppe et remet a zero
sw.start
(
);
...
final
long
duree1 =
sw.elapsedMillis
(
);
Pour ma part, je trouve que cela ressemble déjà plus à un chronomètre. Mais là où c'est vraiment sympa, c'est que cela permet d'avoir une précision bien supérieure à la milliseconde ou, au contraire, bien inférieure. Il suffit de préciser l'unité :
long
jours =
sw.elapsed
(
TimeUnit.DAYS);
long
heures =
sw.elapsed
(
TimeUnit.HOURS);
long
minutes =
sw.elapsed
(
TimeUnit.MINUTES);
long
secondes =
sw.elapsed
(
TimeUnit.SECONDS);
long
millisecondes =
sw.elapsed
(
TimeUnit.MILLISECONDS);
long
microsecondes =
sw.elapsed
(
TimeUnit.MICROSECONDS);
long
nanosecondes =
sw.elapsed
(
TimeUnit.NANOSECONDS);
Il est légitime de devoir être plus précis que la milliseconde, mais pourquoi voudrait-on être moins précis ? Tout simplement parce qu'il existe de nombreux domaines dans lesquels trop de précision ne sert à rien. On pourrait ainsi penser au transport de fret par voie ferrée qui est un domaine dans lequel le temps réel se compte en minutes à l'inverse de l'astronomie qui nécessite une précision fine.
Peut-on faire confiance dans une mesure exprimée en nanosecondes ? Nos processeurs fonctionnent globalement tous à des fréquences de l'ordre de 2,5 GHz. Ça veut dire que le CPU est capable de faire quatre ou cinq cycles d'horloge par nanoseconde… Est-ce que cela suffit pour garantir la précision ? J'avoue que je n'en sais rien… Dans tous les cas, la bonne pratique consiste à mesurer l'opération répétée un certain nombre de fois et de faire la moyenne.
Je vous accorde que ce n'est pas très pratique, ni très sexy, de devoir enchaîner autant de méthodes. Je peux vous proposer une petite classe personnelle :
public
class
QuickStopwatch {
private
Stopwatch stopwatch;
private
QuickStopwatch
(
) {
stopwatch =
new
Stopwatch
(
);
}
public
static
QuickStopwatch createAndStart
(
) {
final
QuickStopwatch quickStopwatch =
new
QuickStopwatch
(
);
quickStopwatch.stopwatch.start
(
);
return
quickStopwatch;
}
public
void
stop
(
) {
stopwatch.stop
(
);
}
public
long
restart
(
) {
return
restart
(
TimeUnit.MICROSECONDS);
}
public
long
restart
(
TimeUnit desiredUnit) {
final
long
elapsed =
stopwatch.elapsed
(
desiredUnit);
stopwatch.reset
(
);
stopwatch.start
(
);
return
elapsed;
}
}
Du coup, le cas d'utilisation décrit plus haut se résume au suivant :
final
QuickStopwatch qsw =
QuickStopwatch.createAndStart
(
);
...
final
long
millis1 =
qsw.restart
(
);
...
final
long
nano2 =
qsw.restart
(
TimeUnit.NANOSECONDS);
À lire, un billet de blog intitulé « Le Stop watch de Guava ».
À lire, un billet de blog intitulé « Quick Stop Watch pour Guava ».
II-D. Gérer le null▲
Le cas du null est assez particulier en Java. Il est au cœur de nombreuses problématiques, dont les solutions consistent généralement à l'éviter.
II-D-1. Vide ou carrément null ?▲
Qui n'a jamais dû tester la nullité d'une string, par exemple à l'occasion de la comparaison avec une valeur particulière ?
String s =
...
if
(
s.equals
(
"abcd"
) ) {
...
}
Bien entendu, il faut tester la nullité pour éviter les exceptions :
String s =
...
if
(
s !=
null
&&
s.equals
(
"abcd"
) ) {
...
}
Et de manière encore plus générale, on tester qu'une String n'est ni nulle ni vide :
String s =
...
if
(
s !=
null
&&
!
s.equals
(
""
) ) {
...
}
Ou même mieux :
String s =
...
if
(
s !=
null
&&
!
s.isEmpty
(
) ) {
...
}
J'aime bien créer une méthode utilitaire, que j'appelle « isNullOrEmpty(..) » ou plus sobrement « noe(..) ». Ça tombe bien puisque Guava propose la même chose :
@Test
public
void
testNOE1
(
) {
// Arrange
final
String s =
null
;
// Act
final
boolean
result =
Strings.isNullOrEmpty
(
s);
// Assert
Assert.assertTrue
(
result);
}
@Test
public
void
testNOE2
(
) {
// Arrange
final
String s =
""
;
// Act
final
boolean
result =
Strings.isNullOrEmpty
(
s);
// Assert
Assert.assertTrue
(
result);
}
Mais parfois, on voudrait surtout avoir une chaîne vide à la place d'une référence nulle. On va alors utiliser la méthode « nullToEmpty(..) » :
@Test
public
void
testNullString
(
) {
// Arrange
final
String s =
null
;
final
String expected =
""
;
// Act
final
String result =
Strings.nullToEmpty
(
s);
// Assert
Assert.assertEquals
(
expected, result);
Assert.assertNotNull
(
expected);
}
Je précise que la méthode fonctionne aussi sur une valeur non nulle :
@Test
public
void
testNullString
(
) {
// Arrange
final
String s =
null
;
final
String expected =
""
;
// Act
final
String result =
Strings.nullToEmpty
(
s);
// Assert
Assert.assertEquals
(
expected, result);
Assert.assertNotNull
(
expected);
}
On peut donc l'appliquer de manière systématique, à titre préventif :
final
String s =
...
final
String s2 =
Strings.nullToEmpty
(
s);
doSomething
(
s2);
À l'opposé, si on préfère travailler avec des valeurs nulles, à la place d'une string vide, on peut utiliser la méthode « emptyToNull(..) », de façon systématique également :
@Test
public
void
testEmptyString
(
) {
// Arrange
final
String s =
""
;
final
String expected =
null
;
// Act
final
String result =
Strings.emptyToNull
(
s);
// Assert
Assert.assertEquals
(
expected, result);
Assert.assertNull
(
expected);
}
II-D-2. Optional▲
Dans de nombreux programmes, la valeur « null » sert à indiquer une sorte d'absence, par exemple lorsqu'on n'a pas trouvé une valeur, ou lorsqu'il y a eu une erreur. Cette stratégie est ambiguë et oblige les développeurs à blinder (polluer) le code pour s'en prémunir.
Le plus simple, quand on veut éviter les valeurs nulles, c'est de ne pas en avoir. Facile à dire ? Pour cela, Guava propose d'encapsuler vos objets dans des wrappers qui, eux, ne sont forcément pas nuls :
Chien milou =
new
Chien
(
"Milou"
);
Optional<
Chien>
opt =
Optional.of
(
milou);
Précisons tout de suite qu'on mange une exception si on essaie de créer un wrapper avec une valeur nulle :
@Test
(
expected =
NullPointerException.class
)
public
void
testOptionalNull
(
) {
// Arrange
Chien milou =
null
;
final
String expected =
"Milou"
;
// Act
Optional<
Chien>
opt =
Optional.of
(
milou); // NPE
}
On va donc plutôt utiliser, systématiquement, la méthode « fromNullable(..) » à la place de « of(..) » :
@Test
public
void
testOptionalNull2
(
) {
// Arrange
Chien milou =
null
;
final
String expected =
"Milou"
;
// Act
Optional<
Chien>
opt =
Optional.fromNullable
(
milou);
boolean
present =
opt.isPresent
(
);
// Assert
Assert.assertFalse
(
present);
}
Pendant qu'on y est, on peut même créer volontairement un wrapper vide (i.e. avec une valeur nulle) à l'aide de « absent(..) » pour indiquer par exemple qu'une requête en base n'aurait pas trouvé l'objet cherché :
@Test
public
void
testOptionalNullAbsent
(
) {
// Arrange
// ...
// Act
Optional<
Chien>
opt =
Optional.absent
(
);
boolean
present =
opt.isPresent
(
);
// Assert
Assert.assertFalse
(
present);
}
Alors ? Comment ça fonctionne ? Ce wrapper possède principalement deux fonctions simples. Les méthodes « isPresent() » et « get() » permettent respectivement de savoir si le wrapper contient ou non une valeur (i.e. non nulle) et de la récupérer.
@Test
public
void
testOptional
(
) {
// Arrange
Chien milou =
new
Chien
(
"Milou"
);
// Act
Optional<
Chien>
opt =
Optional.of
(
milou);
boolean
present =
opt.isPresent
(
);
// Assert
Assert.assertTrue
(
present);
}
@Test
public
void
testOptional2
(
) {
// Arrange
Chien milou =
new
Chien
(
"Milou"
);
final
String expected =
"Milou"
;
// Act
Optional<
Chien>
opt =
Optional.of
(
milou);
Chien c =
opt.get
(
);
// Assert
Assert.assertEquals
(
expected, c.getName
(
));
}
Il faut tester le retour de « isPresent() » avant de faire appel à « get() » :
Chien milou =
new
Chien
(
"Milou"
);
Optional<
Chien>
opt =
...
if
(
opt.isPresent
(
) ) {
Chien c =
opt.get
(
);
}
Sinon on prend le risque d'avoir une exception en cas de valeur nulle :
@Test
(
expected =
IllegalStateException.class
)
public
void
testOptionalNull4
(
) {
// Arrange
Chien milou =
null
;
// Act
Optional<
Chien>
opt =
Optional.fromNullable
(
milou);
Chien c =
opt.get
(
); // throws IllegalStateException
}
Certains lecteurs trouveront qu'il n'y a pas tant de différences entre tester la nullité d'une variable et vérifier que l'optional contient une valeur.
public
void
foo
(
String value) {
if
(
value ==
null
) {
// Gestion d'erreur
}
...
}
C'est pourtant le jour et la nuit. Avec les optionals, on manipule toujours des objets non nuls. Cela va avoir un impact fort lors de l'exécution du programme, notamment dans l'arbre de décision de la JVM. Je vous invite à regarder les présentations de Rémi Forax pour en découvrir un peu plus sur ce sujet.
Si on veut faire plus simple, on utilisera la méthode « orNull() » qui renvoie la valeur du wrapper ou tout simplement « null » s'il est vide :
@Test
public
void
testOptionalOr
(
) {
// Arrange
Chien milou =
null
;
// Act
Optional<
Chien>
opt =
Optional.fromNullable
(
milou);
Chien c =
opt.orNull
(
);
// Assert
Assert.assertNull
(
c);
}
Mais le mieux est encore d'utiliser « or(..) » en spécifiant une valeur de remplacement pour le cas où le wrapper serait vide :
@Test
public
void
testOptionalOr
(
) {
// Arrange
Chien milou =
null
;
final
Chien remplacement =
new
Chien
(
"noname"
);
final
String expected =
"noname"
;
// Act
Optional<
Chien>
opt =
Optional.fromNullable
(
milou);
Chien c =
opt.or
(
remplacement);
// Assert
Assert.assertEquals
(
expected, c.getName
(
));
}
À lire, un billet de blog intitulé « Le wrapper Optional de Guava ».
Un objet nommé « Option » existe déjà en Scala et un objet « Optional » arrivera dans Java 8 au premier trimestre 2014...
II-E. Préconditions▲
Dans votre code, vous êtes amené à tester les valeurs passées à vos méthodes à de nombreuses occasions, l'exécution des dites méthodes s'arrêtant si les conditions ne sont pas remplies. On appelle cela des « préconditions ». Voici un exemple en Java standard pour que ce soit plus clair :
public
Chien
(
final
String name) {
if
(
name ==
null
) {
throw
new
NullPointerException
(
"Le nom du chien ne peut pas être null."
);
}
if
(
name.isEmpty
(
)) {
throw
new
IllegalArgumentException
(
"Le nom du chien ne peut pas être vide."
);
}
this
.name =
name;
}
Ce n'est pas du code horrible, mais on sent qu'on peut faire mieux, surtout quand il y a de nombreux arguments. À l'aide de Guava, cet exemple de constructeur se simplifie :
import
static
com.google.common.base.Preconditions.checkArgument;
import
static
com.google.common.base.Preconditions.checkNotNull;
...
public
Chien
(
final
String name) {
checkNotNull
(
name, "Le nom du chien ne peut pas être null."
);
checkArgument
(
!
name.isEmpty
(
), "Le nom du chien ne peut pas être vide."
);
this
.name =
name;
}
Au passage, on peut faire directement des affectations en une ligne :
public
void
foo
(
String field) {
this
.field =
checkNotNull
(
field, "Message d"
erreur si null
.");
}
Au lieu de deux lignes :
public
void
foo
(
String field) {
checkNotNull
(
field, "Message d"
erreur si null
.");
this
.field =
field;
}
En plus de « checkNotNull(..) » et « checkArgument(..) », Guava propose plusieurs autres types de préconditions sur le même modèle :
- checkArgument(boolean), qui renvoie une IllegalArgumentException (IAE) ;
- checkNotNull(T), qui renvoie une NullPointerException (NPE) ;
- checkState(boolean), qui renvoie une IllegalStateException (ISE) ;
- checkElementIndex(int index, int size), qui renvoie une IndexOutOfBoundsException si l'index est supérieur ou égal à size (ou négatif) ;
- checkPositionIndex(int index, int size), qui renvoie une IndexOutOfBoundsException si l'index est supérieur strictement à size (ou négatif) ;
- checkPositionIndexes(int start, int end, int size), qui renvoie une IndexOutOfBoundsException quand l'intervalle [start, end[ n'est pas compatible avec size.
Précisons qu'on peut passer des valeurs dans le message d'erreur :
checkArgument
(
name.length
(
) <=
8
,
"Le nom du chien doit contenir moins de huit digits, mais %s en contient %s."
,
name, name.length
(
) );
II-F. Tris▲
On vous dit tout le temps qu'il ne faut pas redévelopper les algorithmes de tri, à raison. Mais en même temps, c'est toujours un peu la galère lorsqu'on veut trier nos listes avec des subtilités. Heureusement Guava simplifie tout ça à l'aide des « Orderings ». Voyons ça sur un premier tri simple :
@Test
public
void
testOrdering
(
) {
// Arrange
final
List<
Chien>
chiens =
newArrayList
(
new
Chien
(
"Milou"
),
new
Chien
(
"Pluto"
),
new
Chien
(
"Lassie"
),
new
Chien
(
"Volt"
),
new
Chien
(
"Rantanplan"
),
new
Chien
(
"Idefix"
));
// -> [Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Lassie}, Dog{name=Volt}, Dog{name=Rantanplan}, Dog{name=Idefix}]
final
String firstExpected =
"Idefix"
;
final
String secondExpected =
"Lassie"
;
// Act
final
Ordering<
Chien>
ordering =
new
Ordering<
Chien>(
) {
@Override
public
int
compare
(
Chien left, Chien right) {
return
left.compareTo
(
right);
}
}
;
final
List<
Chien>
result =
ordering.sortedCopy
(
chiens);
// -> [Dog{name=Idefix}, Dog{name=Lassie}, Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Rantanplan}, Dog{name=Volt}]
// Assert
Assert.assertEquals
(
firstExpected, result.get
(
0
).getName
(
));
Assert.assertEquals
(
secondExpected, result.get
(
1
).getName
(
));
}
Les deux lignes importantes sont celles de la définition de l'ordering sous forme de classe anonyme dans laquelle on redéfinit la méthode « compareTo », et la ligne qui l'utilise pour lancer le tri à proprement parler.
Dans ce premier exemple, on utilisait le comparateur naturel des chiens « left.compareTo(right) », ce qui revient à utiliser le code suivant :
@Test
public
void
testOrderingNatural
(
) {
// Arrange
final
List<
Chien>
chiens =
newArrayList
(
new
Chien
(
"Milou"
),
new
Chien
(
"Pluto"
),
new
Chien
(
"Lassie"
),
new
Chien
(
"Volt"
),
new
Chien
(
"Rantanplan"
),
new
Chien
(
"Idefix"
));
// -> [Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Lassie}, Dog{name=Volt}, Dog{name=Rantanplan}, Dog{name=Idefix}]
final
String firstExpected =
"Idefix"
;
final
String secondExpected =
"Lassie"
;
// Act
final
Ordering<
Chien>
ordering =
Ordering.natural
(
);
final
List<
Chien>
result =
ordering.sortedCopy
(
chiens);
// -> [Dog{name=Idefix}, Dog{name=Lassie}, Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Rantanplan}, Dog{name=Volt}]
// Assert
Assert.assertEquals
(
firstExpected, result.get
(
0
).getName
(
));
Assert.assertEquals
(
secondExpected, result.get
(
1
).getName
(
));
}
Mais disons qu'on ait besoin d'un tri utilisant des attributs bien particuliers, dans ce cas il suffit de spécialiser la méthode « compareTo(..) », par exemple sur le nom du chien :
@Test
public
void
testOrderingSpecifique
(
) {
// Arrange
final
List<
Chien>
chiens =
newArrayList
(
new
Chien
(
"Milou"
),
new
Chien
(
"Pluto"
),
new
Chien
(
"Lassie"
),
new
Chien
(
"Volt"
),
new
Chien
(
"Rantanplan"
),
new
Chien
(
"Idefix"
));
// -> [Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Lassie}, Dog{name=Volt}, Dog{name=Rantanplan}, Dog{name=Idefix}]
final
String firstExpected =
"Idefix"
;
final
String secondExpected =
"Lassie"
;
// Act
final
Ordering<
Chien>
ordering =
new
Ordering<
Chien>(
) {
@Override
public
int
compare
(
Chien left, Chien right) {
return
left.getName
(
).compareTo
(
right.getName
(
));
}
}
;
final
List<
Chien>
result =
ordering.sortedCopy
(
chiens);
// -> [Dog{name=Idefix}, Dog{name=Lassie}, Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Rantanplan}, Dog{name=Volt}]
// Assert
Assert.assertEquals
(
firstExpected, result.get
(
0
).getName
(
));
Assert.assertEquals
(
secondExpected, result.get
(
1
).getName
(
));
}
Dans la suite, je vais me contenter d'utiliser l'ordre naturel.
Il faut aussi prendre garde aux valeurs nulles dans la liste puisque Guava n'aime pas beaucoup les nuls, comme on l'a déjà mentionné. Il est possible de les placer à la fin ou au début, bien que je ne voi pas vraiment de bonne raison de les mettre en tête de liste. Pour cela, on va simplement chaîner les méthodes « nullsLast() » ou « nullsFirst() » :
@Test
public
void
testOrderingNullLast
(
) {
// Arrange
final
List<
Chien>
chiens =
newArrayList
(
new
Chien
(
"Milou"
),
new
Chien
(
"Pluto"
),
new
Chien
(
"Lassie"
),
null
,
new
Chien
(
"Volt"
),
new
Chien
(
"Rantanplan"
),
new
Chien
(
"Idefix"
));
// -> [Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Lassie}, null, Dog{name=Volt}, Dog{name=Rantanplan}, Dog{name=Idefix}]
final
String firstExpected =
"Idefix"
;
final
String secondExpected =
"Lassie"
;
// Act
final
Ordering<
Chien>
ordering =
new
Ordering<
Chien>(
) {
@Override
public
int
compare
(
Chien left, Chien right) {
return
left.compareTo
(
right);
}
}
;
final
List<
Chien>
result =
ordering.nullsLast
(
).sortedCopy
(
chiens); // NPE si on oublie nullsLast()
// -> [Dog{name=Idefix}, Dog{name=Lassie}, Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Rantanplan}, Dog{name=Volt}, null]
// Assert
Assert.assertEquals
(
firstExpected, result.get
(
0
).getName
(
));
Assert.assertEquals
(
secondExpected, result.get
(
1
).getName
(
));
}
Comme souvent avec Guava, il y a des petits bonus comme le fait de pouvoir renverser l'ordre du tri :
@Test
public
void
testOrderingNullLastReverse
(
) {
// Arrange
final
List<
Chien>
chiens =
newArrayList
(
new
Chien
(
"Milou"
),
new
Chien
(
"Pluto"
),
new
Chien
(
"Lassie"
),
null
,
new
Chien
(
"Volt"
),
new
Chien
(
"Rantanplan"
),
new
Chien
(
"Idefix"
));
// -> [Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Lassie}, null, Dog{name=Volt}, Dog{name=Rantanplan}, Dog{name=Idefix}]
final
String firstExpected =
"Volt"
;
final
String secondExpected =
"Rantanplan"
;
// Act
final
Ordering<
Chien>
ordering =
new
Ordering<
Chien>(
) {
@Override
public
int
compare
(
Chien left, Chien right) {
return
left.compareTo
(
right);
}
}
;
final
List<
Chien>
result =
ordering.reverse
(
).nullsLast
(
).sortedCopy
(
chiens);
// -> [Dog{name=Volt}, Dog{name=Rantanplan}, Dog{name=Pluto}, Dog{name=Milou}, Dog{name=Lassie}, Dog{name=Idefix}, null]
// Assert
Assert.assertEquals
(
firstExpected, result.get
(
0
).getName
(
));
Assert.assertEquals
(
secondExpected, result.get
(
1
).getName
(
));
}
Autre petit bonus, il est possible de vérifier si la liste est déjà triée, ce qui n'est pas si rare que ça :
@Test
public
void
testOrderingAllreadyOrdered
(
) {
// Arrange
final
List<
Chien>
chiens =
newArrayList
(
new
Chien
(
"Milou"
),
new
Chien
(
"Pluto"
),
new
Chien
(
"Lassie"
),
new
Chien
(
"Volt"
),
new
Chien
(
"Rantanplan"
),
new
Chien
(
"Idefix"
));
// -> [Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Lassie}, Dog{name=Volt}, Dog{name=Rantanplan}, Dog{name=Idefix}]
// Act
final
Ordering<
Chien>
ordering =
new
Ordering<
Chien>(
) {
@Override
public
int
compare
(
Chien left, Chien right) {
return
left.compareTo
(
right);
}
}
;
final
List<
Chien>
result =
ordering.sortedCopy
(
chiens);
// -> [Dog{name=Idefix}, Dog{name=Lassie}, Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Rantanplan}, Dog{name=Volt}]
final
boolean
isChienSorted =
ordering.isOrdered
(
chiens);
final
boolean
isResultSorted =
ordering.isOrdered
(
result);
// Assert
Assert.assertFalse
(
isChienSorted);
Assert.assertTrue
(
isResultSorted);
}
On peut aussi préciser si la liste est strictement triée, et qu'elle ne contient donc pas d'égalité.
Encore une fonction sympa, on peut directement rechercher les valeurs les plus grandes et/ou les plus petites :
@Test
public
void
testOrderingMinMax
(
) {
// Arrange
final
List<
Chien>
chiens =
newArrayList
(
new
Chien
(
"Milou"
),
new
Chien
(
"Pluto"
),
new
Chien
(
"Lassie"
),
new
Chien
(
"Volt"
),
new
Chien
(
"Rantanplan"
),
new
Chien
(
"Idefix"
));
// -> [Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Lassie}, Dog{name=Volt}, Dog{name=Rantanplan}, Dog{name=Idefix}]
final
String minExpected =
"Idefix"
;
final
String maxExpected =
"Volt"
;
// Act
final
Ordering<
Chien>
ordering =
new
Ordering<
Chien>(
) {
@Override
public
int
compare
(
Chien left, Chien right) {
return
left.compareTo
(
right);
}
}
;
final
Chien min =
ordering.min
(
chiens);
final
Chien max =
ordering.max
(
chiens);
// Assert
Assert.assertEquals
(
minExpected, min.getName
(
));
Assert.assertEquals
(
maxExpected, max.getName
(
));
}
Et si on veut les N plus grands/petits, on peut utiliser les méthodes « greatestOf(..) » et « leastOf(..) » :
@Test
public
void
testOrderingGreatest
(
) {
// Arrange
final
List<
Chien>
chiens =
newArrayList
(
new
Chien
(
"Milou"
),
new
Chien
(
"Pluto"
),
new
Chien
(
"Lassie"
),
new
Chien
(
"Volt"
),
new
Chien
(
"Rantanplan"
),
new
Chien
(
"Idefix"
));
// -> [Dog{name=Milou}, Dog{name=Pluto}, Dog{name=Lassie}, Dog{name=Volt}, Dog{name=Rantanplan}, Dog{name=Idefix}]
final
String maxExpected1 =
"Volt"
;
final
String maxExpected2 =
"Rantanplan"
;
final
int
nb =
3
;
// Act
final
Ordering<
Chien>
ordering =
new
Ordering<
Chien>(
) {
@Override
public
int
compare
(
Chien left, Chien right) {
return
left.compareTo
(
right);
}
}
;
final
List<
Chien>
result =
ordering.greatestOf
(
chiens, nb);
// Assert
Assert.assertEquals
(
nb, result.size
(
));
Assert.assertEquals
(
maxExpected1, result.get
(
0
).getName
(
));
Assert.assertEquals
(
maxExpected2, result.get
(
1
).getName
(
));
}
Pour finir avec « Ordering », on retiendra :
- la composition ;
- renverser l'ordre ;
- gérer les valeurs nulles. Pour rappel, par défaut les implémentations de Comparable/Comparator ne sont pas tenues de gérer la valeur nulle ;
- ordonner selon le résultat d'une transformation, ce qui permet par exemple de trier sur une (ou plusieurs) propriété(s) ;
- trier sur le « toString » ;
- plein de méthodes utilitaires : min, max, greatest, least, is*Sorted.
Quel est le gain de l'objet Ordering par rapport à un bête « Comparable » pour un tri « classique » pour lequel on peut passer par le classique « Collections.sort » ? Il n'y a pas vraiment d'avantage… Si la classe implémente « Comparable », alors un appel à « Collections.sort(maListe) » est tout à fait adapté. Si un jour on a besoin de créer un Ordering basé sur ce « Comparable », il suffit de faire « Ordering.natural().immutableSortedCopy() » et il utilisera naturellement la méthode « compare » de la classe.
Le seul avantage est d'éviter de polluer le code métier avec de la logique de comparaison. En général, on aime que le code de comparaison soit séparé du code métier à proprement parler, pour ne pas que la classe soit trop grosse, et surtout, parce qu'on a régulièrement besoin de différents types de comparaisons…
Par exemple, pour une classe métier « Chien », on aurait une classe utilitaire « ChienOrderings » qui contiendrait des « factory methods » pour créer des « Ordering » génériques sur le type « Chien ». Ensuite, on peut faire « ChienOrderings.byFirstName().sortedCopy() » ou « ChienOrderings.byAge().max(chiens) ».
II-G. Throwables▲
Guava va vous aider à traiter les exceptions survenues dans vos programmes. Une des premières options que vous propose la bibliothèque est tout simplement de propager les exceptions, soit directement soit sous condition :
try
{
... // ici du code qui lance une exception
}
catch
(
IllegalArgumentException e) {
... // ici traitement standard
}
catch
(
Throwable t) {
Throwables.propagateIfInstanceOf
(
t, NullPointerException.class
); // Propage si t est une NPE
Throwables.propagateIfInstanceOf
(
t, IOException.class
); // Propage si t est une IOE
throw
Throwables.propagate
(
t); // Propage tel quel...
}
Il y a tout de même une petite subtilité dans l'utilisation de la méthode « propagate(..) », car ça ne peut propager l'exception que si elle est runtime ou si c'est une erreur. Si ce n'est pas le cas, Guava l'encapsule dans une « RuntimeException ». Et comme le type de retour est de type « RuntimeException » (en plus de propager l'exception), on peut l'associer au mot-clé « throw » pour des raisons de compilation.
Une des raisons qui vont vous encourager à utiliser la méthode « propagate(..) » pourrait être que vous travaillez avec Java 5-6 et que vous n'avez donc pas encore accès au « multicatch » apparu avec Java 7 :
try
{
... // ici du code qui lance une exception
}
catch
(
IllegalArgumentException e) {
... // ici traitement standard
}
catch
(
NullPointerException |
IOException e) {
... // propager de facon commune aux NPE et IOE...
}
catch
(
Throwable t) {
... // sinon
}
Accessoirement, ça permet aussi de convertir un « Throwable » en « Exception » et d'assurer la compilation et la conversion en bonus :
public
void
foo
(
) throws
Exception {
try
{
... // ici du code qui lance une exception
}
catch
(
Throwable t) {
Throwables.propagateIfInstanceOf
(
t, Exception.class
); // Propage une Exception
throw
Throwables.propagate
(
t); // Propage tel quel ou RuntimeException donc Exception...
}
III. Conclusion▲
Maintenant que vous avez découvert tous les petits utilitaires de Guava, vous n'allez plus pouvoir vous en passer. N'hésitez pas à consulter les autres épisodes de cette série pour découvrir les fonctionnalités fantastiques de la bibliothèque.
Vos retours nous aident à améliorer nos publications. N'hésitez donc pas à commenter cet article sur le forum : 15 commentaires
IV. Remerciements▲
D'abord j'adresse mes remerciements à l'équipe Guava, chez Google, pour avoir développé une bibliothèque aussi utile et pour la maintenir. Je n'oublie pas tous les contributeurs qui participent notamment sur le forum Guava.
Plus spécifiquement en ce qui concerne cet article, je tiens à remercier l'équipe de Developpez.com et plus particulièrement Bernard Le Roux, Ricky81, Mickael Baron, Yann Caron, Logan, et TODO.
V. Annexes▲
V-A. Liens▲
Guava : https://code.google.com/p/guava-libraries/
Article « Simplifier le code de vos beans Java à l'aide de Commons Lang, Guava et Lombok » :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/simplifier-code-guava-lombok/
Blog sur Guava : https://blog.developpez.com/guava/
V-B. Liens personnels▲
Retrouvez ma page et mes autres articles sur Developpez.com à l'adresse
https://thierry-leriche-dessirier.developpez.com/#page_articlesTutoriels
Suivez-moi sur Twitter : @thierryleriche (https://twitter.com/thierryleriche)@thierryleriche