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 :
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 :
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
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 :
@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() » :
@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() » :
@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 :
@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 :
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 :
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
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 :
for (String nom : noms) {
System.out.println("*" + nom + "*");
}*Milou*
* Pluto*
* Lassie*
* Volt*Pour avoir les bonnes valeurs, il suffit d'enchaîner un appel à « 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 :
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() » :
@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() » :
@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 :
*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 :
@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 :
@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 :
@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.
@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 :
@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 :
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 :
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 :
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 :
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 :
@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 :
@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 :
@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 :
@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é :
@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 :
@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 :
@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 :
@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 :
@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 ![]()
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_articlesTutoriels
Suivez-moi sur Twitter : @thierryleriche(https://twitter.com/thierryleriche)@thierryleriche





