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

Tutoriel d'introduction aux utilitaires de Guava

Thierry

Guava est une bibliothèque, de chez Google, proposant de nombreux outils pour améliorer les codes des programmes Java. Elle permet, entre autres, de manipuler les collections, de jouer efficacement avec les immutables, d'éviter la gestion des beans nuls, de s'essayer à la programmation fonctionnelle, de cacher les objets, de les simplifier, et bien d'autres choses…

Dans ce quatrième article sur Guava, nous allons découvrir tous les petits utilitaires de Guava, bien utiles au quotidien. 15 commentaires Donner une note à l´article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Cet article est le quatrième d'une série consacrée à la bibliothèque Guava :

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 :

Méthode equals(..) d'Eclipse
Sélectionnez
@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;
}
Méthode hashCode(..) d'Eclipse
Sélectionnez
@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 :

Méthode equals(..) d'Eclipse
Sélectionnez
@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;
}
Méthode hashCode(..) d'Eclipse
Sélectionnez
@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 :

Méthode equals(..) avec Guava
Sélectionnez
@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() » :

Méthode hashCode() avec Guava
Sélectionnez
@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.

Méthode toString() d'Eclipse
Sélectionnez
@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 :

Méthode toString() avec Guava
Sélectionnez
@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 :

La méthode comprateTo(..) avec Guava
Sélectionnez
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 :

Chrono system
Sélectionnez
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 :

Chrono system
Sélectionnez
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 :

StopWatch
Sélectionnez
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 :

StopWatch
Sélectionnez
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é :

StopWatch précis
Sélectionnez
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 :

QuickStopwatch.java
Sélectionnez
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 :

QuickStopWatch
Sélectionnez
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 ?

Comparaison
Sélectionnez
String s = ...
if( s.equals("abcd") ) {
    ...
}

Bien entendu, il faut tester la nullité pour éviter les exceptions :

Comparaison
Sélectionnez
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 :

Ni nulle ni vide
Sélectionnez
String s = ...
if( s != null && !s.equals("") ) {
    ...
}

Ou même mieux :

Ni null ni vide
Sélectionnez
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 null ou vide
Sélectionnez
@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 null to empty
Sélectionnez
@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 null to empty
Sélectionnez
@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 :

Toujours null to empty
Sélectionnez
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 :

Empty to null
Sélectionnez
@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 :

Wrapper
Sélectionnez
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 :

Wrapper
Sélectionnez
@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(..) » :

Wrapper from nullable
Sélectionnez
@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é :

Wrapper absent
Sélectionnez
@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.

Wrapper
Sélectionnez
@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() » :

Wrapper
Sélectionnez
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 :

Wrapper vide
Sélectionnez
@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.

Test de nullité
Sélectionnez
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 :

OrNull
Sélectionnez
@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 :

Or avec remplacement
Sélectionnez
@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 :

Constructeur de Chien avec des préconditions
Sélectionnez
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 :

Constructeur de Chien avec des préconditions Guava
Sélectionnez
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 :

Affectation et précondition en une ligne
Sélectionnez
public void foo(String field) {
    this.field = checkNotNull( field, "Message d"erreur si null.");
}

Au lieu de deux lignes :

Affectation et précondition en une ligne
Sélectionnez
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 :

Des valeurs dans l'erreur
Sélectionnez
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 :

Ordering
Sélectionnez
@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 :

Ordering natural
Sélectionnez
@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 :

Tri sur le nom
Sélectionnez
@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() » :

Ordering avec nullsLast()
Sélectionnez
@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 :

Ordering renversé
Sélectionnez
@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 :

Déjà triée ?
Sélectionnez
@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 :

Min/max
Sélectionnez
@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(..) » :

Les deux plus grands
Sélectionnez
@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 :

Propagation d'exception
Sélectionnez
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 :

Multicatch
Sélectionnez
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 :

Conversion d'exception
Sélectionnez
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 Donner une note à l´article (5)

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

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

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