Temps de lecture : 2 min.

Les fondamentaux

En java, la comparaison s’effectue principalement en utilisant 3 méthodes qui n’ont pas les mêmes buts :

  1. L’opérateur == est utilisé pour comparer les types primitifs et les objets. Il compare uniquement les références. Cela signifie que l’opérateur retournera “true” uniquement si les 2 objets comparés sont les mêmes en mémoire.
  2. La méthode ‘equals()’ permet de comparer 2 objets. Toutes les classes ont une méthode ‘equals()’ héritée de la classe Object. Cette méthode compare la valeur de 2 objets. S’ils représentent la même chose, alors la méthode doit renvoyer “true”. Cette méthode est plus difficile qu’il n’y paraît à implémenter. Je pourrais revenir dessus ultérieurement dans un prochain article.
  3. La méthode ‘compareTo()’ permet de comparer 2 objets comme la méthode ‘equals()’. Contrairement à ‘equals()’, les classes ne définissent pas toutes cette méthode. Pour cela il faut hériter de l’interface Comparable. Cette méthode est utilisée pour pouvoir trier les objets l’implémentant. Si les 2 objets sont égaux, alors la méthode doit renvoyer 0 ; si un object O1 est considéré précédent un autre object O2 alors il faut renvoyer -1, si O2 précède O1 alors il faut renvoyer 1.
int i = 5, j = 5, k=8;
Integer int1 = new Integer(42) ;
Integer int2 = new Integer(42) ;
String str1 = "Blog";

java.util.Date t1 = new java.util.Date();
Thread.sleep(1000l);
java.util.Date t2 = new java.util.Date();

System.out.println (i == j); // print true
System.out.println (int1 == int2); // print false
System.out.println ("Blog".equals(str1)); // print true
System.out.println (str1 == "Blog"); // déconseillé mais écrit true
System.out.println (t1.compareTo(t2)); // print -1

Les comparaisons faites avec ‘equals()’ ou ‘compareTo()’ entre une variable et une constante doivent être réalisées comme à la ligne 13 de l’exemple précédent, c’est-à-dire qu’il faut comparer la constante avec la variable à tester. Ceci permet d’éviter une potentielle NullPointerException si l’on faisait ‘str1.equals(“Blog”)’.

Le cas particuliers des enum

Depuis java 5, les enum ont été introduits dans java. Les enum pourraient faire l’objet d’un article complet, mais il est bon de savoir que les enum ont été créés pour être comparés avec l’opérateur ‘==’. N’hésitez donc pas à les utiliser ! En plus d’être très lisibles, ils sont performants à l’usage.

Les performances unitaires

Si on a le choix, quel opérateur/méthode utiliser pour faire la comparaison ? Un des critères de choix peut être la performance. Sur une application où les comparaisons sont très nombreuses, il peut être judicieux d’optimiser les comparaisons et donc de choisir en connaissance de cause.

Voici le petit programme qui m’a permis de tester les performances de chacun. Dans la boucle j’ai utilisé une des instructions en commentaire à chaque test :

long start = System.currentTimeMillis();
for (long l = 0; l < 2 << 60; l++) {
    // "1".equals("1");
    // if ("1" == "1") ;
    // "1".compareTo("1");
}
long end = System.currentTimeMillis();
long timePassedInMs = end - start;
&#91;/sourcecode&#93;

Sans surprise les résultats sont les suivants :
<ul>
	<li><strong>==</strong> : 978 ms de moyenne</li>
	<li><strong>equals()</strong> : 991 ms de moyenne, soit 1.35 % que ==</li>
	<li><strong>compareTo()</strong> : 1004 ms de moyenne, soit 2.62% de plus que ==</li>
</ul>
Quelques remarques sur ces résultats :
<ol>
	<li>La première instruction de la méthode 'equals()' de la classe String est de tester que les 2 objets comparés ont la même référence grâce à l'opérateur ==. La différence de temps entre == et 'equals()' s'explique donc uniquement par l'appel de la méthode et le retour de cette méthode. La différence vient aussi en partie du fait que la JVM va inliner la méthode 'equals()' et que ceci prend un certain temps.</li>
	<li>'compareTo()' de la classe String effectue plusieurs boucles et de nombreux tests. Il est tout à fait logique qu'elle soit plus lente que les 2 autres comparateurs. Elle n'est pas optimisée pour gagner du temps comme l'est 'equals()'.</li>
