Simplifier le code de vos beans Java à l'aide de Commons Lang, Guava et Lombok

Thierry Leriche-Dessirier

Un bean classique, représentant un chien par exemple, peut vite peser des centaines de lignes quand on l'équipe avec les méthodes classiques et indispensables (constructeurs, getters, setters, equals, hashCode, toString, compareTo), même s'il possède peu d'attributs. Dans cet article, nous verrons comment mettre un tel cabot au régime et lui faire une beauté à l'aide de Commons Lang, Guava et Lombok. Nous en profiterons pour comparer ces bibliothèques entre elles et avec le code Java habituel, sans oublier celui qu'Eclipse génère. Commentez

♪

- Titre : Simplifier le code de vos beans Java à l'aide de Commons Lang, Guava et Lombok

- Auteur : Thierry Leriche-Dessirier

- Parution : 27 juin 2012

- Public visé :

- 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 ® 2012 developpez. 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. À propos

- I-B. Avant de commencer

- I-C. Copyright sur les images de chien

- I-D. Présentation dans des JUG

- II. Préparation

- II-A. Télécharger, installer et importer le projet d'exemple

- II-B. Découverte projet d'exemple

- III. Action avec Eclipse

- III-A. Menu source

- III-B. Constructeurs

- III-C. Accesseurs

- III-D. Hash code et Equals

- III-E. To String

- III-F. Bilan des générations à l'aide d'Eclipse

- IV. Action avec Commons Lang

- IV-A. Maven

- IV-B. Equals et hash code

- IV-C. To String

- IV-D. Comparaison compareTo

- IV-E. Bilan de l'utilisation de Commons Lang

- V. Action avec Guava

- V-A. Maven

- V-B. Equals et hash code

- V-C. To String

- V-D. Comparaison de canins

- V-E. Bilan de l'utilisation de Guava

- VI. Action avec Lombok

- VI-A. Maven

- VI-B. Installer Lombok dans Eclipse

- VI-C. Constructeurs

- VI-D. Getters et setters

- VI-D-1. getter lazy

- VI-E. Hash code et Equals

- VI-F. To String

- VI-G. Tout ça à la fois

- VI-H. Bilan de l'utilisation de Lombok

- VII. Conclusion

- VIII. Remerciements

- IX. Annexes

- IX-A. Liens

- IX-B. Liens personnels

- IX-C. Comparatif

- IX-D. Avantages et inconvénients

- IX-E. Annotations Lombok

- IX-E-1. Annotation @Log

- IX-E-2. Annotation @Synchronized

- IX-E-3. Annotation @Cleanup

- IX-E-4. Annotation @Delegate

- IX-E-5. Mot-clé bonus : val

- IX-F. Les fichiers importants en entier

- IX-G. Classes décompilées

I - Introduction

Dans cet article, nous allons nous intéresser aux tâches d'écriture de code dans les beans classiques. Les méthodes qu'on ajoute sont plus ou moins toujours les mêmes (getters, setters, toString, hashCode, etc.) au point que la plupart des développeurs les codent comme un réflexe, le plus souvent à l'aide de quelques clics sous Eclipse. Or c'est souvent du code technique, pas très intéressant, qui pollue le programme et en rend la lecture moins aisée.

Nous allons voir comment des bibliothèques comme Commons Lang, Guava ou Lombok aident à développer du code plus simple.

I-A - À propos

Découvrir une technologie n'est pas chose facile. En aborder plusieurs d'un coup l'est encore moins. Partant de ce constat, cet article a été écrit pour aller à l'essentiel. Les points importants sont présentés dans le corps de l'article et les éléments secondaires sont expliqués en annexe.

I-B - Avant de commencer

Pour écrire ce tutoriel, j'ai utilisé les éléments suivants :

- Java JDK 1.6.0_24-b07 ;

- Eclipse Indigo 3.7 JEE 64b ;

- Maven 3.0.3 ;

- Commons Lang 3.1 ;

- Guava 11.0.1 ;

- Lombok 0.11.0.

I-C - Copyright sur les images de chien

Les images de chien utilisées dans ce tutoriel sont soumises à un copyright : vous ne pouvez pas les copier. Je dispose d'une licence (payante) pour les utiliser, mais pas pour les redistribuer. D'avance, je vous remercie pour votre compréhension.

I-D - Présentation dans des JUG

Cet article m'a été utile pour préparer une présentation au LyonJug, LorraineJug, NormandyJug et BrezthJUG. Les slides de ces présentations sur disponibles online : http://icauda.com/cours.html

II - Préparation

II-A - Télécharger, installer et importer le projet d'exemple

Pour commencer, je vous propose de télécharger le fichier Zip « nice-dog-1.zip » contenant un projet Java-Maven d'exemple qui va nous servir de support pour les fonctionnalités présentées dans la suite de cet article.

Compilez le projet d'exemple et importez-le dans Eclipse (comme expliqué dans le tutoriel « Importer un projet Maven dans Eclipse en 5 minutes ») ou dans l'IDE de votre choix.

Projet dans Eclipse

Pour suivre ce tutoriel, vous pouvez vous contenter de lire les codes proposés ci-dessous (codes complets en annexe) et de faire confiance aux codes et captures d'écran.

II-B - Découverte projet d'exemple

Durée estimée : 15 secondes.

En plus des fichiers de Maven (« pom.xml ») et de licence, le projet ne contient que la classe « Dog » pour représenter naïvement les caractéristiques d'un chien : nom, nom complet, date de naissance, race, inscription au livre des origines (lof), père, mère, couleur et poids. Pour faire bonne mesure, la classe possède également un identifiant dont la valeur peut venir de la base par exemple.

Dog.java

public class Dog {
private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;
}

Dans la suite de ce tutoriel, nous allons ajouter des fonctionnalités classiques au bean « Dog » : constructeurs, accesseurs, hash code, etc.

Chien zen

III - Action avec Eclipse

Traditionnellement, lorsqu'on développe une nouvelle classe avec Eclipse, on écrit d'abord les attributs puis on demande à Eclipse de générer les constructeurs et les accesseurs (getters et setters). Très souvent, on enchaine sur les méthodes classiques (héritées de Object) telles que « toString », « hashCode » et « equals ». Pour de nombreux programmeurs, c'est presque un automatisme. Toutefois, cette procédure est vite laborieuse et produit du code purement technique qui alourdit les sources et pénalise la compréhension.

Cette partie de l'action, à l'aide d'Eclipse, montre ce que font habituellement les développeurs. Dans les chapitres suivants, nous verrons comment simplifier et améliorer ce processus à l'aide de Commons Lang, Guava et Lombok.

III-A - Menu source

Les fonctionnalités de génération de code que nous allons utiliser dans la suite sont accessibles à partir du menu « Source » dans la barre des menus en haut de la fenêtre. Ce menu est également utilisable à partir du clic droit dans le code.

Menu Source

III-B - Constructeurs

Durée estimée : 2 minutes.

Eclipse permet de générer deux types de constructeurs :

- des constructeurs hérités des éventuelles classes parentes (dont « Object »), avec notamment le constructeur par défaut (sans argument) ;

- des constructeurs utilisant les attributs de la classe comme paramètre.

Pour générer un constructeur hérité, il faut passer par le menu « Sources/Generate Constructors from Superclass… ». Dans l'exemple, il n'y a que le constructeur par défaut qui est proposé. Si les classes parentes avaient défini d'autres constructeurs, ils auraient été également proposés.

Génération des constructeurs hérités

Dans la popup, on peut choisir où sera inséré le code généré.

Constructeur hérité des classes parentes

public class DogEclipse {
private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;

public DogEclipse() {
super();
// TODO Auto-generated constructor stub
}
}

Pour générer un constructeur avec argument, il faut passer par le menu « Sources/Generate Constructor using Fields… ». La popup permet de sélectionner les attributs à utiliser et de les ordonner. Pour créer plusieurs constructeurs, avec des paramètres et/ou des ordres différents, il suffit de relancer la fonctionnalité autant de fois que nécessaire.

Dans un premier temps, je génère un constructeur qui utilise tous les attributs de la classe, que je laisse dans l'ordre initial.

Constructeur utilisant tous les attributs

Constructeur utilisant tous les attributs

public class DogEclipse {
...

public DogEclipse(Integer id, String name, String fullName, Date birthday, String race, Boolean lof, Dog father, Dog mother, String color, Double weight) {
super();
this.id = id;
this.name = name;
this.fullName = fullName;
this.birthday = birthday;
this.race = race;
this.lof = lof;
this.father = father;
this.mother = mother;
this.color = color;
this.weight = weight;
}
}

Dans un second temps, je génère un constructeur n'utilisant que les attributs nom, nom complet, date de naissance et race.

Constructeur n'utilisant qu'une partie des attributs

Constructeur n'utilisant qu'une partie des attributs

public class DogEclipse {
...

public DogEclipse(String name, String fullName, Date birthday, String race) {
super();
this.name = name;
this.fullName = fullName;
this.birthday = birthday;
this.race = race;
}
}

On constate que le code généré est relativement simple, et standard.

Classique

III-C - Accesseurs

Durée estimée : 30 secondes.

La génération des accesseurs (getters et setters) est certainement la fonctionnalité de génération de code la plus utilisée d'Eclipse.

Pour générer les getters et setters, il faut passer par le menu « Sources/Generate Getters and Setters… ». La popup permet de sélectionner les attributs pour lesquels on veut générer les accesseurs.

Dans la popup, on peut préciser la visibilité (private, public, etc.) qu'on souhaite utiliser. Eclipse adapte la liste des choix en fonction des attributs.

Génération des accesseurs

Génération des accesseurs

public class DogEclipse {
...

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

...
}

Ici, on ne peut pas faire beaucoup plus simple et efficace.

