Tutoriel Guava : tout pour vos strings et vos primitifs

Thierry Leriche-Dessirier

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

- Titre : Tutoriel Guava : tout pour vos strings et vos primitifs

- Auteur : Thierry Leriche-Dessirier

- Parution : 21 juillet 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 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.

Sommaire

- I. Introduction

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

- I-B. Mises à jour

- II. Tout pour vos strings

- II-A. Splitter et Joiner

- II-A-1. Joiner

- II-A-2. Splitter

- II-B. CharMatcher / CaseFormat / Charset

- III. Et pour vos primitifs

- III-A. Tableaux et listes

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

- IV. Conclusions

- V. Remerciements

- VI. Annexes

- VI-A. Liens

- VI-B. Liens personnels

I - Introduction

Cet article est le sixiè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é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

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

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

@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

@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()

@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()

@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

@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

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

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

@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

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

Console

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

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

trimResult

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

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

@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

@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

*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

@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

@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

@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

@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

@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

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

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

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

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

@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

@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

@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

@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

@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

@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

@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é

@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

@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 : Commentez

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

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

VI-B - Liens personnels

Retrouvez ma page et mes autres articles sur Developpez.com à l'adresse
https://thierry-leriche-dessirier.developpez.com/#page_articles

Suivez-moi sur Twitter : @thierryleriche(https://twitter.com/thierryleriche)