web-dev-qa-db-fra.com

Comment implémenter Comparable pour qu'il soit cohérent avec l'égalité d'identité

J'ai une classe pour laquelle l'égalité (selon equals()) doit être définie par l'identité de l'objet, c'est-à-dire this == other.

Je veux implémenter Comparable pour ordonner de tels objets (par exemple par une propriété getName()). Pour être cohérent avec equals(), compareTo() ne doit pas retourner 0, Même si deux objets ont le même nom.

Existe-t-il un moyen de comparer les identités d'objets au sens de compareTo? Je pourrais comparer System.identityHashCode(o), mais cela retournerait quand même 0 En cas de collisions de hachage.

18
Balz Guenat

Je pense que la vraie réponse ici est: n'implémentez pas Comparable alors. L'implémentation de cette interface implique que vos objets ont un ordre naturel. Les choses qui sont "égales" devraient être au même endroit lorsque vous suivez cette pensée.

Si c'est le cas, vous devriez utiliser un comparateur personnalisé ... mais même cela n'a pas beaucoup de sens. Si la chose qui définit un <b ... n'est pas autorisée à vous donner a == b (quand a et b sont "égaux" selon votre <relation), alors l'approche globale de la comparaison est rompue pour votre cas d'utilisation.

En d'autres termes: ce n'est pas parce que vous pouvez mettre du code dans une classe qui "en quelque sorte" donne ce que vous voulez ... que ce n'est pas une idée bonne de le faire.

Par définition, en attribuant à chaque objet un identifiant unique universel (UUID) (ou un identifiant unique global, (GUID)) en tant que propriété d'identité, l'UUID est comparable et cohérent avec des égaux. Java a déjà une classe UUID , et une fois généré, vous pouvez simplement utiliser la représentation sous forme de chaîne pour la persistance. La propriété dédiée garantira également que l'identité est stable dans toutes les versions/threads/machines. Vous pouvez également utiliser un ID incrémenté si vous avez une méthode pour vous assurer que tout obtient un ID unique, mais l'utilisation d'une implémentation UUID standard vous protégera contre les problèmes de fusion des ensembles et des systèmes parallèles générant des données en même temps.

Si vous utilisez autre chose pour le comparable, cela signifie qu'il est comparable d'une manière distincte de son identité/valeur. Vous devrez donc définir ce que signifie comparable pour cet objet et documenter cela. Par exemple, les personnes sont comparables par leur nom, leur date de naissance, leur hauteur ou une combinaison par ordre de priorité; le plus naturellement par son nom comme une convention (pour une recherche plus facile par les humains) qui est distincte si deux personnes sont la même personne. Vous devrez également accepter que la comparaison et les égaux sont disjoints car ils sont basés sur des choses différentes.

6
Tezra

Vous pouvez ajouter une deuxième propriété (disons int id ou long id) qui serait unique pour chaque instance de votre classe (vous pouvez avoir une variable de compteur static et l'utiliser pour initialiser le id dans votre constructeur).

Ensuite, votre méthode compareTo peut d'abord comparer les noms, et si les noms sont égaux, comparer les ids.

Étant donné que chaque instance a un id différent, compareTo ne renverra jamais 0.

5
Eran

Alors que je m'en tiens à ma réponse originale selon laquelle vous devriez utiliser une propriété UUID pour une configuration de comparaison/égalité stable et cohérente, je me suis dit que j'irais de l'avant avec une réponse à la question "jusqu'où pourriez-vous aller si vous étiez VRAIMENT paranoïaque et vouliez un identité unique garantie pour comparable ".

Fondamentalement, en bref, si vous ne faites pas confiance à l'unicité des UUID ou à l'unicité de l'identité, utilisez simplement autant d'UUID qu'il le faut pour prouver que Dieu conspire activement contre vous. (Notez que bien qu'il ne soit pas techniquement garanti de ne pas lever d'exception, avoir besoin de 2 UUID devrait être exagéré dans n'importe quel univers sain d'esprit.)

import Java.time.Instant;
import Java.util.ArrayList;
import Java.util.UUID;

public class Test implements Comparable<Test>{

    private final UUID antiCollisionProp = UUID.randomUUID();
    private final ArrayList<UUID> antiuniverseProp = new ArrayList<UUID>();

    private UUID getParanoiaLevelId(int i) {
        while(antiuniverseProp.size() < i) {
            antiuniverseProp.add(UUID.randomUUID());
        }

        return antiuniverseProp.get(i);
    }

    @Override
    public int compareTo(Test o) {
        if(this == o)
            return 0;

        int temp = System.identityHashCode(this) - System.identityHashCode(o);
        if(temp != 0)
            return temp;

        //If the universe hates you
        temp = this.antiCollisionProp.compareTo(o.antiCollisionProp);
        if(temp != 0)
            return temp;

        //If the universe is activly out to get you
        temp = System.identityHashCode(this.antiCollisionProp) - System.identityHashCode(o.antiCollisionProp);;
        if(temp != 0)
            return temp;

        for(int i = 0; i < Integer.MAX_VALUE; i++) {
            UUID id1 = this.getParanoiaLevelId(i);
            UUID id2 = o.getParanoiaLevelId(i);
            temp = id1.compareTo(id2);
            if(temp != 0)
                return temp;

            temp = System.identityHashCode(id1) - System.identityHashCode(id2);;
            if(temp != 0)
                return temp;
        }

        // If you reach this point, I have no idea what you did to deserve this
        throw new IllegalStateException("RAGNAROK HAS COME! THE MIDGARD SERPENT AWAKENS!");
    }

}
1
Tezra

En supposant qu'avec deux objets de même nom, si equals() renvoie false alors compareTo() ne doit pas retourner 0. Si c'est ce que vous voulez faire, alors ce qui suit peut vous aider:

  • Remplacez hashcode() et assurez-vous qu'il ne repose pas uniquement sur name
  • Implémentez compareTo() comme suit:
public void compareTo(MyObject object) {
    this.equals(object) ? this.hashcode() - object.hashcode() : this.getName().compareTo(object.getName());
}
0
Darshan Mehta

Vous avez des objets uniques, mais comme l'a dit Eran, vous devrez peut-être un code de compteur/rehash supplémentaire pour toutes les collisions.

private static Set<Pair<C, C> collisions = ...;

@Override
public boolean equals(C other) {
    return this == other;
}

@Override
public int compareTo(C other) {
    ...
    if (this == other) {
        return 0
    }
    if (super.equals(other)) {
        // Some stable order would be fine:
        // return either -1 or 1
        if (collisions.contains(new Pair(other, this)) {
            return 1;
        } else if (!collisions.contains(new Pair(this, other)) {
            collisions.add(new Par(this, other));
        }
        return 1;
    }
    ...
}

Alors, allez avec la réponse d'Eran ou mettez en question l'exigence comme telle .

  • On pourrait considérer que les frais généraux des comparaisons 0 non identiques sont négligeables.
  • On pourrait se pencher sur les fonctions de hachage idéales , si à un certain moment aucune instance n'est plus créée. Cela implique que vous avez une collection de toutes les instances.
0
Joop Eggen