points de vue

les déambulations d'un codeur

Aller au contenu | Aller au menu | Aller à la recherche

L'égalité d'identité et l'égalité de valeur en Java

Dans certains langages de programmation, comme Java, il n’y a pas de méthodes ou d’opérateurs distincts entre l’égalité d’identité et celle de valeur. Si, dans un programme classique écrit dans un langage comme Java, l’égalité d’identité (de l’OID pour Object IDentifier) pourrait se faire avec l’opérateur == et celle de valeur avec la méthode equals surchargée, il n’en va plus de même dès qu’il s’agit d’objets persistés. Et là, in fine, c’est le drame : que compare t’on avec la méthode equals ? la valeur des objets ou leur identité ?

Cette question est importante car elle met en exergue une différence de sémantique qui, mal appréciée ou pas du tout appréhendée, pourrait aboutir à utiliser tantôt l’une, tantôt l’autre, aboutissant inévitablement à des bogues cachés, voir même, dans certaines situations d’usage, à un plantage.

Cette question est d’autant plus pertinente en Java où la méthode equals, par ses propres règles de codage, peut véhiculer les deux sémantiques. Je m’explique. Par défaut, la méthode equals d’un objet en Java effectue une égalité d’identité (qui se trouve être l’adresse mémoire de l’objet, comme dans les autres langages orientés objet). Nous pourrions par conséquence en déduire que la sémantique véhiculée par cette méthode est celle d’une égalité d’identité et donc nous y maintenir. Or, une des règles de codage en Java stipule que lorsqu’un objet peut être comparé, la méthode equals doit être consistante avec la méthode compareTo ; c’est-à-dire que :

myObject.compareTo(anotherObject) == 0 <=> myObject.equals(anotherObject) == true

Or la méthode compareTo effectue une comparaison de valeur ce qui fait que, par extension, la méthode equals devient alors une égalité de valeur !

A première vue, nous pourrions croire alors qu’il n’y a pas de sémantique bien définie avec la méthode equals. En fait, il en est rien. Sa sémantique est bien celle d’une égalité d’identité et le problème ici relève plus de l’utilisation de la comparaison entre objets. En effet, il est fréquent de rencontrer dans du code une implémentation de la méthode compareTo dès qu’une comparaison entre différents objets d’une même classe est à faire. Par exemple, dans le cas d’un tri. Or, ceci peut n’avoir aucun sens vis à vis du concept véhiculé par ces objets. Prenons un exemple. Imaginons que l’on souhaite trier des événements calendaires par leur date de début. Une des solutions pourrait être d’implémenter l’interface Comparable et sa méthode compareTo pour comparer les dates de début. Or ceci est une erreur conceptuelle ; on désire comparer ici des dates, pas les événements calendaires eux même. En effet, la méthode compareTo est une opération de comparaison de valeurs entre deux objets, autrement dit une opération de comparaison d’états entre deux objets. Or, la date de début d’un événement n’est qu’une caractéristique parmi d’autres de son état. Qu’en est-il de sa date de fin ? De sa récurrence ? De son intitulé ? etc. Comparer deux événements n’a de sens que pour indiquer s’ils sont in fine soit différents, soit égaux en valeur, et ça ce n’est pas la sémantique de la méthode compareTo. Pour comparer les événements par leur date de début, on utilisera plutôt un objet de type Comparator qui transportera lui la sémantique de comparaison que l’on voudra sur les événements et qui portera en fait sur une ou plusieurs de leurs propriétés.

Mais alors, qu’en est-il de l’intérêt de l’interface Comparable et de sa méthode compareTo ? Vous connaissez la classe String ? Cette classe qui représente une chaîne de caractères en Java est immutable et constante. Ceci signifie qu’une même chaîne de caractères est représentée par une même instance et ne peut être modifiée ; nous avons ici une égalité à la fois d’identité et de valeur. Lorsque vous comparez deux variables distinctes avec une même chaîne de caractères, en fait vous comparez derrière le même objet (les variables sont des références qui pointent ici sur la même instance). Si vous modifiez une chaîne de caractères, en fait, vous n’obtenez qu’une autre chaîne de caractères, donc une autre instance avec une autre valeur ; celle sur laquelle a été demandée la modification n’a pas changé. Et par conséquent, la méthode equals de String, donnant une égalité à la fois d’identité et de valeur, est consistante avec la méthode compareTo. C’est dans ce contexte que cette méthode prend tout son sens : lorsque l’identité et la valeur d’un objet sont intrinsèquement liés, autrement dit lorsque vos objets sont immutables.

En conséquence, la méthode equals en Java est une opération d’égalité d’identité et devrait le rester dans vos programmes. Si une égalité par valeur doit se faire entre deux de vos objets avec la méthode equals, alors soit vos objets doivent être immutables et vous pouvez surcharger la méthode equals en ce sens, soit vous devez définir une nouvelle méthode, comme par exemple sameAs.

