Tutoriel Guava : tout pour vos strings et vos primitifs

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 sixième article sur Guava, nous allons découvrir comment se simplifier la vie avec les strings et les primitifs. 2 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

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

I-A. Versions des logiciels et bibliothèques utilisés

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

7 janvier 2014 : création

II. Tout pour vos strings

II-A. Splitter et Joiner

Il est fréquent de travailler avec des listes et d'en vouloir une représentation sous forme de String. Il est aussi courant d'avoir l'inverse, c'est-à-dire une String, et de s'en servir pour créer une liste d'objets. Ces deux fonctions sont relativement faciles à coder, mais il y a toujours des points de détails casse-pieds auxquels on ne pense pas.

II-A-1. Joiner

Un Joiner fournit une représentation d'une liste sous forme de String. En Java classique, c'est relativement simple à programmer sous réserve de faire attention aux détails. Je vous laisse lire des billets de blog (cf. ci-dessous) pour avoir des exemples en Java.

A l'aide de Guava, ça va être simple et rapide. Partons d'une liste avec les noms de nos chiens :

Noms des chiens
Sélectionnez

final List<String> names = Lists.newArrayList("Milou", "Asterix", "Lassie");

Pour utiliser le « Joiner », on va indiquer un délimiteur qui sera utilisé pour séparer les valeurs, et on va simplement préciser la liste cible :

Joiner
Sélectionnez

final List<String> names = Lists.newArrayList("Milou", "Asterix", "Lassie");
final String result = Joiner.on(", ").join(names);

Et voici un petit test pour bien comprendre comment ça fonctionne :

Test du Joiner
Sélectionnez

@Test
public void testJoiner() {

    // Arrange
    final List<String> names = Lists.newArrayList("Milou", "Asterix", "Lassie");
    final String expected = "Milou, Asterix, Lassie";

    // Act
    final String result = Joiner.on(", ").join(names);

    // Assert
    Assert.assertEquals(expected, result);
}

Il y a un cas auquel on ne pense généralement pas, c'est lorsqu'un élément « null » s'est glissé dans la liste. Le Joiner va alors lancer une Exception :

Exception sur l'élément null
Sélectionnez

@Test(expected = NullPointerException.class)
public void testJoinerAvecNull() {

    // Arrange
    final List<String> names = Lists.newArrayList("Milou", "Asterix", null, "Lassie");
    final String expected = "Milou, Asterix, Lassie";

    // Act
    final String result = Joiner.on(", ").join(names);

}

Pour traiter les « null », il suffit de les sauter à l'aide de la méthode « skipNulls() » :

skipNulls()
Sélectionnez

@Test
public void testJoinerSkipNull() {

    // Arrange
    final List<String> names = Lists.newArrayList("Milou", "Asterix", null, "Lassie");
    final String expected = "Milou, Asterix, Lassie";

    // Act
    final String result = Joiner.on(", ").skipNulls().join(names);

    // Assert
    Assert.assertEquals(expected, result);
}

On peut aussi se dire qu'il y avait une bonne raison pour que l'élément « null » soit arrivé dans la liste. Et bien entendu, on ne veut pas perdre l'information. Dans ce cas, on peut utiliser la méthode « useForNull() » à la place de « skipNulls() » :

useForNull()
Sélectionnez

@Test
public void testJoinerUseForNull() {

    // Arrange
    final List<String> names = Lists.newArrayList("Milou", "Asterix", null, "Lassie");
    final String expected = "Milou, Asterix, noname, Lassie";

    // Act
    final String result = Joiner.on(", ").useForNull("noname").join(names);

    // Assert
    Assert.assertEquals(expected, result);
}

Bien entendu, il est aussi possible de faire la même chose avec des objets, c'est-à-dire pas seulement avec de simples String. C'est juste un peu plus complexe :

Joiner sur Chien
Sélectionnez

@Test
public void testJoinerChien() {

    // Arrange
    final List<Chien> chiens = Lists.newArrayList( 
            new Chien("Milou"), 
            new Chien("Asterix"), 
            new Chien("Lassie"));

    final String expected = "Milou, Asterix, Lassie";

    // Act
    final String result = Joiner.on(", ") 
            .appendTo(new StringBuilder(),
                    Iterables.transform(chiens, 
                            new Function<Chien, String>() {
                                @Override
                                public String apply(Chien chien) {
                                    return chien.getName();
                                }
                            })) 
            .toString();

    // Assert
    Assert.assertEquals(expected, result);
}