</ol>
Ces quelques tests montrent bien que faire un == est plus rapide que l'appel d'un 'equals()', même si dans le cas de la classe String la différence est faible. Si vous avez le choix, privilégiez donc le == à 'equals()', même si les performances ont l'air très proches. Il peut arriver que vous utilisiez du code que vous ne pouvez pas modifier (API, framework, librairie partagée...) et dans ce cas la méthode 'equals()' peut être mal écrite et donc bien plus lente qu'elle ne l'est pour la classe String. Mais la question est la suivante : comment faire pour avoir le choix entre == et 'equals()' ? Je vais essayer de vous montrer une manière d'avoir le choix un peu plus loin dans ce post, après vous avoir expliqué un peu plus en détail comment fonctionne la classe String.

Nous laissons de côté 'compareTo()' pour la fin du post, car elle n'a pas la même utilité que les 2 autres méthodes de comparaison.
<h2>Le cas de la classe String</h2>
Comme nous l'avons vu au début du post, les String doivent être comparés à l'aide de la méthode 'equals()'. Cependant il est tentant de vouloir utiliser l'opérateur == plus rapide et renvoyant quasiment tout le temps le même résultat que la méthode 'equals()'.

La classe String a beaucoup de particularités : c'est la seule classe que l'on peut initialiser sans appel explicite à un constructeur ou méthode statique ; elle possède des opérateurs surchargés... et d'autres spécificités encore. Une de ses particularité est que la JVM garde un pool d'instances des objets String. Ainsi, si deux String sont initialisés avec la même valeur, alors la JVM utilisera le même objet. Mais ce n'est pas vrai dans tous les cas. Regardons cet exemple :

[sourcecode language="java"]
final String TEST_VALUE = "infine";

String inPool = "infine";
String inPool2 = "in" + "fine";
String noPool = new String("infine");
String suffix = "fine";
String noPool2 = "in" + suffix;

System.out.println(inPool == TEST_VALUE);  // true
System.out.println(inPool2 == TEST_VALUE);  // true
System.out.println(noPool == TEST_VALUE);  // false
System.out.println(noPool2 == TEST_VALUE);  // false

En voyant les résultats, vous comprenez pourquoi il ne faut pas utiliser l’opérateur == pour la comparaison de String, le résultat est trop incertain. Lorsqu’un objet String est créé en utilisant l’opérateur new (comme pour un objet “normal” en fait), alors la JVM ne renvoie pas un objet String du pool, mais crée un nouvel objet (cas de l’objet “noPool”). De plus, si un objet String est calculé à l’exécution, par une concaténation par exemple, alors il n’est pas créé à partir du pool (cas de l’objet “noPool2”).

Pour se rendre compte de la manière dont le compilateur gère les String, voici un extrait du bytecode généré obtenu avec la commande javap :

0:	ldc	#16; //String infine
2:	astore_1
3:	ldc	#16; //String infine
5:	astore_2
6:	ldc	#16; //String infine
8:	astore_3