Maintenant, les choses sont un tant soit peu un peu plus délicats avec les objets persistés. En effet, ce qui est stocké dans une source de données, quelle qu’elle soit (une source NoSQL ou une base de données), c’est l’état des objets, leur valeur, et en aucune manière les objets mêmes. (Ou alors vous disposez d’une source de données orientée objet et, mise à part dans le monde Smalltalk, je n’en connais aucune qui soit utilisée dans les autres écosystèmes de programmation.) Aussi, si par deux fois un même événement calendaire est demandé, le programme obtiendra, sans utilisation de caches, deux instances différentes de ce même événement et construites à partir d’une même valeur récupérée de la source de données. La méthode equals retournera donc, par défaut, false lorsqu’un bout de code voudra savoir s’il s’agit d’un même événement. Dans ce contexte, la sémantique d’égalité d’identité est brisée.

À la différence des objets à vie courte, les objets persistants ont une durée de vie qui va au delà du temps d’exécution du programme, voir même ont une vie au delà d’un processus donné. L’identité par adresse mémoire ne suffit donc plus. C’est là qu’intervient l’identité de persistance ou de stockage. En effet, cette identité assure l’unicité de l’objet dans la source de données et peut donc être utilisée aussi dans le programme pour marquer l’identité des objets en lieu et place de leur adresse mémoire. Pour aller plus loin et assurer aussi son unicité au delà des sources de données, l’idéal serait que cette identité soit une valeur unique et universelle (UUID). Ainsi, dès que vous devez manipuler des objets persistés, vous pouvez surcharger la méthode equals pour qu’elle compare l’égalité des identifiants de persistance des objets si ces derniers sont persistés, sinon leur valeur d’adresse si l’un d’eux ne l’est pas :

public boolean equals(Object other) {
  if (! other instanceof CalendarEvent) {
    return false;
  }
  CalendarEvent otherEvent = (CalendarEvent) other;
  if (this.getId() != null && otherEvent.getId() != null) {
    return this.getId().equals(otherEvent.getId());
  }
  return this == otherEvent;
}

(Nous pouvons aussi s’assurer ici que la méthode getId renvoie toujours une valeur sous forme de Value Object, soit le hash code dans le cas où l’objet n’est pas encore persisté, sinon l’identifiant de persistance.)

Miguel Moquillon

Auteur: Miguel Moquillon

Restez au courant de l'actualité et abonnez-vous au Flux RSS de cette catégorie

Commentaires (0)

Soyez le premier à réagir sur cet article

Ajouter un commentaire Fil des commentaires de ce billet

no attachment



À voir également

Exemple d'immutabilité en Java

Dans un billet précédent sur l’égalité d’identité et celle de valeurs, je vous ai parlé d’objets immuables pour lesquels l’égalité de valeur et l’égalité d’identité se confondent. J’ai souvent vu dans divers blogues sur l’immutabilité en Java l’utilisation du mot clé final. J’ai toujours trouvé son usage pour réaliser l’immutabilité comme absurde et surtout par trop contraignant. Pour moi, il ne sert à rien de qualifier les propriétés des objets comme final étant donné que celles-ci doivent être encapsulées selon les principes de la programmation orienté objet. Non, l’immutabilité des objets devrait au contraire se faire au niveau du comportement et surtout des mutateurs de ces objets.

Lire la suite

Une histoire d'objets obèses ...

Il arrive dans un projet en Java de se trouver, selon le métier ou le domaine adressé, avec des classes d’objets obèses en méthodes qu’elles soient publics ou propres aux objets de la classe. Or, sachant que l’on passe plus de temps à lire, voir à toucher du code existant qu’à en écrire de nouveaux, ceci peut vite devenir pénible. Evidemment, avec nos IDE actuels, il est facile de naviguer entre les différentes méthodes et propriétés d’une classe. Mais en général ceci signifie que l’on sait, déjà, à peu près ce que l’on cherche ou que l’on connait a minima les responsabilités ou certaines particularités d’implémentation de la classe. Lorsqu’on doit toucher du code inconnu ou au mieux revenir sur du code au bout de 6 mois, nous aimons bien identifier aisément les parties à utiliser ou à retoucher et accéder à l’essentiel sans se perdre dans les méandres de la ou des classes inspectées. En effet, il peut être difficile, avec de telles classes, de démêler le comportement de l’objet, ce qui le caractérise, du reste. En tout cas c’est mon cas. Pour éviter de tels embonpoints, je vous propose d’utiliser les approches de certains langages fonctionnels comme Haskell (ou OCaml), dans lesquels les types et les fonctions sur ces types sont séparés (au sein d’un même module tout de même).

Lire la suite