Comparaison d'objets en Java

1. Introduction

La comparaison d'objets est une caractéristique essentielle des langages de programmation orientés objet.

Dans ce didacticiel, nous allons examiner certaines des fonctionnalités du langage Java qui nous permettent de comparer des objets. De plus, nous examinerons ces fonctionnalités dans les bibliothèques externes.

2. == et ! = Opérateurs

Commençons par les opérateurs == et ! = Qui peuvent dire si deux objets Java sont identiques ou non, respectivement.

2.1. Primitifs

Pour les types primitifs, être le même signifie avoir des valeurs égales:

assertThat(1 == 1).isTrue();

Grâce au déballage automatique, cela fonctionne également lors de la comparaison d'une valeur primitive avec son homologue de type wrapper :

Integer a = new Integer(1); assertThat(1 == a).isTrue();

Si deux entiers ont des valeurs différentes, l' opérateur == renvoie false , tandis que l' opérateur ! = Renvoie true .

2.2. Objets

Disons que nous voulons comparer deux types de wrapper Integer avec la même valeur:

Integer a = new Integer(1); Integer b = new Integer(1); assertThat(a == b).isFalse();

En comparant deux objets, la valeur de ces objets n'est pas 1. Ce sont plutôt leurs adresses mémoire dans la pile qui sont différentes puisque les deux objets ont été créés à l'aide de l' opérateur new . Si nous avions attribué a à b , alors nous aurions eu un résultat différent:

Integer a = new Integer(1); Integer b = a; assertThat(a == b).isTrue();

Voyons maintenant ce qui se passe lorsque nous utilisons la méthode d'usine Integer # valueOf :

Integer a = Integer.valueOf(1); Integer b = Integer.valueOf(1); assertThat(a == b).isTrue();

Dans ce cas, ils sont considérés comme identiques. Cela est dû au fait que la méthode valueOf () stocke l' Integer dans un cache pour éviter de créer trop d'objets wrapper avec la même valeur. Par conséquent, la méthode renvoie la même instance Integer pour les deux appels.

Java fait également cela pour String :

assertThat("Hello!" == "Hello!").isTrue();

Cependant, s'ils étaient créés à l'aide du nouvel opérateur, ils ne seraient pas les mêmes.

Enfin, deux références nulles sont considérées comme identiques, tandis que tout objet non nul sera considéré comme différent de nul :

assertThat(null == null).isTrue(); assertThat("Hello!" == null).isFalse();

Bien entendu, le comportement des opérateurs d'égalité peut être limitatif. Et si nous voulons comparer deux objets mappés à des adresses différentes tout en les considérant comme égaux en fonction de leurs états internes? Nous verrons comment dans les sections suivantes.

3. Object # equals Méthode

Parlons maintenant d'un concept plus large d'égalité avec la méthode equals () .

Cette méthode est définie dans la classe Object afin que chaque objet Java en hérite. Par défaut, son implémentation compare les adresses mémoire des objets, donc il fonctionne de la même manière que l' opérateur == . Cependant, nous pouvons remplacer cette méthode afin de définir ce que signifie l'égalité pour nos objets.

Tout d'abord, voyons comment il se comporte pour les objets existants comme Integer :

Integer a = new Integer(1); Integer b = new Integer(1); assertThat(a.equals(b)).isTrue();

La méthode renvoie toujours true lorsque les deux objets sont identiques.

Nous devons noter que nous pouvons passer un objet nul comme argument de la méthode, mais bien sûr, pas comme objet sur lequel nous appelons la méthode.

Nous pouvons utiliser la méthode equals () avec notre propre objet. Disons que nous avons une classe Person :

public class Person { private String firstName; private String lastName; public Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }

Nous pouvons remplacer la méthode equals () pour cette classe afin de pouvoir comparer deux personnes en fonction de leurs détails internes:

@Override public boolean equals(Object o)  if (this == o) return true; if (o == null 

Pour plus d'informations, consultez notre article sur ce sujet.

4. Objects # est égal à la méthode statique

Regardons maintenant la méthode statique Objects # equals . Nous avons mentionné précédemment que nous ne pouvons pas utiliser null comme valeur du premier objet, sinon une NullPointerException serait lancée.

La méthode equals () de la classe d'assistance Objects résout ces problèmes. Il prend deux arguments et les compare, en gérant également les valeurs nulles .

Comparons à nouveau les objets Person avec:

Person joe = new Person("Joe", "Portman"); Person joeAgain = new Person("Joe", "Portman"); Person natalie = new Person("Natalie", "Portman"); assertThat(Objects.equals(joe, joeAgain)).isTrue(); assertThat(Objects.equals(joe, natalie)).isFalse();

Comme nous l'avons dit, la méthode gère les valeurs nulles . Par conséquent, si les deux arguments sont nuls, il retournera true , et si un seul d'entre eux est nul , il retournera false .

Cela peut être très pratique. Disons que nous voulons ajouter une date de naissance facultative à notre classe Person :

public Person(String firstName, String lastName, LocalDate birthDate) { this(firstName, lastName); this.birthDate = birthDate; }

Ensuite, nous devrons mettre à jour notre méthode equals () mais avec une gestion nulle . Nous pourrions le faire en ajoutant cette condition à notre méthode equals () :

birthDate == null ? that.birthDate == null : birthDate.equals(that.birthDate);

Cependant, si nous ajoutons de nombreux champs Nullable à notre classe, cela peut devenir vraiment compliqué. L'utilisation de la méthode Objects # equals dans notre implémentation equals () est beaucoup plus propre et améliore la lisibilité:

Objects.equals(birthDate, that.birthDate);

5. Interface comparable

Comparison logic can also be used to place objects in a specific order. The Comparable interface allows us to define an ordering between objects, by determining if an object is greater, equal, or lesser than another.

The Comparable interface is generic and has only one method, compareTo(), which takes an argument of the generic type and returns an int. The returned value is negative if this is lower than the argument, 0 if they are equal, and positive otherwise.

Let's say, in our Person class, we want to compare Person objects by their last name:

public class Person implements Comparable { //... @Override public int compareTo(Person o) { return this.lastName.compareTo(o.lastName); } }

The compareTo() method will return a negative int if called with a Person having a greater last name than this, zero if the same last name, and positive otherwise.

For more information, take a look at our article about this topic.

6. Comparator Interface

The Comparator interface is generic and has a compare method that takes two arguments of that generic type and returns an integer. We already saw that pattern earlier with the Comparable interface.

Comparator is similar; however, it's separated from the definition of the class. Therefore, we can define as many Comparators we want for a class, where we can only provide one Comparable implementation.

Let's imagine we have a web page displaying people in a table view, and we want to offer the user the possibility to sort them by first names rather than last names. It isn't possible with Comparable if we also want to keep our current implementation, but we could implement our own Comparators.

Let's create a PersonComparator that will compare them only by their first names:

Comparator compareByFirstNames = Comparator.comparing(Person::getFirstName);

Let's now sort a List of people using that Comparator:

Person joe = new Person("Joe", "Portman"); Person allan = new Person("Allan", "Dale"); List people = new ArrayList(); people.add(joe); people.add(allan); people.sort(compareByFirstNames); assertThat(people).containsExactly(allan, joe);

There are other methods on the Comparator interface we can use in our compareTo() implementation:

@Override public int compareTo(Person o) { return Comparator.comparing(Person::getLastName) .thenComparing(Person::getFirstName) .thenComparing(Person::getBirthDate, Comparator.nullsLast(Comparator.naturalOrder())) .compare(this, o); }

In this case, we are first comparing last names, then first names. Then, we compare birth dates but as they are nullable we must say how to handle that so we give a second argument telling they should be compared according to their natural order but with null values going last.

7. Apache Commons

Let's now take a look at the Apache Commons library. First of all, let's import the Maven dependency:

 org.apache.commons commons-lang3 3.10 

7.1. ObjectUtils#notEqual Method

First, let's talk about the ObjectUtils#notEqual method. It takes two Object arguments, to determine if they are not equal, according to their own equals() method implementation. It also handles null values.

Let's reuse our String examples:

String a = new String("Hello!"); String b = new String("Hello World!"); assertThat(ObjectUtils.notEqual(a, b)).isTrue(); 

It should be noted that ObjectUtils has an equals() method. However, that's deprecated since Java 7, when Objects#equals appeared

7.2. ObjectUtils#compare Method

Now, let's compare object order with the ObjectUtils#compare method. It's a generic method that takes two Comparable arguments of that generic type and returns an Integer.

Let's see that using Strings again:

String first = new String("Hello!"); String second = new String("How are you?"); assertThat(ObjectUtils.compare(first, second)).isNegative();

By default, the method handles null values by considering them as greater. It offers an overloaded version that offers to invert that behavior and consider them lesser, taking a boolean argument.

8. Guava

Now, let's take a look at Guava. First of all, let's import the dependency:

 com.google.guava guava 29.0-jre 

8.1. Objects#equal Method

Similar to the Apache Commons library, Google provides us with a method to determine if two objects are equal, Objects#equal. Though they have different implementations, they return the same results:

String a = new String("Hello!"); String b = new String("Hello!"); assertThat(Objects.equal(a, b)).isTrue();

Though it's not marked as deprecated, the JavaDoc of this method says that it should be considered as deprecated since Java 7 provides the Objects#equals method.

8.2. Comparison Methods

Now, the Guava library doesn't offer a method to compare two objects (we'll see in the next section what we can do to achieve that though), but it does provide us with methods to compare primitive values. Let's take the Ints helper class and see how its compare() method works:

assertThat(Ints.compare(1, 2)).isNegative();

As usual, it returns an integer that may be negative, zero, or positive if the first argument is lesser, equal, or greater than the second, respectively. Similar methods exist for all the primitive types, except for bytes.

8.3. ComparisonChain Class

Finally, the Guava library offers the ComparisonChain class that allows us to compare two objects through a chain of comparisons. We can easily compare two Person objects by the first and last names:

Person natalie = new Person("Natalie", "Portman"); Person joe = new Person("Joe", "Portman"); int comparisonResult = ComparisonChain.start() .compare(natalie.getLastName(), joe.getLastName()) .compare(natalie.getFirstName(), joe.getFirstName()) .result(); assertThat(comparisonResult).isPositive();

The underlying comparison is achieved using the compareTo() method, so the arguments passed to the compare() methods must either be primitives or Comparables.

9. Conclusion

Dans cet article, nous avons examiné les différentes façons de comparer des objets en Java. Nous avons examiné la différence entre la similitude, l'égalité et l'ordre. Nous avons également examiné les fonctionnalités correspondantes dans les bibliothèques Apache Commons et Guava.

Comme d'habitude, le code complet de cet article est disponible sur GitHub.