A lire, un billet de blog intitulé « Joiner pour assembler des items ».

A lire, un billet de blog intitulé « Représentation d'une liste en String ».

II-A-2. Splitter

Le Splitter est exactement l'inverse du Joiner ; à partir d'une chaîne de caractères, on souhaite construire une liste d'objets.

Prenons un exemple pour illustrer cela. Disons que nous avons un String contenant des noms de chiens :

Des chiens
Sélectionnez

final String s = "Milou, Pluto, Lassie, Volt";

Nous voulons donc une liste de String avec les noms de ces quatre chiens. Le Splitter va s'utiliser exactement comme le Joiner :

Splitter simple
Sélectionnez

final Iterable<String> noms 
    = Splitter.on(",").split(s);

Ça renvoie donc un Iterable. On a déjà expliqué ce type plus tôt et nous allons nous en contenter pour l'instant. Voyons ce que le test complet donne :

Test complet
Sélectionnez

@Test
public void testSplitter() {
    // Arrange
    final String s = "Milou, Pluto, Lassie, Volt";
    final int nbChien = 4;

    // Act
    final Iterable<String> iterable 
        = Splitter.on(",").split(s);
    final List<String> noms = Lists.newArrayList(iterable);

    // Assert
    Assert.assertEquals(nbChien, noms.size());
}

Si on regarde les valeurs, on se rend compte qu'il y a des espaces en trop :

sysout
Sélectionnez

for (String nom : noms) {
    System.out.println("*" + nom + "*");
}
Console
Sélectionnez

*Milou*
* Pluto*
* Lassie*
* Volt*

Pour avoir les bonnes valeurs, il suffit d'enchaîner un appel à « trimResult() » :

trimResult
Sélectionnez

final Iterable<String> iterable 
    = Splitter.on(",")
        .trimResults()
        .split(s);

Allons plus loin. Il arrive souvent, quelle qu'en soit la raison, que la chaîne contienne des valeurs vides :

Avec une valeur vide
Sélectionnez

final String s = "Milou, Pluto, Lassie, , Volt";

L'appel à « trimResult() » ne va pas être suffisant pour filtrer cette valeur. Pour cela, on peut ajouter à « omitEmptyString() » :

Omit empty string
Sélectionnez

@Test
public void testSplitterSkip() {
    // Arrange
    final String s = "Milou, Pluto, Lassie, , Volt";
    final int nbChien = 4;

    // Act
    final Iterable<String> iterable 
        = Splitter.on(",")
            .trimResults()
            .omitEmptyStrings()
            .split(s);
    final List<String> noms = Lists.newArrayList(iterable);

    // Assert
    Assert.assertEquals(nbChien, noms.size());
}

C'est relativement pratique, mais on peut encore s'amuser avec. Disons par exemple qu'on ne souhaite que les N premiers items, on pourra indiquer une limite à l'aide en chaînant « limit() » :

Limitation
Sélectionnez

@Test
public void testSplitterLimit() {
    // Arrange
    final String s = "Milou, Pluto, Lassie, , Volt";
    final int nbChien = 2;

    // Act
    final Iterable<String> iterable 
        = Splitter.on(",")
            .limit(2)
            .trimResults()
            .omitEmptyStrings()
            .split(s);
    final List<String> noms = Lists.newArrayList(iterable);

    // Assert
    Assert.assertEquals(nbChien, noms.size());
}

Mais attention, même s'il est vrai que ça limite le nombre d'éléments trouvés, il faut bien comprendre que cela va se faire sur les N premiers éléments. Plus concrètement, ça va parser/séparer la liste jusqu'à avoir trouvé les N-1 premiers séparateurs. Ça ne va donc pas séparer/splitter l'intégralité de la liste mais ça s'arrêtera dès que N éléments auront été trouvés. Une sortie console sera certainement plus parlante :

Sortie console
Sélectionnez

*Milou*
*Pluto, Lassie, , Volt*

Il y a donc un élément contenant « Milou » et un autre contenant « Pluto, Lassie, , Volt ».

A lire, un billet de blog intitulé « Splitter pour séparer des items ».

Il est aussi possible de « splitter » des String correspondant à des couples clé-valeur directement, ce qui peut être assez pratique :

Séparation clé-valeur
Sélectionnez

