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