Comment fonctionne equals et == en Java ?

Vous avez appris comment utiliser == pour comparer des nombres et que les références d’objets font référence au même objet. Dans cette section, nous examinons ce que signifie pour deux objets d’être équivalents ou identiques. Nous examinons également l’impact du pool de String sur l’égalité.

Comparer equals() et ==

Considérez le code suivant qui utilise == avec des objets :

var un = new StringBuilder();
var deux = new StringBuilder();
var trois = un.append("a");
System.out.println(un == deux); // false
System.out.println(un == trois); // true

Puisque cet exemple ne traite pas des primitives, nous savons qu’il faut chercher si les références font référence au même objet. Les variables un et deux sont deux objets StringBuilder complètement séparés, ce qui nous donne deux objets. Par conséquent, la première instruction print nous donne false. La variable trois est plus intéressante. Rappelez-vous comment les méthodes StringBuilder aiment retourner la référence courante pour le chaînage ? Cela signifie que un et trois pointent tous deux vers le même objet, et la deuxième instruction print nous donne true.

Vous avez vu précédemment que equals() utilise l’égalité logique plutôt que l’égalité d’objet pour les objets String :

var x = "Bonjour Monde";
var z = " Bonjour Monde".trim();
System.out.println(x.equals(z)); // true

Cela fonctionne parce que les auteurs de la classe String ont implémenté une méthode standard appelée equals() pour vérifier les valeurs à l’intérieur du String plutôt que la référence de chaîne elle-même. Si une classe n’a pas de méthode equals(), Java détermine si les références pointent vers le même objet, ce qui est exactement ce que fait ==.

Au cas où vous vous poseriez la question, les auteurs de StringBuilder n’ont pas implémenté equals(). Si vous appelez equals() sur deux instances de StringBuilder, cela vérifiera l’égalité de référence. Vous pouvez appeler toString() sur StringBuilder pour obtenir un String afin de vérifier l’égalité à la place.

Enfin, pouvez-vous deviner pourquoi ce code ne compile pas ?

var nom = "a";
var constructeur = new StringBuilder("a");
System.out.println(nom == constructeur); // NE COMPILE PAS

Rappelez-vous que == vérifie l’égalité de référence d’objet. Le compilateur est assez intelligent pour savoir que deux références ne peuvent pas pointer vers le même objet lorsqu’elles sont de types complètement différents.

Le Pool de String

Comme les chaînes sont partout en Java, elles utilisent beaucoup de mémoire. Dans certaines applications de production, elles peuvent utiliser une grande quantité de mémoire dans l’ensemble du programme. Java réalise que de nombreuses chaînes se répètent dans le programme et résout ce problème en réutilisant les plus courantes. Le pool de chaînes, également connu sous le nom de pool d’internement, est un emplacement dans la machine virtuelle Java (JVM) qui collecte toutes ces chaînes.

Le pool de chaînes contient des valeurs littérales et des constantes qui apparaissent dans votre programme. Par exemple, “nom” est un littéral et va donc dans le pool de chaînes. La méthode monObjet.toString() renvoie une chaîne mais pas un littéral, elle ne va donc pas dans le pool de chaînes.

Visitons maintenant le scénario plus complexe et déroutant, l’égalité des String, rendu ainsi en partie à cause de la façon dont la JVM réutilise les littéraux String.

var x = "Bonjour Monde";
var y = "Bonjour Monde";
System.out.println(x == y); // true

Rappelez-vous qu’un String est immuable et que les littéraux sont mis en commun. La JVM n’a créé qu’un seul littéral en mémoire. Les variables x et y pointent toutes deux vers le même emplacement en mémoire ; par conséquent, l’instruction affiche true. Cela devient encore plus délicat. Considérez ce code :

var x = "Bonjour Monde";
var z = " Bonjour Monde".trim();
System.out.println(x == z); // false

Dans cet exemple, nous n’avons pas deux littéraux String identiques. Bien que x et z évaluent la même chaîne, l’une est calculée à l’exécution. Puisqu’elle n’est pas la même à la compilation, un nouvel objet String est créé. Essayons-en un autre. Que pensez-vous que cela affiche ?

var chaineUnique = "bonjour monde";
var concat = "bonjour ";
concat += "monde";
System.out.println(chaineUnique == concat); // false

Ceci imprime false. Appeler += revient à appeler une méthode et résulte en un nouveau String. Vous pouvez même forcer le problème en créant un nouveau String :

var x = "Bonjour Monde";
var y = new String("Bonjour Monde");
System.out.println(x == y); // false

Le premier dit d’utiliser normalement le pool de chaînes. Le second dit : “Non, JVM, je ne veux vraiment pas que tu utilises le pool de chaînes. Crée-moi un nouvel objet même si c’est moins efficace.”

Vous pouvez également faire l’inverse et dire à Java d’utiliser le pool de chaînes. La méthode intern() utilisera un objet dans le pool de chaînes si un est présent.

public String intern()

Si le littéral n’est pas encore dans le pool de chaînes, Java l’y ajoutera à ce moment-là.

var nom = "Bonjour Monde";
var nom2 = new String("Bonjour Monde").intern();
System.out.println(nom == nom2); // true

D’abord, nous disons à Java d’utiliser normalement le pool de chaînes pour nom. Ensuite, pour nom2, nous disons à Java de créer un nouvel objet en utilisant le constructeur mais de l’interner et d’utiliser quand même le pool de chaînes. Puisque les deux variables pointent vers la même référence dans le pool de chaînes, nous pouvons utiliser l’opérateur ==.

Essayons-en un autre. Que pensez-vous que cela affiche ? Soyez prudent. C’est délicat.

var premier = "rat" + 1;
var deuxieme = "r" + "a" + "t" + "1";
var troisieme = "r" + "a" + "t" + new String("1");
System.out.println(premier == deuxieme);
System.out.println(premier == deuxieme.intern());
System.out.println(premier == troisieme);
System.out.println(premier == troisieme.intern());

À la première ligne, nous avons une constante de compilation qui est automatiquement placée dans le pool de chaînes sous forme de “rat1”. À la deuxième ligne, nous avons une expression plus compliquée qui est également une constante de compilation. Par conséquent, premier et deuxieme partagent la même référence du pool de chaînes. Cela fait que les lignes 4 et 5 impriment true.

À la troisième ligne, nous avons un constructeur String. Cela signifie que nous n’avons plus une constante de compilation, et troisieme ne pointe pas vers une référence dans le pool de chaînes. Par conséquent, la ligne 6 imprime false. À la ligne 7, l’appel intern() cherche dans le pool de chaînes. Java remarque que premier pointe vers la même String et imprime true.

Lorsque vous écrivez des programmes, vous ne voudriez pas créer un String d’un String ou utiliser la méthode intern(). Vous devez savoir que les deux sont autorisés et comment ils se comportent.

Rappelez-vous de ne jamais utiliser intern() ou == pour comparer des objets String dans votre code.