@Test
public void testWithKeyValueSeparator() {
    // Arrange
    final String s = "Milou:blanc,Garfield:orange,Scoobydoo:marron,felix:noir";
    final String expectedCouleurGarfield = "orange";

    // Act
    final Map<String, String> map = Splitter.on(",") //
            .withKeyValueSeparator(":") //
            .split(s);

    // Assert
    Assert.assertEquals(expectedCouleurGarfield, map.get("Garfield"));
}

II-B. CharMatcher / CaseFormat / Charset

Dans nos programmes, on passe beaucoup de temps à manipuler des strings : suppression des espaces inutiles, mise en majuscule, remplacement de caractère, etc. Et à chaque fois, c'est la galère pour développer proprement ce type de fonctionnalité ; il faut découper la string, employer des regex douteuses, concaténer, etc. Heureusement, Guava va encore nous aider.

En fait, on peut généralement décomposer le sujet en deux. Dans un premier temps, il faut trouver les caractères qui nous intéressent. Dans un second temps, il faut les traiter. Un exemple sera probablement plus parlant. Disons que nous souhaitons supprimer tous les chiffres d'une string :

Suppresion des strings
Sélectionnez

@Test
public void testSuppressionChiffres() {
    // Arrange
    final String original = "ab12cd34";
    final String expected = "abcd";

    // Act
    final CharMatcher matcher = CharMatcher.DIGIT; // -> match les chiffres
    final String result = matcher.removeFrom(original); // -> "abcd"

    // Assert
    Assert.assertEquals(expected, result);
}

Guava offre une douzaine de matchers prédéfinis :

  • ANY, qui correspond à tous les caractères ;
  • NONE, qui ne correspond à rien (sera généralement utilisé par la négative) ;
  • WHITESPACE, qui correspond aux espaces blancs Unicode* ;
  • BREAKING_WHITESPACE, qui correspond aux espaces blancs séparant des mots ;
  • INVISIBLE, qui correspond aux caractères invisibles tels que les retours à la ligne ;
  • DIGIT, que l'on vient de voir et qui correspond donc aux chiffres Unicode ;
  • JAVA_LETTER, qui correspond aux 26 lettres de l'alphabet en minuscule ou en majuscule ;
  • JAVA_DIGIT, qui correspond aux chiffres de 0 à 9 ;
  • JAVA_LETTER_OR_DIGIT, qui correspond aux lettres ou chiffres (c'est l'union des deux matchers précédents) ;
  • JAVA_ISO_CONTROL, qui correspond aux caractères « iso » de contrôle, c'est-à-dire s'il est dans l'intervalle de valeurs comprises entre '\u0000' et '\u001F', ou entre '\u007F' et '\u009F' ;
  • JAVA_LOWER_CASE, qui correspond aux lettres en minuscule ;
  • JAVA_UPPER_CASE, qui correspond aux lettres en majuscule ;
  • ASCII, qui correspond aux caractères Ascii ;
  • SINGLE_WIDTH, qui correspond aux caractères de taille simple (par opposition à double) et que vous n'utiliserez probablement jamais ;-)

La définition des espaces blancs n'est pas la même d'une bibliothèque à l'autre. Dans le cadre de Guava, il faut se référer à la définition Unicode des espaces blancs.

Bien entendu, il est possible de combiner (ou, et, négation) plusieurs matchers pour réaliser une opération. A titre d'illustration, disons que nous voulons remplacer les chiffres et les majuscules par des étoiles :

Remplacement
Sélectionnez

@Test
public void testRemplacementChiffreEtMajusculeParEtoile() {
    // Arrange
    final String original = "aB12cD34";
    final String expected = "a***c***";

    // Act
    final CharMatcher matcher1 = CharMatcher.DIGIT;
    final CharMatcher matcher2 = CharMatcher.JAVA_UPPER_CASE;
    final String result = matcher1.or(matcher2).replaceFrom(original, "*");

    // Assert
    Assert.assertEquals(expected, result);
}

Les matchers donnent donc accès à un ensemble d'opérations standards dont :

  • collapseFrom, qui remplace des groupes de caractères par une seule séquence ;
  • countIn ;
  • indexIn ;
  • lastIndexIn ;
  • matches ;
  • matchesAllOf ;
  • matchesAnyOf ;
  • matchesNoneOf ;
  • removeFrom ;
  • replaceFrom ;
  • retainFrom, qui supprime tous les caractères qui ne correspondent pas ;
  • trimAndCollapseFrom ;
  • trimFrom ;
  • trimLeadingFrom ;
  • trimTrailingFrom.
collapse
Sélectionnez