En tant que développeur, je ne peux pas m'imaginer en train d'écrire des getters et des setters à la main (c'est-à-dire sans génération), ne serait-ce que pour un seul attribut. C'est impensable…

Classique

III-D - Hash code et Equals

Durée estimée : 1 minute.

Les méthodes « hashCode » et « equals » permettent respectivement d'avoir une représentation numérique d'un objet et de le comparer avec un autre objet.

Je n'inclus pas le père et la mère dans ces méthodes, car ça reviendrait à naviguer dans l'arbre généalogique, ce qui n'est pas mon but. Et puis c'est sans compter les orphelins et les problèmes récursifs qui entraineraient un (trop) long débat.

Génération du equals et du hashCode

Méthode hashCode

public class DogEclipse {
...

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((birthday == null) ? 0 : birthday.hashCode());
result = prime * result + ((color == null) ? 0 : color.hashCode());
result = prime * result + ((fullName == null) ? 0 : fullName.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((lof == null) ? 0 : lof.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((race == null) ? 0 : race.hashCode());
result = prime * result + ((weight == null) ? 0 : weight.hashCode());
return result;
}

Méthode hashCode

public class DogEclipse {
...

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DogEclipse other = (DogEclipse) obj;
if (birthday == null) {
if (other.birthday != null)
return false;
} else if (!birthday.equals(other.birthday))
return false;
if (color == null) {
if (other.color != null)
return false;
} else if (!color.equals(other.color))
return false;
if (fullName == null) {
if (other.fullName != null)
return false;
} else if (!fullName.equals(other.fullName))
return false;

...

return true;
}

}

On constate que le code généré (version complète en annexe) est relativement lourd.

Pas génial

III-E - To String

Durée estimée : 30 secondes.

La méthode « toString » donne une représentation de l'objet sous forme de caractères. En général, on utilise le nom de la classe et les valeurs de ses attributs intéressants.

Génération du toString

Génération du toString

public class DogEclipse {
...

@Override
public String toString() {
return "DogEclipse [id=" + id + ", name=" + name
+ ", fullName=" + fullName + ", birthday=" + birthday
+ ", race=" + race + ", lof=" + lof + ", color=" + color
+ ", weight=" + weight + "]";
}

}

Pas génial

III-F - Bilan des générations à l'aide d'Eclipse

Sans compter les imports et les commentaires, il aura fallu environ deux cents lignes de code, pas toujours très lisibles, pour équiper la classe « Dog » avec les méthodes classiques (et indispensables).

IV - Action avec Commons Lang

Les « Commons » sont un projet Apache qui existe depuis assez longtemps. Dans ce chapitre nous allons en utiliser une sous-partie : « Commons Lang ».

IV-A - Maven

Durée estimée : 1 minute.

Pour utiliser Apache Commons Lang, il faut ajouter une dépendance dans le fichier « pom.xml ».

Dépendance dans le pom.xml

<!-- Commons Lang -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.1</version>
</dependency>

Puis relancer une installation Maven.

Commande Maven

mvn clean install eclipse:eclipse

En fonction des éléments déjà présents sur le disque dur, le résultat de l'installation Maven devrait ressembler à la trace suivante :

Commande Maven

D:\javadev\article\developpez.com\nice-dog>mvn clean install eclipse:eclipse

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Nice dog 1.0-SNAPSHOT

...

[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.076s
[INFO] Finished at: Fri Jun 08 20:21:15 CEST 2012
[INFO] Final Memory: 12M/162M
[INFO] ------------------------------------------------------------------------

On doit ensuite faire un refresh (touche F5) dans Eclipse pour faire apparaitre Guava (et ses dépendances) dans la liste des bibliothèques référencées.

IV-B - Equals et hash code

Commons Lang va nous permettre de simplifier sensiblement le code de la méthode « equals » présentée plus haut. Pour cela, la bibliothèque dispose de l'objet « EqualsBuilder » auquel on indique les attributs à prendre en compte :

Méthode equals

import org.apache.commons.lang3.builder.EqualsBuilder;

public class DogCommonsLang {

private Integer id;
private String name;
private String fullName;
private Date birthday;
...

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof DogCommonsLang))
return false;

DogCommonsLang other = (DogCommonsLang) obj;

return new EqualsBuilder()
.append(id, other.id)
.append(name, other.name)
.append(fullName, other.fullName)
.append(birthday, other.birthday)
.append(race, other.race)
.append(lof, other.lof)
.append(color, other.color)
.append(weight, other.weight)
.isEquals();
}

}

Même principe pour la méthode « hashCode », on utilise l'objet « HashCodeBuilder » auquel on indique les attributs souhaités :

Méthode hasCode

import org.apache.commons.lang3.builder.HashCodeBuilder;

public class DogCommonsLang {

private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
...

@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(id)
.append(name)
.append(fullName)
.append(birthday)
.append(race)
.append(lof)
.append(color)
.append(weight)
.toHashCode();
}
}

Bien entendu, l'ordre des « append » a son importance. Si on change l'ordre, on aura un résultat différent.

IV-C - To String

Encore le même principe pour la méthode « toString », on utilise l'objet « ToStringBuilder » auquel on indique les attributs souhaités :

Méthode toString

import org.apache.commons.lang3.builder.ToStringBuilder;

public class DogCommonsLang {

private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
...

@Override
public String toString() {
return new ToStringBuilder(this)
.append("id", id)
.append("name", name)
.append("fullName", fullName)
.append("birthday", birthday)
.append("race", race)
.append("lof", lof)
.append("color", color)
.append("weight", weight)
.toString();
}
}

IV-D - Comparaison compareTo

La méthode « compareTo » est indispensable lorsqu'on a besoin de comparer des beans en Java, par exemple pour trier des listes. À ma connaissance, Eclipse ne propose pas de fonction de génération de code pour cette méthode. Dans la suite, je vous propose donc du code Java classique, tel que j'avais l'habitude de l'écrire avant de découvrir Commons Lang (ou Guava).

Pour qu'un bean soit « comparable », il faut implémenter l'interface « Comparable ».

À titre d'exemple, nous allons limiter les comparaisons aux attributs id, name, fullName, race, color et weight. Ça ne prend pas en compte les attributs birthday, lof, father et mother. En Java classique, cela ressemblerait au code suivant :

Méthode compareTo en Java classique

public class Dog implements Comparable<Dog> {

private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;

/**
* Dans l'ordre : id, name, fullName, race, color et weight.
*
* Ça ne prend pas en compte birthday, lof, father et mother.
*/
@Override
public int compareTo(Dog other) {
int result = 0;

result = id.compareTo(other.id);
if (result != 0) {
return result;
}

result = name.compareTo(other.name);
if (result != 0) {
return result;
}

result = fullName.compareTo(other.fullName);
if (result != 0) {
return result;
}

result = race.compareTo(other.race);
if (result != 0) {
return result;
}

result = color.compareTo(other.color);
if (result != 0) {
return result;
}

result = weight.compareTo(other.weight);
if (result != 0) {
return result;
}

return result;
}
}

Ce code est relativement long et lourd. Heureusement, Comons Lang propose la classe « CompareToBuilder » qui simplifie clairement la méthode.

Méthode compareTo