Dans cet extrait, nous voyons que 3 variables font référence au même objet String valant “infine” (matérialisé par la référence #16). Ces 3 variables sont dans le code précédent TEST_VALUE, inPool et inPool2. Elles sont déclarées différemment mais le compilateur ne crée qu’une seul référence dans le pool de String de la JVM.

Si on souhaite forcer la récupération d’un objet String à partir du pool, alors il faut utiliser la méthode ‘intern()’. Reprenons l’exemple ci-dessus, mais en utilisant la méthode ‘intern()’ :

final String TEST_VALUE = "infine";

String inPool = "infine";
String inPool2 = "in" + "fine";
String noPool = new String("infine");
String suffix = "fine";
String notPool2 = "in" + suffix;

System.out.println(inPool == TEST_VALUE);  // true
System.out.println(inPool2 == TEST_VALUE);  // true
System.out.println(noPool.intern() == TEST_VALUE);  // true !
System.out.println(notPool2.intern() == TEST_VALUE);  // true !

Pour en savoir plus, n’hésitez pas à chercher String literal pool dans votre moteur de recherche préféré.

Étude de cas : == doit renvoyer la même chose que equals()

La classe String arrive bien à faire que dans certains cas == et ‘equals()’ renvoient le même résultat. Nous allons donc essayer de faire mieux ! Nous allons donc partir d’un exemple simple dans lequel == et ‘equals()’ ne sont pas interchangeables. Enfin, nous allons modifier le code afin d’utiliser == pour chaque comparaison, ceci dans le but d’avoir de bonnes performances, et une meilleure gestion de la mémoire.
Bien entendu, nous pourrions gagner des performances de plein de manières différentes, mais ce n’est pas le but de cet article.

Présentation du code existant

Voici une classe permettant de gérer des couleurs RGB. Elle est donc composée de 3 attributs pouvant avoir une valeur comprise entre 0 et 255 chacun. Voici une partie du code de la classe avec son constructeur :

public class RgbColor {

    private final int green;
    private final int red;
    private final int blue;

    public RgbColor(int green, int red, int blue) {
        super();
        this.green = green;
        this.red = red;
        this.blue = blue;
    }

    // ...other methods, including hashcode() and equals()
}

Comme nous le voyons, cette classe possède un unique constructeur. Il est donc possible de créer autant d’objet de type RgbColor que nous voulons, et s’ils sont identiques nous seront obligés de les comparer avec ‘Equals()’ car chaque objet RgbColor sera différent.
Ce constructeur est l’équivalent du ‘String test = new String(“test”)’ vu précédemment dans ce post. Un appel crée toujours un objet.

Pool de gestion des instances

La version améliorée ajoute une nouvelle méthode statique. Cette méthode permet de créer un objet s’il n’existe pas encore dans le pool, ou alors de renvoyer l’objet existant si il a déjà été créé auparavant. Ce pattern est appelé static factory pattern, et est très bien décrit dans le livre Effective Java de Josh Bloch.

public class RgbColor {

private final int green;
private final int red;
private final int blue;
// pooling
private static Map pool = new ConcurrentHashMap();

/**
* Values must be between 0 and 255.
*
* @param green
* @param red
* @param blue
* @return
*/
public static RgbColor getColor(int green, int red, int blue) {
if (!isAllowedColorValue(green) || !isAllowedColorValue(red) || !isAllowedColorValue(blue)) {
throw new IllegalArgumentException(“Allowed color value is between 0 and 255”);
}
// same key as hashcode
int key = green << 16 | red << 8 | blue; if (pool.containsKey(key)) { return pool.get(key); } RgbColor color = new RgbColor(green, red, blue); pool.put(key, color); return pool.get(key); } private static boolean isAllowedColorValue(int color) { if (color < 0 || color > 255) {
return false;
}
return true;
}

private RgbColor(int green, int red, int blue) {
super();
this.green = green;
this.red = red;
this.blue = blue;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RgbColor other = (RgbColor) obj;
if (blue != other.blue)
return false;
if (green != other.green)
return false;
if (red != other.red)
return false;
return true;
}

@Override
public int hashCode() {
return green << 16 | red << 8 | blue; } // ...other methods } [/sourcecode] La méthode statique 'getColor()' permet de gérer un pool de toutes les instances de la classe RgbColor. De plus le constructeur est devenu privé, ce qui oblige l'utilisateur de cette classe à construire un object avec la méthode 'getColor()'. Contrairement à la classe String, tous les objets seront donc comparable avec l'opérateur == car un objet unique est créé par triplet rouge, vert, bleu. Elle est l'équivalent de la construction d'un object String sans constructeur : String test = "test"; Je ne vous joins pas de classe de test, mais les performances sont désormais améliorées car toutes les instances de RgbColor sont comparables avec l'opérateur ==. La mémoire est également mieux gérée car toutes les instances se retrouvent dans le pool. On pourrait encore améliorer cette classe, en remplaçant la 'ConcurrentHashMap' par une SoftHashMap par exemple, avec un peu d’adaptation du code.

Fichiers joints
Code source utilisé pour faire ce post : Archive zip

Rédigé par

Florian Boulay

Développeur java, geek android