@Test
public void testCollapse() {
    // Arrange
    final String original = "ab12cd34";
    final String expected = "ab*cd*";

    // Act
    final CharMatcher matcher = CharMatcher.DIGIT;
    final String result = matcher.collapseFrom(original, '*');

    // Assert
    Assert.assertEquals(expected, result);
}

On peut aussi travailler plus spécifiquement sur la casse :

Format d'une constante vers Camel
Sélectionnez

@Test
public void testConstanteToCamel() {
    // Arrange
    final String original = "un_gentil_chien_jaune";
    final String expected = "unGentilChienJaune";

    // Act
    final CaseFormat startFormat = CaseFormat.LOWER_UNDERSCORE;
    final CaseFormat toFormat = CaseFormat.LOWER_CAMEL;
    final String result = startFormat.to(toFormat, original);

    // Assert
    Assert.assertEquals(expected, result);
}

Les formats prédéfinis sont :

  • LOWER_HYPHEN, utilisant un tiret (touche 6 sur le clavier) ;
  • LOWER_UNDERSCORE, utilisant un « underscore » (touche 8 sur le clavier) ;
  • UPPER_UNDERSCORE ;
  • LOWER_CAMEL ;
  • UPPER_CAMEL.

Enfin, et pour en finir avec les strings, on est tout le temps obligé de les convertir en fonction de l'encodage des fichiers d'entrée, notamment pour passer d'ISO à UTF-8. Et je suis certain que vous vous êtes trompés sur le nom du format, car il faut les définir en String aussi. Et une erreur est vite arrivée :

Erreur sur le nom du format
Sélectionnez

final String prenom = "Milou";

try {
    bytes = prenom.getBytes("UTF-8");
} catch(UnsupportedEncodingException e) {
    ...
}

Java oblige à encadrer l'instruction par un try-catch car on peut se tromper sur le nom de format passé sous forme de string. A la place, il vaut mieux passer par la méthode qui prend directement un CharSet en paramètre (qui ne nécessite pas de try). Guava nous aide dans la mesure où il nous fournit une liste de Charsets déjà prêts :

Avec un Charset déjà prêt
Sélectionnez

bytes = prenom.getBytes(Charsets.UTF_8);

Guava propose les formats d'encodage classiques : Iso 8859-1, Ascii, UTF-8, UTF-16 et UTF-16LE.

III. Et pour vos primitifs

Avant d'utiliser une solution Guava, vérifiez que les classes wrapper du JDK ne répondent pas déjà à votre besoin.

En ce qui concerne Guava, il y a des classes utilitaires adaptées à chaque type primitif :

  • byte : Bytes, SignedBytes et UnsignedBytes ;
  • short : Shorts ;
  • int : Ints, UnsignedInteger et UnsignedInts ;
  • long : Longs, UnsignedLong et UnsignedLongs ;
  • float : Floats ;
  • double : Doubles ;
  • char : Chars ;
  • boolean : Booleans.

III-A. Tableaux et listes

On a déjà vu qu'on peut facilement créer une liste :

Création d'une liste
Sélectionnez

List<Double> liste = Doubles.asList(1.1, 1.2, 1.3, 1.4, 1.5);

En partant d'une liste, on peut facilement en faire un tableau :

En tableau
Sélectionnez

double[] tab = Doubles.toArray(liste);

Ça n'a l'air de rien, mais c'est pratique. De même, c'est sympa de pouvoir concaténer des tableaux :

Concat
Sélectionnez

@Test
public void testConcat() {
    // Arrange
    final double[] tab1 = { 1.1, 1.2, 1.3, 1.4, 1.5 };
    final double[] tab2 = { 1.6, 1.7, 1.8, 1.9 };
    final double[] tab3 = { 2.0, 2.1 };
    final double[] expected = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1 };

    // Act
    double[] concat = Doubles.concat(tab1, tab2, tab3);

    // Assert
    Assert.assertEquals(expected.length, concat.length);
    for (int i = 0; i < expected.length; i++) {
        Assert.assertEquals(expected[i], concat[i], 0.000001);
    }
}

Ce n'est jamais vraiment ni pratique ni facile de vérifier qu'une valeur fait partie d'un tableau, en particulier avec les flottants. Heureusement, Guava dispose d'une telle fonction :

Contains
Sélectionnez

@Test
public void testContains() {
    // Arrange
    final double[] tab = { 1.1, 1.2, 1.3, 1.4, 1.5 };
    final double valeur = 1.4;

    // Act
    final boolean isIn = Doubles.contains(tab, valeur);

    // Assert
    Assert.assertTrue(isIn);
}

