Guava par l'exemple (1/3) : les classes utilitaires
Guava est une librairie Java éditée par Google, qui fournit de nombreuses fonctionnalités qui auraient dû se trouver dans le JDK.
Cette série d’articles n’a pas pour vocation de détailler toutes les classes de la librairie, mais d’en présenter une sélection facilement utilisables sur vos projets.
Chaque classe présentée est assortie d’un exemple sous la forme d’un test unitaire mettant en évidence son fonctionnement.
Cet article fait partie d’une série :
Joiner / Splitter
Joiner
et Splitter
sont deux classes utilitaires facilitant la transformation bidirectionnelle entre des ensembles d’objets et des chaînes de caractères.
Joiner
Joiner
prend un ensemble d’objets (Collection
, Iterable
, varargs…) et les joint grâce à un séparateur. Cette fonction est très utile pour générer du CSV par exemple (val1;val2;val3
…)
L’API de type builder permet de spécifier le caractère de séparation, et la façon de gérer les valeurs nulles : les ignorer (skipNulls
) ou les remplacer (useForNull
).
Il existe une variante fonctionnant avec des Maps. Dans ce cas, il faut également préciser le caractère qui sépare les clés des valeurs (k1=v1, k2=v2…)
@Test public void joinVarArgs() { String result = null; Joiner joinerOnString = Joiner.on(", "); result = joinerOnString.join("Hello", "World"); assertEquals("Hello, World", result); Joiner joinerOnChar = Joiner.on(' '); result = joinerOnChar.join("Hello", "World"); assertEquals("Hello World", result); } @Test public void joinIterable() { String result = null; List<String> daltons = Arrays.asList("Joe", "Jack", "William", "Averell"); Joiner joiner = Joiner.on(", "); result = joiner.join(daltons); assertEquals("Joe, Jack, William, Averell", result); } @Test public void joinNulls() { String result = null; List<String> daltons = Arrays.asList("Joe", null, "Jack", null, "William", null, "Averell"); Joiner joinerSkippingNulls = Joiner.on(", ").skipNulls(); result = joinerSkippingNulls.join(daltons); assertEquals("Joe, Jack, William, Averell", result); Joiner joinerReplacingNulls = Joiner.on(", ").useForNull("<null>"); result = joinerReplacingNulls.join(daltons); assertEquals("Joe, <null>, Jack, <null>, William, <null>, Averell", result); } @Test public void joinMap() { String result = null; Map<String, Integer> daltonsSize = new LinkedHashMap<String, Integer>(); daltonsSize.put("Joe", 1); daltonsSize.put("Jack", 2); daltonsSize.put("William", 3); daltonsSize.put("Averell", 4); Joiner.MapJoiner mapJoiner = Joiner.on(", ").withKeyValueSeparator("="); result = mapJoiner.join(daltonsSize); assertEquals("Joe=1, Jack=2, William=3, Averell=4", result); }
Splitter
Splitter
fait l’inverse de Joiner
: il découpe une chaîne de caractères pour en extraire une collection de valeurs. Il peut être très pratique pour parser des fichiers CSV.
Splitter
peut découper selon un délimiteur fixe (type String
) ou selon un Pattern
(regex). Il peut également être paramétré pour supprimer les blancs autour des valeurs (trimResults
) et/ou pour ignorer les valeurs vides (omitEmptyStrings
).
@Test public void splitByString() { String daltons = "Joe, Jack, William, Averell"; String delimiter = ", "; Splitter splitterOnString = Splitter.on(delimiter); Iterable<String> split = splitterOnString.split(daltons); Iterator<String> iterator = split.iterator(); assertEquals("Joe", iterator.next()); assertEquals("Jack", iterator.next()); assertEquals("William", iterator.next()); assertEquals("Averell", iterator.next()); } @Test public void splitByPattern() { String daltons = "Joe, Jack, William, Averell"; String delimiter = ",\\s+"; Splitter splitterOnString = Splitter.onPattern(delimiter); Iterable<String> split = splitterOnString.split(daltons); Iterator<String> iterator = split.iterator(); assertEquals("Joe", iterator.next()); assertEquals("Jack", iterator.next()); assertEquals("William", iterator.next()); assertEquals("Averell", iterator.next()); } @Test public void splitAndRemoveEmptyResults() { String daltons = "Joe, , Jack, , William, , Averell"; String delimiter = ", "; Splitter splitterOnString = Splitter.on(delimiter).trimResults().omitEmptyStrings(); Iterable<String> split = splitterOnString.split(daltons); Iterator<String> iterator = split.iterator(); assertEquals("Joe", iterator.next()); assertEquals("Jack", iterator.next()); assertEquals("William", iterator.next()); assertEquals("Averell", iterator.next()); }
Objects
Objects
contient deux méthodes utiles au quotidien, et qui gèrent correctement les null
:
equal()
qui permet de tester l’égalité de deux instances ;
toStringHelper
, qui permet d’implémenter toString
facilement, grâce à une API fluide.
private static class Pojo { private final String aString; private final int anInt; private final Object anObject; private Pojo(String aString, int anInt, Object anObject) { this.aString = aString; this.anInt = anInt; this.anObject = anObject; } @Override public String toString() { return Objects.toStringHelper(this) .add("aString", aString) .add("anInt", anInt) .add("anObject", anObject) .toString(); } } @Test public void equal() { Pojo pojo = new Pojo("foo", 42, null); assertTrue(Objects.equal(null, null)); assertFalse(Objects.equal(pojo, null)); assertFalse(Objects.equal(null, pojo)); assertTrue(Objects.equal(pojo, pojo)); assertFalse(Objects.equal(pojo, new Object())); } @Test public void toStringHelper() { assertEquals("Pojo{aString=foo, anInt=42, anObject=null}", new Pojo("foo", 42, null).toString()); assertEquals("Pojo{aString=foo, anInt=42, anObject=bar}", new Pojo("foo", 42, "bar").toString()); }
Preconditions
La classe Preconditions
propose deux méthodes facilitant la vérification des paramètres passés en entrée des méthodes.
checkNotNull
vérifie la non-nullité du paramètre, et lance un NullPointerException
si besoin.
checkArgument
vérifie la validité du paramètre grâce à une expression de validation, et lance un IllegalArgumentException
si besoin.
Les deux méthodes peuvent également prendre en paramètre une chaîne de caractères (simple ou à formater), qui sera utilisée comme message si une exception est lancée.
Notes à propos du débat IllegalArgumentException
vs NullPointerException
: certaines personnes n’aiment pas lancer NullPointerException “manuellement” et considèrent que c’est une mauvaise pratique. Pourtant, tout le monde s’accorde sur le fait qu’il faut toujours lancer l’exception la plus précise et décrivant le mieux le cas d’erreur rencontré. Dans le cas où l’on a fourni par mégarde une référence nulle à votre méthode, c’est bien le NPE qui correspond le mieux, et qui doit donc être lancé. L’intégralité du code du JDK est basé sur cette règle, et le code de Google également. Alors pourquoi pas le vôtre ?
private void methodWithNotNullParam(String param) { // Preconditions.checkNotNull(param); // Preconditions.checkNotNull(param, "Parameter 'param' must not be null"); Preconditions.checkNotNull(param, "Parameter '%s' must not be null", "param"); } private void methodWithValidParam(String param) { // Preconditions.checkArgument(param.trim().length() > 0); // Preconditions.checkArgument(param.trim().length() > 0, "Parameter 'param' must not be empty"); Preconditions.checkArgument(param.trim().length() > 0, "Parameter '%s' must not be empty", "param"); } @Test public void testMethodWithNotNullParam() { try { methodWithNotNullParam(null); fail("Target method should have thrown an exception"); } catch (NullPointerException npe) { assertEquals("Parameter 'param' must not be null", npe.getMessage()); } } @Test public void testMethodWithValidParam() { try { methodWithValidParam(""); fail("Target method should have thrown an exception"); } catch (IllegalArgumentException iae) { assertEquals("Parameter 'param' must not be empty", iae.getMessage()); } }
Strings
La classe utilitaire Strings
fournit une poignée de méthodes qui manquent dans la classe String
: – pour la gestion des chaînes nulles ou vides : emptyToNull
, nullToEmpty
, isNullOrEmpty
– pour ajouter du padding : padStart
et padEnd
– pour répéter une chaîne : repeat
@Test public void nullString() { String nullString = null; String emptyString = ""; String someString = "Hello World"; assertEquals(null, Strings.emptyToNull(nullString)); assertEquals(null, Strings.emptyToNull(emptyString)); assertEquals(someString, Strings.emptyToNull(someString)); assertEquals("", Strings.nullToEmpty(nullString)); assertEquals("", Strings.nullToEmpty(emptyString)); assertEquals(someString, Strings.nullToEmpty(someString)); assertTrue(Strings.isNullOrEmpty(nullString)); assertTrue(Strings.isNullOrEmpty(emptyString)); assertFalse(Strings.isNullOrEmpty(someString)); } @Test public void padding() { String greeting = "Hello World"; char padding = '.'; assertEquals(greeting, Strings.padStart(greeting, -1, padding)); assertEquals(greeting, Strings.padStart(greeting, 0, padding)); assertEquals("...."+greeting, Strings.padStart(greeting, 15, padding)); assertEquals(greeting, Strings.padEnd(greeting, -1, padding)); assertEquals(greeting, Strings.padEnd(greeting, 0, padding)); assertEquals(greeting+"....", Strings.padEnd(greeting, 15, padding)); } @Test public void repeat() { String laugh = "ha"; assertEquals("", Strings.repeat(laugh, 0)); assertEquals("ha", Strings.repeat(laugh, 1)); assertEquals("hahaha", Strings.repeat(laugh, 3)); }
Conclusion
Les classes utilitaires de Guava fournissent des méthodes qui mériteraient de figurer dans le JDK, et qui sont actuellement réimplémentées “à la main” dans la majorité des projets.
En utilisant Guava, vous bénéficiez d’un code robuste concentrant tout le savoir-faire de Google, et vous pouvez vous concentrer sur l’écriture de code à valeur ajoutée.
Dans le prochain billet, je vous présenterai les classes et concepts liés à la gestion des Collections. Stay tuned !