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

Author: Miguel Moquillon

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

Commentaires (0)

Les commentaires sont fermés


Aucune annexe



À voir également

L'extensibilité d'objets métiers dans différents langages

Il arrive fréquemment qu’avec l’évolution des besoins dans le temps, les objets métiers qui ont été définis auparavant nécessitent d’être étendu par l’ajout de nouvelles fonctionnalités. Selon la nature des langages de programmation, mais aussi selon les caractéristiques propres aux langages, les méthodes d’extensions varient et peuvent être plus ou moins aisées à mettre en œuvre, en particulier lorsque les extensions sont fournies dans des modules (paquetages, bibliothèques, …) à part et que l’existant ne doit pas être impacté par ces ajouts (ou du moins le minimum possible). Dans ce petit billet je voudrais vous présenter certaines d’entre elles, et en particulier dans le contexte présenté ci-dessus, et ceci avec trois langages de programmations différents : Java, un langage impératif (orienté classe), Smalltalk, un des rares langages qui soient vraiment orienté objet, et Haskell, un langage fonctionnel.

Lire la suite

Foncteurs, Foncteurs Applicatifs et Monades

Dans ce premier billet de l’année 2018, je vais m’essayer de vous présenter ce que sont les foncteurs, les foncteurs applicatifs et les monades de façon simple, sans étalage de la théorie mathématique derrière (celle des catégories) dont, de toute manière, je ne maîtrise pas. Bien que ce soient des constructions utilisées dans la programmation fonctionnelle, elles peuvent aussi être utilisées dans d’autres approches de programmation et avec d’autres langages que ceux fonctionnels. C’est pourquoi je présenterai chacun des concepts non seulement avec du code en Haskell mais aussi en Java.

Lire la suite