Et quitte à savoir que l'élément est bien dans le tableau, on voudrait connaitre sa position :

Index
Sélectionnez

@Test
public void testIndex() {
    // Arrange
    final double[] tab = { 1.1, 1.2, 1.3, 1.4, 1.5 };
    final double valeur = 1.4;
    final int expected = 3;

    // Act
    final int position = Doubles.indexOf(tab, valeur);

    // Assert
    Assert.assertEquals(expected, position);
}

Ça renvoie l'index du premier élément trouvé.

Pour connaitre la dernière position, c'est tout aussi facile :

Last index
Sélectionnez

@Test
public void testLastIndex() {
    // Arrange
    final double[] tab = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.4, 1.6 };
    final double valeur = 1.4;
    final int expected = 5;

    // Act
    final int position = Doubles.lastIndexOf(tab, valeur);

    // Assert
    Assert.assertEquals(expected, position);
}

Pendant qu'on y est, on peut chercher l'élément le plus petit ou le plus grand, sachant que le tableau n'est pas forcément trié :

Min et max
Sélectionnez

@Test
public void testLastMinMax() {
    // Arrange
    final double[] tab = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.4, 1.6 };

    final double expectedMin = 1.1;
    final double expectedMax = 1.6;

    // Act
    final double min = Doubles.min(tab);
    final double max = Doubles.max(tab);

    // Assert
    Assert.assertEquals(expectedMin, min, 0.000001);
    Assert.assertEquals(expectedMax, max, 0.000001);
}

On a vu, plus haut, qu'on peut utiliser un Joiner pour fabriquer une représentation d'un iterable sous forme de String. Et bien c'est encore plus simple avec des tableaux de primitifs :

Join
Sélectionnez

@Test
public void testJoin() {
    // Arrange
    final double[] tab = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.4, 1.6 };
    final String expected = "1.1, 1.2, 1.3, 1.4, 1.5, 1.4, 1.6";

    // Act
    final String result = Doubles.join(", ", tab);

    // Assert
    Assert.assertEquals(expected, result);
}

III-B. Méthodes générales

Une des choses les plus simples qu'on peut faire sur des primitifs, c'est tout simplement de les comparer :

Comparaison
Sélectionnez

@Test
public void testCompare() {
    // Arrange
    final double a = 1.4;
    final double b = 1.2;

    // Act
    final int comp = Doubles.compare(a, b);

    // Assert
    Assert.assertTrue(comp > 0);
}

Ce qu'on fait souvent, et on en a déjà parlé plus haut, ce sont des conversions. Or il est rare qu'on soit satisfait d'un "cast" qui perdrait de l'information. C'est pourtant ce qu'offre Java lorsqu'on convertit un type vers un type plus restreint, et de manière silencieuse. Guava a, quant à lui, la gentillesse de nous prévenir :

Cast vérifié
Sélectionnez

@Test
public void testCastOk() {
    // Arrange
    final int nb = 123;
    final short expected = 123;

    // Act
    final short result = Shorts.checkedCast(nb);

    // Assert
    Assert.assertEquals(expected, result);
}

@Test(expected = IllegalArgumentException.class)
public void testCastNok() {
    // Arrange
    final int nb = 123456789;

    // Act
    final short result = Shorts.checkedCast(nb);
}

Si on préfère malgré tout que la conversion se déroule sans warning, on peut demander que ce soit "arrondi" à la valeur la plus proche, qui sera en toute logique un des maximums du type cible :

Saturate
Sélectionnez

@Test
public void testSaturate() {
    // Arrange
    final int nb = 123456789;
    final short expected = Short.MAX_VALUE;

    // Act
    final short result = Shorts.saturatedCast(nb);

    // Assert
    Assert.assertEquals(expected, result);
}

IV. Conclusions

Maintenant que vous savez que Guava peut vous aider à manipuler vos strings et vos primitifs, vous n'aurez plus d'excuse pour en bâcler les traitements. La String est mort, vive la String. 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 : 2 commentaires Donner une note à l'article (5)

V. 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, Alain Bernard, Marc, Régis Pouiller, et Philippe Duval.

VI. Annexes

VI-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" :
http://thierry-leriche-dessirier.developpez.com/tutoriels/java/simplifier-code-guava-lombok/

Blog sur Guava : http://blog.developpez.com/guava/

VI-B. Liens personnels

Retrouvez ma page et mes autres articles sur Developpez.com à l'adresse
http://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 et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.