implements Comparable<DogCommonsLang> {

public class DogCommonsLang implements Comparable<DogCommonsLang> {

private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
...

/**
* Dans l'ordre : id, name, fullName, race, color et weight.
*
* Ça ne prend pas en compte birthday, lof, father et mother.
*/
@Override
public int compareTo(DogCommonsLang other) {
return new CompareToBuilder()
.append(id, other.id)
.append(name, other.name)
.append(fullName, other.fullName)
.append(race, other.race)
.append(color, other.color)
.append(weight, other.weight)
.toComparison();
}
}

IV-E - Bilan de l'utilisation de Commons Lang

Comme on l'a vu, l'utilisation de Commons Lang permet de gagner quelques lignes de code, mais ce n'est pas l'essentiel. Ce qui compte, c'est que le code est bien plus lisible et robuste, ce qui en augmente la qualité et la maintenance.

V - Action avec Guava

Guava est une bibliothèque créée par Google, sous l'impulsion de Kevin Bourrillon et la supervision de Josh Bloch. Dans cet article, nous allons utiliser uniquement ses composants « base », mais on peut garder en tête que Guava fait beaucoup d'autres choses.

Le nom initial de Guava était « Google-Collections ». J'avais d'ailleurs écrit un article sur les Google Collections il y a quelques mois, avant le changement de nom.

V-A - Maven

Durée estimée : 1 minute.

Pour utiliser Guava, il faut ajouter une dépendance dans le fichier « pom.xml ».

Dépendance dans le pom.xml

<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>11.0.1</version>
</dependency>

Puis relancer une installation Maven.

Commande Maven

mvn clean install eclipse:eclipse

En fonction des éléments déjà présents sur le disque dur, le résultat de l'installation Maven devrait ressembler à la trace suivante :

Commande Maven

D:\javadev\article\developpez.com\nice-dog>mvn clean install eclipse:eclipse

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Nice dog 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.4.1:clean (default-clean) @ nice-dog ---
[INFO] Deleting D:\javadev\article\developpez.com\nice-dog\target
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:resources (default-resources) @ nice-dog ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:2.3.1:compile (default-compile) @ nice-dog ---
[INFO] Compiling 2 source files to D:\javadev\article\developpez.com\nice-dog\target\classes
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ nice-dog ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:2.3.1:testCompile (default-testCompile) @ nice-dog ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.7.2:test (default-test) @ nice-dog ---
[INFO] Surefire report directory: D:\javadev\article\developpez.com\nice-dog\target\surefire-reports

...

[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.076s
[INFO] Finished at: Fri Jun 08 20:21:15 CEST 2012
[INFO] Final Memory: 12M/162M
[INFO] ------------------------------------------------------------------------

On doit ensuite faire un refresh (touche F5) dans Eclipse pour faire apparaitre Guava (et ses dépendances) dans la liste des bibliothèques référencées.

V-B - Equals et hash code

Durée estimée : 2 minutes.

Guava permet de simplifier la méthode « equals » à l'aide de l'objet « Objects » et plus spécifiquement de sa méthode « equals » :

Méthode equals

public class DogGuava {
...

@Override
public boolean equals(Object obj) {

if (this == obj) return true;
if (obj == null) return false;
if (! (obj instanceof DogGuava)) return false;

DogGuava other = (DogGuava) obj;

return Objects.equal(id, other.id)
&& Objects.equal(name, other.name)
&& Objects.equal(fullName, other.fullName)
&& Objects.equal(race, other.race)
&& Objects.equal(lof, other.lof)
&& Objects.equal(color, other.color)
&& Objects.equal(weight, other.weight);
}

On voit que Guava et Commons Lang ont deux approches différentes. Guava propose principalement l'objet « Objects » dont les méthodes réalisent des fonctionnalités variées alors que Commons Lang préfère proposer plusieurs objets pour réaliser les mêmes opérations.

Pour simplifier la méthode « hashCode » à l'aide de Guava, on va là aussi utiliser l'objet « Objects », mais avec « hashCode » :

Méthode hashCode

public class DogGuava {
...

@Override
public int hashCode() {
return Objects.hashCode(id, name, fullName, birthday, race, lof, color, weight);
}

La version Guava de la méthode « hashCode » est donc un peu plus concise que celle de Commons Lang.

Génial

V-C - To String

Sans surprise, pour améliorer la méthode « toString » de notre chien avec Guava, on va utiliser les fonctions « toString » et « toStringHelper » de l'objet « Objects » :

Méthode toString

public class DogGuava {
...

@Override
public String toString() {
return Objects.toStringHelper(this)
.add("id", id)
.add("name", name)
.add("fullName", fullName)
.add("birthday", birthday)
.add("race", race)
.add("lof", lof)
.add("color", color)
.add("weight", weight)
.toString();
}

C'est tout de même plus lisible.

Génial

V-D - Comparaison de canins

L'API Guava est relativement riche lorsqu'il s'agit de comparer des objets. Dans notre cas, pour réaliser la méthode « compareTo », le plus simple est d'utiliser la classe « ComparisonChain » :

Méthode compareTo avec Guava

public class DogGuava implements Comparable<DogGuava> {

private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;

...

/**
* Dans l'ordre : id, name, fullName, race, color et weight.
*
* Ça ne prend pas en compte birthday, lof, father et mother.
*/
@Override
public int compareTo(DogGuava other) {
return ComparisonChain.start()
.compare(id, other.id)
.compare(name, other.name)
.compare(fullName, other.fullName)
.compare(race, other.race)
.compare(color, other.color)
.compare(weight, other.weight)
.result();
}

...

Guava s'arrête dès que la comparaison donne un résultat. En d'autres mots, la bibliothèque n'est pas obligée d'aller jusqu'au bout pour renvoyer sa réponse.

On peut passer (en troisième argument de « compare ») un comparateur spécifique. On n'est donc pas limité au comparateur automatique.

Génial

V-E - Bilan de l'utilisation de Guava

On constate que notre bean est relativement plus simple grâce à Guava. La bibliothèque ne permet pas un gain considérable en nombre de lignes, mais elle rend le code plus lisible, plus efficace et plus maintenable.

On voit qu'il y a finalement assez peu de différences entre les versions de notre chien écrites avec Commons Lang et Guava. Apache avait été un précurseur sur le sujet, mais s'était fait plus discret au passage à Java 5 (en particulier sur les generics). Google, quant à lui, est relativement actif et Guava est un peu plus à la mode que Commons Lang. Alors, lequel choisir ? Les deux bibliothèques remplissent le contrat de façon équivalente. Le choix de l'un ou de l'autre sera une question de goût. Ou de mode… Dans tous les cas, je vous invite à consulter une discussion sur stackoverflow (Guava Vs Apache) qui montre qu'il y a un très large consensus en faveur de Guava.

VI - Action avec Lombok

Lombok fonctionne directement à l'aide d'annotations (contrairement à Commons Lang et Guava). Comme nous allons le voir, cela permet d'écrire des classes relativement minimalistes.

Dans ce chapitre, je ne vais présenter que les fonctionnalités de Lombok qui sont en rapport avec le fil rouge de cet article. Lombok est plus gros que ça. Je détaille les autres fonctionnalités (annotations) en annexe.

VI-A - Maven

Durée estimée : 1 minute.

Pour utiliser Lombok, il faut ajouter une dépendance dans le fichier « pom.xml ».

Dépendance dans le pom.xml

<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>0.11.0</version>
</dependency>

Puis relancer une installation Maven.

Commande Maven

mvn clean install eclipse:eclipse

En fonction des éléments déjà présents sur le disque dur, le résultat de l'installation Maven devrait ressembler à la trace suivante :

Commande Maven

D:\javadev\article\developpez.com\nice-dog>mvn clean install eclipse:eclipse

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Nice dog 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
Downloading: http://repository.jboss.org/nexus/content/groups/public/org/projectlombok/lombok/0.11.0/lombok-0.11.0.jar
Downloaded: http://repository.jboss.org/nexus/content/groups/public/org/projectlombok/lombok/0.11.0/lombok-0.11.0.jar (1707 KB at 206.8 KB/sec)
[INFO]
[INFO] --- maven-clean-plugin:2.4.1:clean (default-clean) @ nice-dog ---
[INFO] Deleting D:\javadev\article\developpez.com\nice-dog\target
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:resources (default-resources) @ nice-dog ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:2.3.1:compile (default-compile) @ nice-dog ---
[INFO] Compiling 2 source files to D:\javadev\article\developpez.com\nice-dog\target\classes
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ nice-dog ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:2.3.1:testCompile (default-testCompile) @ nice-dog ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.7.2:test (default-test) @ nice-dog ---
[INFO] Surefire report directory: D:\javadev\article\developpez.com\nice-dog\target\surefire-reports

...

[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.076s
[INFO] Finished at: Fri Jun 08 21:31:23 CEST 2012
[INFO] Final Memory: 11M/162M
[INFO] ------------------------------------------------------------------------

On doit ensuite faire un refresh (touche F5) dans Eclipse pour faire apparaitre Lombok (et ses dépendances) dans la liste des bibliothèques référencées.

VI-B - Installer Lombok dans Eclipse

Durée estimée : 1 minute.

Puisque Lombok fonctionne à base d'annotations, les méthodes générées n'apparaissent pas dans la fenêtre « outline » d'Eclipse. Pour y remédier, il faut installer un petit plugin dans l'IDE.

Pour ajouter les fonctionnalités de Lombok à l'IDE Eclipse, il faut d'abord télécharger le « jar » de Lombok sur le site Web du projet Lombok (cf. capture en annexe, rubrique « liens »), puis double-cliquer (sous Windows) sur le fichier « jar » pour l'exécuter.

Je conseille de quitter Eclipse avant de poursuivre l'installation.

Lancement du jar

Dans la fenêtre qui s'ouvre, il faut sélectionner le dossier d'installation de l'IDE (Eclipse et Netbeans sont supportés) à l'aide du bouton « specify location ».

Avec Eclipse sélectionné

Puis cliquer sur le bouton « Install/Update ».

Il faut relancer Eclipse

Et pour finir, il suffit de quitter et relancer Eclipse.

Il n'est nécessaire d'exécuter cette procédure qu'une seule fois, pour l'ensemble de tous les projets.

VI-C - Constructeurs

Commençons par le commencement : les constructeurs. Grâce à Lombok, on peut les remplacer en partie par l'ajout de simples annotations.

À titre de rappel, voici à quoi ressemble Eclipse avant qu'on commence à utiliser Lombok. Au fur et à mesure, on va voir que la fenêtre « outline » va se remplir.

Dans Eclipse

Annotations @NoArgsConstructor et @AllArgsConstructor

import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
public class DogLombok {
private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;
}

Annotations @NoArgsConstructor et @AllArgsConstructor

On constate que les constructeurs sont apparus dans la fenêtre « outline » (et donc aussi dans le code compilé) alors qu'ils sont absents du code Java pur.

Quand on décompile le résultat des annotations « @NoArgsConstructor » et « @AllArgsConstructor », on obtient le code suivant.

Annotations @NoArgsConstructor et @AllArgsConstructor décompilées

import java.beans.ConstructorProperties;
import java.util.Date;

public class DogLombok
{
private Integer id;
private String name;
private String fullName;
...

public DogLombok()
{
}

@ConstructorProperties({"id", "name", "fullName", "birthday", "race", "lof", "father", "mother", "color", "weight"})
public DogLombok(Integer id, String name, String fullName, Date birthday, String race, Boolean lof, Dog father,
Dog mother, String color, Double weight)
{
this.id = id;
this.name = name;
this.fullName = fullName;
this.birthday = birthday;
...
}
}

Sans surprise, ça ressemble à du code tel qu'on a l'habitude de l'écrire, avec le bonus du « @ConstructorProperties », mais il n'y a pas grand-chose à inventer sur ce sujet.Quoique…

Génial

Avec Lombok, on peut également créer un constructeur n'utilisant qu'une partie des attributs à l'aide de l'annotation « @RequiredArgsConstructor », mais il y a une petite mécanique à comprendre.

Annotation @RequiredArgsConstructor

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class DogLombok {
private Integer id;

private String name;
private String fullName;
private Date birthday;

...
}

Cette annotation va générer un « static factory » qui prend en paramètre tous les attributs marqués « final » et les attributs annotés « @NonNull ». Bien entendu, il est possible d'indiquer à l'annotation « @RequiredArgsConstructor » le nom de la méthode à créer.

Annotations @RequiredArgsConstructor et @NonNull

import lombok.RequiredArgsConstructor;
import lombok.NonNull;

@RequiredArgsConstructor(staticName = "of")
public class DogLombok {
@NonNull
private Integer id;

@NonNull
private String name;

private String fullName;
private Date birthday;

...
}

L'exemple ci-dessus crée donc un constructeur « static » prenant les attributs « id » et « name » en paramètres. Pour l'utiliser, on écrira un code ressemblant au suivant :

Utilisation

DogLombok dog= DogLombok.of(12, "Milou");

Pour encore mieux comprendre comment ça fonctionne, voici le code décompilé correspondant à cet exemple.

Annotations @RequiredArgsConstructor et @NonNull décompilées

import lombok.NonNull;

public class DogLombok
{
@NonNull
private Integer id;

@NonNull
private String name;

private String fullName;
private Date birthday;
...

private DogLombok(@NonNull Integer id, @NonNull String name)
{
if (id == null)
throw new NullPointerException("id");
if (name == null)
throw new NullPointerException("name");
this.id = id;
this.name = name;
}

public static DogLombok of(@NonNull Integer id, @NonNull String name)
{
return new DogLombok(id, name);
}
}

Il y a encore un peu de travail à fournir sur cet aspect pour que la bibliothèque soit parfaite, car on ne peut créer qu'un seul « constructeur static », avec un seul jeu de paramètres qui, de surcroit, sont associés à des attributs devant répondre à des conditions contraignantes. Toutefois, d'expérience, cela correspond à des situations qu'on retrouve relativement souvent et peut donc s'apparenter à un compromis acceptable. À voir ce que nous proposeront les prochaines versions de Lombok…

Pas génial

VI-D - Getters et setters

Les accesseurs correspondent certainement à une des cibles privilégiées des utilisateurs de Lombok. En effet, les getters et setters standards ne sont que du code technique, qui prend une place considérable dans les beans. Dit autrement, il faut six lignes de code pour écrire les accesseurs d'un seul attribut. Comme on va le voir, Lombok permet d'économiser 75 % d'espace au minimum.

Pour créer un getter à l'aide de Lombok, il faut utiliser l'annotation « @Getter » sur l'attribut concerné. Pour créer un setter, on doit l'annoter avec « @Setter » :

Annotations @Getter et @Setter sur l'id

import lombok.Getter;
import lombok.Setter;

@NoArgsConstructor
@AllArgsConstructor
public class DogLombok {

@Getter
@Setter
private Integer id;

...

C'est magique. On voit que les méthodes apparaissent dans la fenêtre « outline » d'Eclipse.

Annotations @Getter et @Setter sur l'id

Faisons de même avec l'ensemble des attributs. Sans compter les commentaires et les lignes de séparation, les accesseurs des dix attributs nécessitaient soixante lignes de code (80 avec les sauts de ligne et environ 150 en comptant la Javadoc standard). Grâce à Lombok, on gagne cinquante lignes (140 en comptant les sauts de ligne et la doc).

Annotations @Getter et @Setter sur tous les attributs

import lombok.Getter;
import lombok.Setter;

@NoArgsConstructor
@AllArgsConstructor
public class DogLombok {

@Getter    @Setter
private Integer id;

@Getter    @Setter
private String name;

@Getter    @Setter
private String fullName;

@Getter    @Setter
private Date birthday;

@Getter    @Setter
private String race;

...

Annotations @Getter et @Setter sur tous les attributs

Clairement, on gagne un nombre de lignes énorme, mais on peut encore en gagner plus dans le cas où on veut générer des accesseurs pour tous les attributs. Dans ce cas, on peut directement ajouter les annotations au niveau de la classe.

L'annotation @Getter au niveau de la classe crée des getters pour tous les attributs

import lombok.Setter;

@Setter
public class DogLombok {

private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;

...

Annotation @Getter au niveau de la classe

En complément des annotations « @Getter » et « @Setter » au niveau de la classe, qui créent des accesseurs pour tous les attributs, on peut utiliser ces annotations en complément sur chaque attribut pour changer les valeurs par défaut.

Le code suivant génère ainsi des setters « public » pour tous les attributs (à cause de l'annotation « @Setter » au niveau de la classe) sauf pour les attributs « id » (pour lequel on ne génère rien) et « name » (pour lequel on génère un setter « protected »).

Combinaison d'annotations @Setter

import static lombok.AccessLevel.NONE;
import static lombok.AccessLevel.PROTECTED;

import lombok.Getter;

@Setter
public class DogLombok {

@Setter(NONE)
private Integer id;

@Setter(PROTECTED)
private String name;

private String fullName;
private Date birthday;
private String race;

...

Combinaison d'annotations @Setter

VI-D-1 - getter lazy

Un peu en marge des « getters » classiques, imaginons que le bean comporte un attribut « private » et « final », ce qui n'est pas si rare, et que la valeur de l'attribut soit couteuse à calculer. Le code d'un tel bean ressemblerait alors au bloc suivant.

Bean avec constante

@Getter
public class DogLombokAvecLazzy {

private Integer id;
private String name;
private String fullName;
...

private final String description = calculdescription(); // constante locale

private String calculdescription() {
System.out.println("calculdescription");
return "foo" + Math.random();
}
}

Dans l'exemple, la valeur de l'attribut « description » est calculée dès la création du bean. Java ne peut pas savoir à l'avance si l'attribut sera accédé et donc si le calcul ne sera pas réalisé pour rien.

Pour vérifier cela, aidons-nous de quelques traces :

Ordre de calcul

System.out.println("avant creation");
DogLombokAvecLazzy dog = new DogLombokAvecLazzy();
System.out.println("avant appel");
dog.getDescription();
System.out.println("apres appel");

Trace

avant creation
calculdescription
avant appel
apres appel

Ce qu'on aimerait, c'est que la valeur soit « lazy calculated », c'est-à-dire qu'elle n'est calculée que lorsqu'on en a besoin. Avec Lombok, il suffit de le préciser dans l'annotation « @Getter » à l'aide du paramètre « lazy ».

Bean avec constante lazy

@Getter
public class DogLombokAvecLazzy {

...

@Getter(lazy = true)
private final String description = calculdescription(); // constante locale

private String calculdescription() {
System.out.println("calculdescription");
return "foo" + Math.random();
}
}

Trace avec constante lazy

avant creation
avant appel
calculdescription
apres appel

On constate que la valeur n'est calculée que lorsqu'on la demande. Pour réaliser la même chose en Java pur, il faut de nombreuses lignes de code complexe.

Génial

VI-E - Hash code et Equals

Pour créer les méthodes « equals » et « hashCode », il faut utiliser l'annotation « @EqualsAndHashCode » (qui génère les deux d'un coup) au niveau de la classe :

Annotation @EqualsAndHashCode

import lombok.EqualsAndHashCode;

@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class DogLombok {
...
}

Annotation @EqualsAndHashCode

L'annotation « @EqualsAndHashCode » utilise tous les attributs non « static » pour remplir les contrats des méthodes « equals » et « hashCode ». Si on souhaite exclure une partie des attributs, il faut utiliser le paramètre « exclude » de l'annotation. Par exemple, pour exclure les attributs « father » et « mother », on peut écrire le code suivant :

Annotation @EqualsAndHashCode avec des exclusions

@EqualsAndHashCode(exclude = { "father", "mother" })
public class DogLombok {
private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;
}

Si, comme moi, on préfère spécifier la liste des attributs à prendre en compte, on doit utiliser le paramètre « of » de l'annotation « @EqualsAndHashCode ». Par exemple, pour utiliser les attributs « name », « fullName » et « id », on peut écrire le code suivant :

Annotation @EqualsAndHashCode sur les attributs name, fullName et id

@EqualsAndHashCode(of = { "name", "fullName", "id" })
public class DogLombok {
private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
...
}

Génial

VI-F - To String

Pour générer la méthode « toString » à l'aide de Lombok, il suffit d'utiliser l'annotation « @ToString » au niveau de la classe :

Annotation @ToString

import lombok.ToString;

@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public class DogLombok {
...
}

Résultat de @ToString

DogLombok(id=123, name=Milou, fullName=Milou de Belgique, birthday=somedate, race=fox terrier, lof=true, father=null, mother=null, color=blanc, weight=10.5)

Annotation @ToString

Par défaut, l'annotation « @ToString » utilise tous les attributs non marqués « static » pour remplir le contrat de la méthode « toString ». Si on souhaite exclure certains attributs, on peut utiliser le paramètre « exclude » de l'annotation.

Annotation @ToString avec des exclude de father et mother

@ToString(exclude={"father", "mother"})
public class DogLombok {
private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;
}

Résultat de @ToString avec des exclude de father et mother

DogLombok(id=123, name=Milou, fullName=Milou de Belgique, birthday=somedate, race=fox terrier, lof=true, color=blanc, weight=10.5)

À l'opposé, on peut préférer indiquer la liste des attributs qu'on souhaite utiliser. Dans ce cas, il faut se servir du paramètre « of » de l'annotation « @ToString ».

Annotation @ToString avec utilisation des id, name et fullName

@ToString(of={"id", "name", "fullName})
public class DogLombok {
private Integer id;
private String name;
private String fullName;
private Date birthday;
...
}

Résultat de @ToString avec utilisation des id, name et fullName

DogLombok(id=123, name=Milou, fullName=Milou de Belgique)

Je préfère indiquer les attributs à utiliser (paramètre « of ») que ceux à exclure (paramètre « exclude »), car je trouve que c'est plus lisible. Cela dit, c'est une affaire de goût.

Si on ne souhaite pas un rappel du nom des attributs, on peut utiliser le paramètre « includeFieldNames » de l'annotation « @ToString ».

Annotation @ToString sans le nom des attributs

@ToString(of = { "id", "name", "fullName" }, includeFieldNames = false)
public class DogLombok {
private Integer id;
private String name;
private String fullName;
....
}

Résultat de @ToString sans le nom des attributs

DogLombok(123, Milou, Milou de Belgique)

Je trouve que ça rend le résultat moins lisible. Je n'utilise donc jamais le paramètre « includeFieldNames ».

Génial

VI-G - Tout ça à la fois

Si on veut faire, d'un seul coup, tout ce qui a été présenté dans les paragraphes précédents, on peut utiliser l'annotation « @Data » au niveau de la classe :

L'annotation « @Data » fonctionne comme l'addition des annotations « @Getter », « @Setter », « @ToString », « @EqualsAndHashCode » et « @RequiredArgsConstructor ».

Annotation @Data

import lombok.Data;

@Data
public class DogLombok {

private Integer id;
private String name;
private String fullName;
private Date birthday;
...
}

Annotation @Data

Pour le coup, on peut difficilement imaginer plus concis.

La version décompilée de cet exemple, utilisant uniquement l'annotation « @Data », est disponible en annexe.

Génial

VI-H - Bilan de l'utilisation de Lombok

Sans compter les imports et les commentaires, il aura fallu seulement une douzaine de lignes de code pour avoir la classe complète. Sur ces lignes, une dizaine sont incompressibles puisqu'elles définissent la classe et ses attributs. Avec la seule annotation « @Data » de Lombok, on arrive donc au même résultat qu'avec les 190 lignes de code purement techniques générées par Eclipse ; cela se ressent sur la lisibilité et sur la maintenance.

Le projet Lombok-pg est un bon complément à Lombok, auquel il ajoute une vingtaine d'annotations.

VII - Conclusion

Comme on a pu le constater dans cet article, Apache Commons Lang et Google Guava apportent des solutions relativement semblables au surpoids du bean « Dog ». À l'opposé, Lombok se base sur une série d'annotations dont une partie (« @Getter », « @Setter », etc.) aurait une place légitime dans Java.

Ni Guava ni Lombok n'apportent de solution complète aux problématiques exposées dans cet article. Chaque bibliothèque apporte son lot de bons et/ou mauvais côtés. Je propose d'ailleurs une liste personnelle des avantages et des inconvénients en annexe.

Le code final de ce tutoriel est disponible dans le fichier ZIP le fichier Zip « nice-dog-2.zip »

Vos retours nous aident à améliorer nos publications. N'hésitez donc pas à commenter cet article sur le forum : Commentez

Retrouvez mes autres tutoriels sur developpez.com à l'adresse https://thierry-leriche-dessirier.developpez.com/#page_articles

VIII - Remerciements

Je tiens à remercier, en tant qu'auteur du tutoriel, toutes les personnes qui m'ont aidé et soutenu. Je pense tout d'abord à mes collègues qui subissent mes questions au quotidien, mais aussi à mes contacts et amis du Web, dans le domaine de l'informatique ou non, qui m'ont fait part de leurs remarques et critiques. Bien entendu, je n'oublie pas l'équipe de developpez.com qui m'a guidé dans la rédaction de cet article et m'a aidé à le corriger et le faire évoluer, principalement sur le forum.

Plus particulièrement j'adresse mes remerciements à Mickael Baron (keulkeul), Nemek, Kevin Bourrillion, Florent Ramière et Claude Leloup.

IX - Annexes

IX-A - Liens

Commons Lang : http://commons.apache.org/lang/

QR Code vers Commons Lang

Guava : http://code.google.com/p/guava-libraries/

QR Code vers Guava

Blog Guava by Google : https://blog.developpez.com/guava/

QR Code vers blog Guava by Google

Article « À la découverte du framework Google Collections » (Guava) : https://thierry-leriche-dessirier.developpez.com/tutoriels/java/tuto-google-collections/

QR Code vers article sur les Google-Collections

Lombok : http://projectlombok.org/

QR Code vers le projet Lombok

Le lien vers le fichier « jar » d'installation de Lombok dans Eclipse se trouve en haut à droite sur le site Web du projet Lombok (cf. capture).

Site Web du projet Lombok

Lombok-pg : https://github.com/peichhorn/lombok-pg/wiki

QR code vers Lombok-pg

IX-B - Liens personnels

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

QR Code vers mes articles

Ajoutez-moi à vos contacts à l'aide du QR Code suivant :

QR Code contenant ma vCard

Suivez-moi sur Twitter (@thierrylerichehttps://twitter.com/#!/thierryleriche) :

@thierryleriche

IX-C - Comparatif

Voici un récapitulatif des différentes solutions :

Commons Lang

Guava

Lombok

Constructeurs

-

-

« @NoArgsConstructor », « @AllArgsConstructor » et « @RequiredArgsConstructor »

Accesseurs

-

-

-

Equals et hashCode

« EqualsBuilder » et « HashCodeBuilder »

« Objects.equal » et « Objects.hashCode »

« @EqualsAndHashCode »

ToString

« ToStringBuilder »

« Objects.toStringHelper »

« @ToString »

compareTo

« CompareToBuilder »

« ComparisonChain »

-

Et voici un tableau détaillant les gains attendus (pour un objet possédant dix attributs) sans compter les lignes vides et les commentaires, en comparaison de la version Eclipse/Java :

Eclipse/Java

Commons Lang

Guava

Lombok

Constructeur vide

3 lignes

-

-

1 ligne (gain de 33 %)

Constructeur plein

12 lignes

1 ligne (gain de 91 %)

Accesseurs

6 lignes

-

-

1 ligne par attribut (gain de 83 %)
ou 1 ligne au niveau de la classe (gain de 98 %)

Equals et hashCode

12 + 49 lignes

34 lignes (gain de 45 %)

34 lignes

1 ligne (gain de 98 %)

ToString

13 lignes

14 lignes (perte)

14 lignes

1 ligne (gain de 92 %)

compareTo

34 lignes

14 lignes (gain de 58 %)

14 lignes

-

Comparaison du nombre de lignes

IX-D - Avantages et inconvénients

Très clairement, ni Commons Lang ni Guava ni Lombok ne couvrent toutes les problématiques que les développeurs rencontrent habituellement dans le cadre de l'écriture des codes standards (beans simples). Les approches des trois bibliothèques (commons Lang/Guava Vs Lombok) sont relativement différentes. Parfois elles se chevauchent, couvrant ainsi des besoins similaires. Parfois, elles se complètent. Parfois, elles ne traitent pas du tout du sujet. Parfois encore, elles sont en opposition.

En ce qui me concerne, je pense qu'on peut utiliser Guava et Lombok de concert. J'aime utiliser Lombok sur les beans simples, pour lesquels je peux me contenter de traitements standardisés comme une méthode « equals » basée sur l'ensemble des attributs. En général, j'utilise carrément l'annotation « @Data » de Lombok. Quand j'ai besoin de préciser un peu plus les choses, ou de réaliser des traitements spécifiques, je fais appel à Guava.

La plupart du temps, j'utilise les annotations « @Getter » et « @Setter » de Lombok au niveau de la classe, car je n'ai rien trouvé de plus concis et efficace.

Finalement, et quand Guava ne suffit plus, il ne me reste que l'option du code Java pur. D'ailleurs, en ce qui concerne les constructeurs (et mis à part pour les beans vraiment simples sur lesquels je ne souhaite pas dépenser mon énergie), je préfère souvent écrire du « vrai » code Java. Je trouve (et ça n'engage que moi) que c'est plus clair et sans ambiguïté au premier coup d'œil.

En fait, je crois que la raison principale de la faible adoption de Lombok est que cette bibliothèque fait des choses trop extraordinaires ; ça peut faire peur.

Dans les avantages de Lombok, je note évidemment la réduction de la quantité de code. On gagne de nombreuses lignes qui correspondent souvent à du code technique sans autre intérêt que d'être indispensable. Je pense en particulier aux accesseurs (getters et setters) qui demandent souvent de « scroller » pour savoir s'ils sont présents. Avec les annotations dédiées, on le sait dès la déclaration des attributs.

En outre, encore plus que pour savoir s'ils sont présents, on doit souvent scroller pour s'assurer que les méthodes classiques ne réalisent pas des choses exotiques ou inhabituelles. Dans la suite, je vais prendre l'exemple d'un setter, mais le raisonnement s'applique aussi aux getters, equals, hashCode, etc.

Un setter classique/standard se contente d'affecter un attribut :

Setter classique

public class MaClasse {

private String field;

...

public void setField(String field) {
this.field = field;
}

...

Mais, parfois, les setters font des trucs inhabituels, par exemple :

Setter inhabituel

public class MaClasse {

// private String field;
private Map<String, Integer> maMap;

...

public void setField(String field) {
Integer value = maMap.get(field);
if(value == null) {
value = 0;
}
maMap.put(field, ++value);

someDao.save(maMap);
...
}

...

Juste en regardant la fenêtre outline, il n'y a aucune chance que je détecte une telle méthode. Il faut absolument scroller pour le découvrir. Et bien entendu, je ne peux pas me permettre de scroller dans tous les beans de mes programmes à chaque fois que je veux utiliser une fonction. Et bien entendu, je pars du principe que la Javadoc va (volontairement) oublier de préciser qu'il y a quelque chose de spécial.

Avec Lombok, c'est plus simple. D'abord, tout est défini par annotations dès le début de la classe. Ensuite, tout est généré et donc je peux m'attendre à toujours avoir du code standard et sans (mauvaise) surprise.

Un peu dans la même idée, on essaie généralement d'avoir des méthodes « equals » et « hashCode » qui soient cohérentes entre elles, c'est-à-dire qui sont construites sur un modèle équivalent. Ce n'est d'ailleurs pas une surprise que Lombok dispose d'une seule annotation (« @EqualsAndHashCode ») pour générer les deux méthodes d'un coup.

Voici ce qu'en dit la Javadoc :

public int hashCode() : [..] The general contract of hashCode is:
- Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
- If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
- It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hashtables.

public boolean equals(Object obj) : [..] Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

Ce qui arrive souvent (et qui m'est arrivé par inattention lors de l'écriture de cet article), c'est d'avoir les méthodes « equals » est « hashCode » qui ne sont pas cohérentes, par exemple en oubliant de prendre en compte un des attributs. Or, c'est impossible de le détecter sans scroller et regarder attentivement.

Je note également que Lombok, tout comme Guava, est relativement bien développée et que je ne serais pas capable de faire mieux à l'aide de code personnel la plupart du temps. Là je pense en particulier à la synchronisation, au « lazy » et au « future » qui traumatisent de nombreux développeurs.

Pour autant, je pense que Lombok est loin d'être arrivé à maturité, notamment lorsqu'on doit se concentrer sur un sous-ensemble d'attributs, ou des combinaisons variées. C'est un des points qui me gênent plus particulièrement pour les constructeurs.

Un inconvénient que je note, et qui m'a été soufflé par un certain Kevin, concerne la documentation. Avec Lombok, il n'y a aucune documentation qui est générée, notamment par des annotations simples comme « @Getter » par exemple. Et en effet, ça peut vite devenir gênant. En fait, pour vraiment bien faire, il faudrait que ce soit directement intégré à Java et que les annotations utilisent (reportent) la documentation des attributs pour construire celles des méthodes générées. Mais ça n'est pas au programme des futures versions de Java.

IX-E - Annotations Lombok

Au moment où j'écris cet article (juin 2012), Lombok possède officiellement les annotations suivantes :

Dans la liste suivante, les annotations marquées d'un astérisque (*) sont expliquées dans le corps de l'article. Les autres sont un peu plus détaillés plus bas.

- « @Getter* » et « @Setter* » : ces deux annotations créent des accesseurs. Par défaut, Lombok crée des getters/setters « public », mais il est possible d'indiquer un autre niveau, à choisir parmi « PUBLIC », « PROTECTED », « PACKAGE » et « PRIVATE ». Ainsi « @Setter(AccessLevel.PROTECTED) private String name; » crée le setter « protected », nommé « setName » pour l'attribut « name ». À noter également qu'il est possible d'empêcher la création d'un accesseur particulier, notamment en association les annotations « @Data », « @Getter » et « @Setter » au niveau de la classe (qui créent des accesseurs pour tous les attributs). Pour cela on doit utiliser le niveau supplémentaire « NONE ».
En outre, il est possible de fabriquer un getter « lazy » sur des attributs marqués « final ». Pour cela, il faut utiliser le paramètre « lazy » de l'annotation « @Getter », comme expliqué plus haut dans cet article ;

- « @ToString* » : cette annotation génère la méthode « toString ». Par défaut, elle utilise tous les attributs non marqués « static ». Les paramètres « exclude » et « of » servent respectivement à indiquer si on veut exclure ou utiliser des attributs dans le résultat. Le paramètre « includeFieldNames » sert à préciser si on veut rappeler le nom des attributs utilisés ;

- « @EqualsAndHashCode* » : cette annotation génère les méthodes « equals » et « hasCode » d'un seul coup. Par défaut, elle utilise tous les attributs non marqués « static » ou « transient ». Les paramètres « exclude » et « of » servent respectivement à indiquer si on veut exclure ou utiliser des attributs dans le calcul.

- « @NoArgsConstructor* », « @RequiredArgsConstructor* » et « @AllArgsConstructor* » : ces trois annotations génèrent respectivement un constructeur sans argument, un constructeur utilisant les attributs marqués « final » et/ou « @NonNull », et enfin un constructeur utilisant tous les attributs de la classe. Le paramètre « staticName » de l'annotation « @RequiredArgsConstructor » permet de générer un « static factory » ayant le nom indiqué ;

- « @Data* » : cette annotation peut être vue comme la combinaison de « @ToString », « @EqualsAndHashCode », « @Getter » sur tous les attributs, « @Setter » sur tous les attributs non marqués « final », et « @RequiredArgsConstructor » ;

- « @Log » : cette annotation ajoute un logeur (par exemple Log4J) dans la classe ;

- « @Synchronized » : cette annotation génère l'équivalent d'un « locked synchronize » ;

- « @Cleanup » : cette annotation s'utilise dans le code pour indiquer que Lombok doit gérer la bonne fermeture d'une ressource ;

- « @SneakyThrows » : cette annotation permet de lancer des exceptions sans les déclarer dans la clause "throws de la méthode contenante ;

- « @Delegate » : cette annotation génère les « delegates » de toutes les méthodes « public » du type de l'objet de l'attribut.

Dans la suite, je vous propose quelques explications supplémentaires sur ces annotations. Vous trouverez encore des explications plus précises des annotations Lombok sur le site du projet.

Et si vous avez besoin d'annotations additionnelles, vous pouvez jeter un œil du côté de Lombok-pg, qui en propose une vingtaine : « @Action », « @AutoGenMethodStub », « @DoPrivileged », « @FluentSetter », « @ListenerSupport », « @Singleton », etc.

IX-E-1 - Annotation @Log

Quand on écrit des programmes, et plus particulièrement des gros programmes, on doit loger un grand nombre d'informations. Pour cela, on doit commencer par déclarer un logeur (par exemple Log4J) dans la classe. En général, on le fait de la manière suivante :

Déclaration et utilisation classiques d'un logeur

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MaClasse {

private static final Logger log = LoggerFactory.getLogger(MaClasse.class);

public void foo() {
log.info("bla bla bla");
...
}

...

Bien entendu, quand on veut changer d'implémentation, il faut changer toutes les classes du programme. Et puis ce n'est pas très pratique. Avec Lombok, il suffit d'utiliser l'annotation « @Log » :

Déclaration et utilisation d'un logeur avec Lombok

import lombok.extern.java.Log;

@Log
public class MaClasse {

public void foo() {
log.info("bla bla bla");
...
}

...

Si on préfère, on peut directement utiliser les implémentations spécifiques d'Apache, Java, Log4J ou encore Slf4J à l'aide des annotations « @CommonsLog », « @Log », « @Log4j » et « @Slf4j ».

IX-E-2 - Annotation @Synchronized

Le besoin de synchroniser des blocs de code est assez récurrent. En Java, on utilise principalement deux stratégies : le mot-clé « synchronized » et le « lock synchronized » :

synchronized standard

public class MaClasse {

public synchronized void foo() {
...
}

...

Le cas d'utilisation qui me vient tout de suite à l'esprit est le singleton :

Singleton

public class MaClasse {

private static MaClasse instance;

private MaClasse() {

}

public synchronized static MaClasse getInstance() {

if(instance == null) {
instance = new MaClasse();
}
return instance;
}

...

Le « synchronized lock » réalise un lock sur une variable plutôt que sur la méthode :

synchronized lock

public class MaClasse {

private final Object monObjetLock = new Object();

public void foo() {
synchronized(monObjetLock) {
...
}
}

...

Et là, je ne parle même pas du « synchronized lock double check », qu'on retrouve dans le code de Guava.

Lombok permet de générer un code de « synchronized lock » à l'aide de l'annotation « @Synchronized » :

@Synchronized

public class MaClasse {

@Synchronized
public void foo() {
...
}

...

Si on souhaite réaliser le « synchronized lock » sur une variable de la classe, il suffit d'en indiquer le nom dans l'annotation :

@Synchronized sur une variable

public class MaClasse {

private final Object monObjetLock = new Object();

@Synchronized("monObjetLock")
public void foo() {
...
}

...

Cela va générer un code équivalent au code proposé plus haut.

IX-E-3 - Annotation @Cleanup

Avec l'annotation « @Cleanup », on se rapproche de la nouvelle gestion des ressources qui est apparue dans Java 7. Plus concrètement, voici un exemple qui devrait vous rappeler des souvenirs :

Gestion des ressources à l'ancienne

public void lireJava6(String from) {
InputStream in = null;
try {
in = new FileInputStream(from);
byte[] b = new byte[10000];
while (true) {
int r = in.read(b);
if (r == -1)
break;
...
}
} catch (Exception e) {
...
} finally {
if (in != null) {
try {
in.close();
} catch (Exception e) {
...
}
}
}
}

Ça vous rappelle des souvenirs ou des cauchemars ? Heureusement, Java 7 apporte une réponse. Mais Lombok apporte aussi une solution, dès les versions précédentes du langage :

Gestion des ressources avec Lombok

public void lire(String from) throws IOException {

@Cleanup
InputStream in = new FileInputStream(from);

byte[] b = new byte[10000];
while (true) {
int r = in.read(b);
if (r == -1)
break;
...
}
}

Le gain saute aux yeux. Bien entendu, toutes les ressources ne disposent pas forcément de la méthode « close ». Dans ce cas, il suffit de spécifier le nom de la méthode équivalente dans l'annotation. Par exemple, si la méthode qu'on veut appeler à la fin du traitement s'appelle « die », le code ressemblera au suivant :

Die die die

public void foo() {

@Cleanup("die")
MonObj monObj = new MonObj(...);

//...
}

Pour faire simple, Lombok va appeler « monObj.die() » à la fin du traitement (c.-à-d. à l'accolade fermante de la méthode).

IX-E-4 - Annotation @Delegate

Illustrons le fonctionnement de cette annotation en créant un objet « Diet » qui possède plusieurs méthodes « public »

Diet

public class Diet {

public void eatLess() {
// ...
}

public void eatApple(int numberOfApples) {
// ...
}

public double calcul() {
// ...
return 0;
}

...

Et disons que nous déclarons un attribut de type « Diet » dans notre « Dog » :

Diet dans le chien

public class DogByThierry {

private Integer id;
private String name;
private String fullName;
...

@Delegate
private Diet diet = new Diet();

...

L'annotation « @Delegate » va créer dans « DogByThierry » toutes les méthodes nécessaires pour déléguer des appels vers les méthodes « public » de l'objet « Diet ».

Plus concrètement, voici à quoi va ressembler le code décompilé :

Diet dans le chien, code décompilé

public class DogByThierry
{
private Integer id;
private String name;
...
private Diet diet;

...

public double calcul()
{
return getDiet().calcul();
}

public void eatApple(int numberOfApples)
{
getDiet().eatApple(numberOfApples);
}

public void eatLess()
{
getDiet().eatLess();
}

...

IX-E-5 - Mot-clé bonus : val

En plus de ces annotations, Lombok propose le mot-clé « val » qui fonctionne un peu comme la syntaxe « Diamond » de Java 7 et dont voici un exemple d'utilisation.

Synthaxe Diamand

List<String> list1 = new ArrayList<>();

La syntaxe « Diamand » n'est disponible qu'à partir de la version 7 de Java.

Mot-clé val

import lombok.val;
...

val list2 = new ArrayList<String>();
list2.size();

Pour ma part, je préfère la vision de Guava, qui est plus proche de la syntaxe « Diamand ».

Avec Guava

import com.google.common.collect.Lists;
...

List<String> list3 = Lists.newArrayList();

IX-F - Les fichiers importants en entier

Dog.java

package com.dvp.tutoriel.nicedog;

import java.util.Date;

public class Dog implements Comparable<Dog> {

private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;

/**
* Dans l'ordre : id, name, fullName, race, color et weight.
*
* Ça ne prend pas en compte birthday, lof, father et mother.
*/
@Override
public int compareTo(Dog other) {
int result = 0;

result = id.compareTo(other.id);
if (result != 0) {
return result;
}

result = name.compareTo(other.name);
if (result != 0) {
return result;
}

result = fullName.compareTo(other.fullName);
if (result != 0) {
return result;
}

result = race.compareTo(other.race);
if (result != 0) {
return result;
}

result = color.compareTo(other.color);
if (result != 0) {
return result;
}

result = weight.compareTo(other.weight);
if (result != 0) {
return result;
}

return result;
}
}

DogEclipse.java (très long)

package com.dvp.tutoriel.nicedog;

import java.util.Date;

public class DogEclipse {

private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;

public DogEclipse() {
super();
// TODO Auto-generated constructor stub
}

public DogEclipse(Integer id, String name, String fullName, Date birthday, String race, Boolean lof, Dog father, Dog mother, String color, Double weight) {
super();
this.id = id;
this.name = name;
this.fullName = fullName;
this.birthday = birthday;
this.race = race;
this.lof = lof;
this.father = father;
this.mother = mother;
this.color = color;
this.weight = weight;
}

public DogEclipse(String name, String fullName, Date birthday, String race) {
super();
this.name = name;
this.fullName = fullName;
this.birthday = birthday;
this.race = race;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((birthday == null) ? 0 : birthday.hashCode());
result = prime * result + ((color == null) ? 0 : color.hashCode());
result = prime * result + ((fullName == null) ? 0 : fullName.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((lof == null) ? 0 : lof.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((race == null) ? 0 : race.hashCode());
result = prime * result + ((weight == null) ? 0 : weight.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DogEclipse other = (DogEclipse) obj;
if (birthday == null) {
if (other.birthday != null)
return false;
} else if (!birthday.equals(other.birthday))
return false;
if (color == null) {
if (other.color != null)
return false;
} else if (!color.equals(other.color))
return false;
if (fullName == null) {
if (other.fullName != null)
return false;
} else if (!fullName.equals(other.fullName))
return false;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (lof == null) {
if (other.lof != null)
return false;
} else if (!lof.equals(other.lof))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (race == null) {
if (other.race != null)
return false;
} else if (!race.equals(other.race))
return false;
if (weight == null) {
if (other.weight != null)
return false;
} else if (!weight.equals(other.weight))
return false;
return true;
}

@Override
public String toString() {
return "DogEclipse [id=" + id + ", name=" + name
+ ", fullName=" + fullName + ", birthday=" + birthday
+ ", race=" + race + ", lof=" + lof + ", color=" + color
+ ", weight=" + weight + "]";
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getFullName() {
return fullName;
}

public void setFullName(String fullName) {
this.fullName = fullName;
}

public Date getBirthday() {
return birthday;
}

public void setBirthday(Date birthday) {
this.birthday = birthday;
}

public String getRace() {
return race;
}

public void setRace(String race) {
this.race = race;
}

public Boolean getLof() {
return lof;
}

public void setLof(Boolean lof) {
this.lof = lof;
}

public Dog getFather() {
return father;
}

public void setFather(Dog father) {
this.father = father;
}

public Dog getMother() {
return mother;
}

public void setMother(Dog mother) {
this.mother = mother;
}

public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}

public Double getWeight() {
return weight;
}

public void setWeight(Double weight) {
this.weight = weight;
}
}

DogCommonsLang.java

package com.dvp.tutoriel.nicedog;

import java.util.Date;

import org.apache.commons.lang3.builder.CompareToBuilder;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;

public class DogCommonsLang implements Comparable<DogCommonsLang> {

private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private DogCommonsLang father;
private DogCommonsLang mother;
private String color;
private Double weight;

@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(id)
.append(name)
.append(fullName)
.append(birthday)
.append(race)
.append(lof)
.append(color)
.append(weight)
.toHashCode();
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof DogCommonsLang))
return false;

DogCommonsLang other = (DogCommonsLang) obj;

return new EqualsBuilder()
.append(id, other.id)
.append(name, other.name)
.append(fullName, other.fullName)
.append(birthday, other.birthday)
.append(race, other.race)
.append(lof, other.lof)
.append(color, other.color)
.append(weight, other.weight)
.isEquals();
}

@Override
public String toString() {
return new ToStringBuilder(this)
.append("id", id)
.append("name", name)
.append("fullName", fullName)
.append("birthday", birthday)
.append("race", race)
.append("lof", lof)
.append("color", color)
.append("weight", weight)
.toString();
}

/**
* Dans l'ordre : id, name, fullName, race, color et weight.
*
* Ça ne prend pas en compte birthday, lof, father et mother.
*/
@Override
public int compareTo(DogCommonsLang other) {
return new CompareToBuilder()
.append(id, other.id)
.append(name, other.name)
.append(fullName, other.fullName)
.append(race, other.race)
.append(color, other.color)
.append(weight, other.weight)
.toComparison();
}

}

DogGuava.java

package com.dvp.tutoriel.nicedog;

import java.util.Date;

import com.google.common.base.Objects;
import com.google.common.collect.ComparisonChain;

public class DogGuava implements Comparable<DogGuava> {
private Integer id;

private String name;
private String fullName;
private Date birthday;

private String race;

private Boolean lof;
private Dog father;
private Dog mother;

private String color;
private Double weight;

@Override
public int hashCode() {
return Objects.hashCode(id, name, fullName, birthday, race, lof, color, weight);
}

@Override
public boolean equals(Object obj) {

if (this == obj)
return true;
if (obj == null)
return false;
if (! (obj instanceof DogGuava))
return false;

DogGuava other = (DogGuava) obj;

return Objects.equal(id, other.id)
&& Objects.equal(name, other.name)
&& Objects.equal(fullName, other.fullName)
&& Objects.equal(race, other.race)
&& Objects.equal(lof, other.lof)
&& Objects.equal(color, other.color)
&& Objects.equal(weight, other.weight);
}

@Override
public String toString() {
return Objects.toStringHelper(this)
.add("id", id)
.add("name", name)
.add("fullName", fullName)
.add("birthday", birthday)
.add("race", race)
.add("lof", lof)
.add("color", color)
.add("weight", weight)
.toString();
}

/**
* Dans l'ordre : id, name, fullName, race, color et weight.
*
* Ça ne prend pas en compte birthday, lof, father et mother.
*/
@Override
public int compareTo(DogGuava other) {
return ComparisonChain.start()
.compare(id, other.id)
.compare(name, other.name)
.compare(fullName, other.fullName)
.compare(race, other.race)
.compare(color, other.color)
.compare(weight, other.weight)
.result();
}
}

DogLombok.java

package com.dvp.tutoriel.nicedog;

import java.util.Date;

import lombok.Data;

@Data
public class DogLombok {

private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;
}

Voici une proposition (très personnelle) correspondant au type de bean que j'aime bien écrire. Cela n'engage que moi et je n'invite personne à me suivre. Toutefois je trouve que c'est un bon compromis parce qu'il n'y a pas trop de code purement technique grâce à Lombok et que j'ai pu compléter mes choix à l'aide de Guava :

- Lombok crée le constructeur vide (« @NoArgsConstructor ») et le constructeur plein (« @AllArgsConstructor ») ;

- j'ai ajouté deux constructeurs (en Java classique) avec des attributs sélectionnés pour correspondre à mon domaine ;

- Lombok crée les accesseurs (« @Getter » et « @Setter ») pour tous les attributs ;

- Lombok crée la méthode « toString » (« @ToString ») pour les attributs « id », « name », « fullName », « birthday », « race », « lof », « color » et « weight » ;

- Lombok crée les méthodes « equals » et « hashCode » (« @EqualsAndHashCode ») pour les attributs « name », « fullName » et « id » ;

- Guava me sert pour écrire la méthode « compareTo » pour les attributs « id », « name », « fullName », « race », « color » et « weight ».

Je n'ai pas utilisé l'annotation « @Data », car elle va en contradiction avec mon besoin de précision. Néanmoins, pour un bean plus simple, et dans une approche plus mécanique, j'aurais eu tendance à l'utiliser pour me faciliter la vie.

DogByThierry.java

package com.dvp.tutoriel.nicedog;

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import com.google.common.base.Objects;
import com.google.common.collect.ComparisonChain;

/*
* Constructeurs vide (par defaut) et plein (avec tous les attributs).
*/
@NoArgsConstructor
@AllArgsConstructor

/*
* Getter et setter sur tous les attributs.
*/
@Getter
@Setter

/*
* Je prefere indiquer les attributs qui m'interessent.
*/
@ToString(of = { "id", "name", "fullName", "birthday", "race", "lof", "color", "weight" })

/*
* La aussi, je prefere indiquer les attributs qui m'interessent.
*/
@EqualsAndHashCode(of = { "name", "fullName", "id" })
public class DogByThierry implements Comparable<DogByThierry> {

private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;

@Delegate
private Diet diet = new Diet();

/*
* Selon les gouts, on pourra ecrire un vrai constructeur vide ou
* l'annotation @NoArgsConstructor. Ici j'ai mis en commentaire le
* constructeur au profit de l'annotation.
*/
// public DogNice() {
// super();
// }

/*
* Constructeur avec juste l'identite du chien.
*/
public DogByThierry(String name, String fullName, Date birthday, String race) {
this();
this.name = name;
this.fullName = fullName;
this.birthday = birthday;
this.race = race;
}

/*
* Constructeur avec l'identite du chien et ses couleurs et poids.
*/
public DogByThierry(String name, String fullName, Date birthday, String race, String color, Double weight) {
this(name, fullName, birthday, race);
this.color = color;
this.weight = weight;
}

/*
* Guava.
*/
/**
* Dans l'ordre : id, name, fullName, race, color et weight.
*
* Ça ne prend pas en compte birthday, lof, father et mother.
*/
@Override
public int compareTo(DogByThierry other) {
return ComparisonChain.start()
.compare(id, other.id)
.compare(name, other.name)
.compare(fullName, other.fullName)
.compare(race, other.race)
.compare(color, other.color)
.compare(weight, other.weight)
.result();
}
}

IX-G - Classes décompilées

DogLombok.class décompilée

package com.dvp.tutoriel.nicedog;

import java.util.Date;

public class DogLombok
{
private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;

public Integer getId()
{
return this.id;
}

public String getName()
{
return this.name;
}

public String getFullName()
{
return this.fullName;
}

public Date getBirthday()
{
return this.birthday;
}

public String getRace()
{
return this.race;
}

public Boolean getLof()
{
return this.lof;
}

public Dog getFather()
{
return this.father;
}

public Dog getMother()
{
return this.mother;
}

public String getColor()
{
return this.color;
}

public Double getWeight()
{
return this.weight;
}

public void setId(Integer id)
{
this.id = id;
}

public void setName(String name)
{
this.name = name;
}

public void setFullName(String fullName)
{
this.fullName = fullName;
}

public void setBirthday(Date birthday)
{
this.birthday = birthday;
}

public void setRace(String race)
{
this.race = race;
}

public void setLof(Boolean lof)
{
this.lof = lof;
}

public void setFather(Dog father)
{
this.father = father;
}

public void setMother(Dog mother)
{
this.mother = mother;
}

public void setColor(String color)
{
this.color = color;
}

public void setWeight(Double weight)
{
this.weight = weight;
}

public boolean equals(Object o)
{
if (o == this)
return true;
if (!(o instanceof DogLombok))
return false;
DogLombok other = (DogLombok)o;
if (!other.canEqual(this))
return false;
if (getId() == null ? other.getId() != null : !getId().equals(other.getId()))
return false;
if (getName() == null ? other.getName() != null : !getName().equals(other.getName()))
return false;
if (getFullName() == null ? other.getFullName() != null : !getFullName().equals(other.getFullName()))
return false;
if (getBirthday() == null ? other.getBirthday() != null : !getBirthday().equals(other.getBirthday()))
return false;
if (getRace() == null ? other.getRace() != null : !getRace().equals(other.getRace()))
return false;
if (getLof() == null ? other.getLof() != null : !getLof().equals(other.getLof()))
return false;
if (getFather() == null ? other.getFather() != null : !getFather().equals(other.getFather()))
return false;
if (getMother() == null ? other.getMother() != null : !getMother().equals(other.getMother()))
return false;
if (getColor() == null ? other.getColor() != null : !getColor().equals(other.getColor()))
return false;
return getWeight() == null ? other.getWeight() == null : getWeight().equals(other.getWeight());
}

public boolean canEqual(Object other)
{
return other instanceof DogLombok;
}

public int hashCode()
{
int PRIME = 31;
int result = 1;
result = result * 31 + (getId() == null ? 0 : getId().hashCode());
result = result * 31 + (getName() == null ? 0 : getName().hashCode());
result = result * 31 + (getFullName() == null ? 0 : getFullName().hashCode());
result = result * 31 + (getBirthday() == null ? 0 : getBirthday().hashCode());
result = result * 31 + (getRace() == null ? 0 : getRace().hashCode());
result = result * 31 + (getLof() == null ? 0 : getLof().hashCode());
result = result * 31 + (getFather() == null ? 0 : getFather().hashCode());
result = result * 31 + (getMother() == null ? 0 : getMother().hashCode());
result = result * 31 + (getColor() == null ? 0 : getColor().hashCode());
result = result * 31 + (getWeight() == null ? 0 : getWeight().hashCode());
return result;
}

public String toString()
{
return "DogLombok(id=" + getId() + ", name=" + getName() + ", fullName=" + getFullName()
+ ", birthday=" + getBirthday() + ", race=" + getRace() + ", lof=" + getLof() + ", father=" + getFather()
+ ", mother=" + getMother() + ", color=" + getColor() + ", weight=" + getWeight() + ")";
}
}

DogByThierry.class décompilée

package com.dvp.tutoriel.nicedog;

import com.google.common.collect.ComparisonChain;
import java.beans.ConstructorProperties;
import java.util.Date;

public class DogByThierry
implements Comparable<DogByThierry>
{
private Integer id;
private String name;
private String fullName;
private Date birthday;
private String race;
private Boolean lof;
private Dog father;
private Dog mother;
private String color;
private Double weight;

public DogByThierry(String name, String fullName, Date birthday, String race)
{
this();
this.name = name;
this.fullName = fullName;
this.birthday = birthday;
this.race = race;
}

public DogByThierry(String name, String fullName, Date birthday, String race, String color, Double weight)
{
this(name, fullName, birthday, race);
this.color = color;
this.weight = weight;
}

public int compareTo(DogByThierry other)
{
return ComparisonChain.start().compare(this.id, other.id).compare(this.name, other.name)
.compare(this.fullName, other.fullName).compare(this.race, other.race).compare(this.color, other.color)
.compare(this.weight, other.weight).result();
}

public DogByThierry()
{
}

@ConstructorProperties({"id", "name", "fullName", "birthday", "race", "lof", "father", "mother", "color", "weight"})
public DogByThierry(Integer id, String name, String fullName, Date birthday, String race, Boolean lof,
Dog father, Dog mother, String color, Double weight)
{
this.id = id;
this.name = name;
this.fullName = fullName;
this.birthday = birthday;
this.race = race;
this.lof = lof;
this.father = father;
this.mother = mother;
this.color = color;
this.weight = weight;
}

public Integer getId()
{
return this.id;
}

public String getName()
{
return this.name;
}

public String getFullName()
{
return this.fullName;
}

public Date getBirthday()
{
return this.birthday;
}

public String getRace()
{
return this.race;
}

public Boolean getLof()
{
return this.lof;
}

public Dog getFather()
{
return this.father;
}

public Dog getMother()
{
return this.mother;
}

public String getColor()
{
return this.color;
}

public Double getWeight()
{
return this.weight;
}

public void setId(Integer id)
{
this.id = id;
}

public void setName(String name)
{
this.name = name;
}

public void setFullName(String fullName)
{
this.fullName = fullName;
}

public void setBirthday(Date birthday)
{
this.birthday = birthday;
}

public void setRace(String race)
{
this.race = race;
}

public void setLof(Boolean lof)
{
this.lof = lof;
}

public void setFather(Dog father)
{
this.father = father;
}

public void setMother(Dog mother)
{
this.mother = mother;
}

public void setColor(String color)
{
this.color = color;
}

public void setWeight(Double weight)
{
this.weight = weight;
}

public String toString()
{
return "DogByThierry(id=" + getId() + ", name=" + getName() + ", fullName=" + getFullName()
+ ", birthday=" + getBirthday() + ", race=" + getRace() + ", lof=" + getLof() + ", color=" + getColor()
+ ", weight=" + getWeight() + ")";
}

public boolean equals(Object o)
{
if (o == this)
return true;
if (!(o instanceof DogByThierry))
return false;
DogByThierry other = (DogByThierry)o;
if (!other.canEqual(this))
return false;
if (getId() == null ? other.getId() != null : !getId().equals(other.getId()))
return false;
if (getName() == null ? other.getName() != null : !getName().equals(other.getName()))
return false;
return getFullName() == null ? other.getFullName() == null : getFullName().equals(other.getFullName());
}

public boolean canEqual(Object other)
{
return other instanceof DogByThierry;
}

public int hashCode()
{
int PRIME = 31;
int result = 1;
result = result * 31 + (getId() == null ? 0 : getId().hashCode());
result = result * 31 + (getName() == null ? 0 : getName().hashCode());
result = result * 31 + (getFullName() == null ? 0 : getFullName().hashCode());
return result;
}
}