web-dev-qa-db-fra.com

Est-il possible dans Java faire quelque chose comme Comparateur mais pour implémenter equals () et hashCode () personnalisés

J'ai un tableau d'objets et je veux le concaténer avec un autre tableau d'objets, sauf que les objets qui ont le même identifiant. Ces objets sont utilisés à de nombreux endroits du système et n'ont pas de code de hachage ou d'égaux implémentés. Donc je ne veux pas implémenter hashCode() et equals(), car j'ai peur de casser quelque chose quelque part dans le système où ces objets sont utilisés et je ne sais pas à ce sujet.

Je veux mettre tous ces objets dans un ensemble, mais faire en sorte que les objets utilisent des hashCode() et equals() personnalisés. Quelque chose comme personnalisé Comparator, mais pour égal.

52
dhblah

Oui, il est possible de faire une telle chose. Mais cela ne vous permettra pas de placer vos objets dans un HashMap, un HashSet, etc. En effet, les classes de collection standard attendent des objets clés qu'ils fournissent les méthodes equals et hashCode. (C'est ainsi qu'ils sont conçus pour fonctionner ...)

Alternatives:

  1. Implémentez une classe wrapper qui contient une instance de la classe réelle et fournit sa propre implémentation de equals et hashCode.

  2. Implémentez vos propres classes basées sur une table de hachage qui peuvent utiliser un objet "hachable" pour fournir des fonctionnalités d'égalité et de code de hachage.

  3. Mordez la puce et implémentez les remplacements equals et hashCode sur les classes pertinentes.

En fait, la 3e option est probablement la meilleure, car votre base de code est très probablement a besoin pour utiliser une notion cohérente de ce que cela signifie pour ces objets d'être égaux. Il y a d'autres choses qui suggèrent que votre code a besoin d'une refonte. Par exemple, le fait qu'il utilise actuellement un tableau d'objets au lieu d'une implémentation Set pour représenter ce qui est censé être un ensemble.

D'un autre côté, il y avait peut-être/est une raison réelle (ou imaginaire) de performance pour l'implémentation actuelle; par exemple. réduction de l'utilisation de la mémoire. Dans ce cas, vous devriez probablement écrire un tas de méthodes d'assistance pour effectuer des opérations telles que la concaténation de 2 ensembles représentés sous forme de tableaux.

34
Stephen C

Dans 90% des cas, lorsqu'un utilisateur souhaite une relation d'équivalence, il existe déjà une solution plus simple. Vous souhaitez dédupliquer un tas de choses basées uniquement sur les identifiants? Pouvez-vous simplement les mettre tous dans une carte avec les identifiants comme clés, puis obtenir la collection values() de cela?

13

HashingStrategy est le concept que vous recherchez. Il s'agit d'une interface de stratégie qui vous permet de définir des implémentations personnalisées d'égaux et de code de hachage.

public interface HashingStrategy<E>
{
    int computeHashCode(E object);
    boolean equals(E object1, E object2);
}

Comme d'autres l'ont souligné, vous ne pouvez pas utiliser un HashingStrategy avec le HashSet ou HashMap intégré. Collections Eclipse inclut un ensemble appelé UnifiedSetWithHashingStrategy et une carte appelée UnifiedMapWithHashingStrategy.

Regardons un exemple. Voici une simple classe Data que nous pouvons utiliser dans une UnifiedSetWithHashingStrategy.

public class Data
{
    private final int id;

    public Data(int id)
    {
        this.id = id;
    }

    public int getId()
    {
        return id;
    }

    // No equals or hashcode
}

Voici comment configurer un UnifiedSetWithHashingStrategy et l'utiliser.

Java.util.Set<Data> set =
  new UnifiedSetWithHashingStrategy<>(HashingStrategies.fromFunction(Data::getId));
Assert.assertTrue(set.add(new Data(1)));

// contains returns true even without hashcode and equals
Assert.assertTrue(set.contains(new Data(1)));

// Second call to add() doesn't do anything and returns false
Assert.assertFalse(set.add(new Data(1)));

Pourquoi ne pas simplement utiliser un Map? UnifiedSetWithHashingStrategy utilise la moitié de la mémoire d'un UnifiedMap et un quart de la mémoire d'un HashMap. Et parfois, vous n'avez pas de clé pratique et devez en créer une synthétique, comme un Tuple. Cela peut gaspiller plus de mémoire.

Comment effectuons-nous des recherches? N'oubliez pas que les ensembles ont contains(), mais pas get(). UnifiedSetWithHashingStrategy implémente Pool en plus de MutableSet, donc il implémente également une forme de get().

Remarque: je suis un committer pour les collections Eclipse.

10
Craig P. Motlin

Bien sûr, vous pouvez créer un objet externe fournissant une comparaison d'égalité et un HashCode. Mais les collections intégrées de Java n'utilisent pas un tel objet pour leurs comparaisons/recherches.

J'ai déjà créé une interface comme celle-ci dans ma collection de packages (juste fraîchement traduite en anglais):

public interface HashableEquivalenceRelation {

    /**
     * Returns true if two objects are considered equal.
     *
     * This should form an equivalence relation, meaning it
     * should fulfill these properties:
     *  <ul>
     *    <li>Reflexivity:  {@code areEqual(o, o)}
     *            should always return true.</li>
     *    <li>Symmetry: {@code areEqual(o1,o2) == areEqual(o2,o1)}
     *            for all objects o1 and o2</li>
     *    <li>Transitivity: If {@code areEqual(o1, o2)} and {@code areEqual(o2,o3)},
     *            then {@code areEqual(o1,o3}} should hold too.</li>
     *  </ul>
     * Additionally, the relation should be temporary consistent, i.e. the
     * result of this method for the same two objects should not change as
     * long as the objects do not change significantly (the precise meaning of
     * <em>change significantly</em> is dependent on the implementation).
     *
     * Also, if {@code areEqual(o1, o2)} holds true, then {@code hashCode(o1) == hashCode(o2)}
     * must be true too.
     */
    public boolean areEqual(Object o1, Object o2);

    /**
     * Returns a hashCode for an arbitrary object.
     *
     * This should be temporary consistent, i.e. the result for the same
     * objects should not change as long as the object does not change significantly
     * (with change significantly having the same meaning as for {@link areEqual}).
     *
     * Also, if {@code areEqual(o1, o2)} holds true, then {@code hashCode(o1) == hashCode(o2)}
     * must be true too.
     */
    public int hashCode(Object o);

}

Que j'avais un groupe d'interfaces CustomCollection, CustomSet, CustomList, CustomMap, etc. définies comme les interfaces dans Java.util, mais en utilisant une telle relation d'équivalence pour toutes les méthodes au lieu de la relation d'intégration donnée par Object.equals. J'ai également eu quelques implémentations par défaut:

/**
 * The equivalence relation induced by Object#equals.
 */
public final static EquivalenceRelation DEFAULT =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2)
        {
            return
                o1 == o2 ||
                o1 != null &&
                o1.equals(o2);
        }
        public int hashCode(Object ob)
        {
            return
                ob == null?
                0 :
                ob.hashCode();
        }
        public String toString() { return "<DEFAULT>"; }
    };

/**
 * The equivalence relation induced by {@code ==}.
 * (The hashCode used is {@link System#identityHashCode}.)
 */
public final static EquivalenceRelation IDENTITY =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2) { return o1 == o2; }
        public int hashCode(Object ob) { return System.identityHashCode(ob); }
        public String toString() { return "<IDENTITY>"; }
    };

/**
 * The all-relation: every object is equivalent to every other one.
 */
public final static EquivalenceRelation ALL =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2) { return true; }
        public int hashCode(Object ob) { return 0; }
        public String toString() { return "<ALL>"; }
    };

/**
 * An equivalence relation partitioning the references
 * in two groups: the null reference and any other reference.
 */
public final static EquivalenceRelation NULL_OR_NOT_NULL =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2)
        {
            return (o1 == null && o2 == null) ||
                (o1 != null && o2 != null);
        }
        public int hashCode(Object o) { return o == null ? 0 : 1; }
        public String toString() { return "<NULL_OR_NOT_NULL>"; }
    };

/**
 * Two objects are equivalent if they are of the same (actual) class.
 */
public final static EquivalenceRelation SAME_CLASS =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2)
        {
            return o1 == o2 || o1 != null && o2 != null &&
                o1.getClass() == o2.getClass();
        }
        public int hashCode(Object o) { return o == null ? 0 : o.getClass().hashCode(); }
        public String toString() { return "<SAME_CLASS>"; }
    };


/**
 * Compares strings ignoring case.
 * Other objects give a {@link ClassCastException}.
 */
public final static EquivalenceRelation STRINGS_IGNORE_CASE =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2)
        {
            return o1 == null ?
                o2 == null :
                ((String)o1).equalsIgnoreCase((String)o2);
        }
        public int hashCode(Object o)
        {
            return o == null ? -12345 : ((String)o).toUpperCase().hashCode();
        }
        public String toString() { return "<STRINGS_IGNORE_CASE>"; }
    };


/**
 * Compares {@link CharSequence} implementations by content.
 * Other object give a {@link ClassCastException}.
 */
public final static EquivalenceRelation CHAR_SEQUENCE_CONTENT =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2) 
        {
            CharSequence seq1 = (CharSequence)o1;
            CharSequence seq2 = (CharSequence)o2;
            if (seq1 == null ^ seq2 == null) // nur eins von beiden null
                return false;
            if (seq1 == seq2)   // umfasst auch den Fall null == null
                return true;
            int size = seq1.length();
            if (seq2.length() != size)
                return false;
            for (int i = 0; i < size; i++)
                {
                    if (seq1.charAt(i) != seq2.charAt(i))
                        return false;
                }
            return true;
        }
        /**
         * Entrspricht String.hashCode
         */
        public int hashCode(Object o)
        {
            CharSequence sequence = (CharSequence)o;
            if (sequence == null)
                return 0;
            int hash = 0;
            int size = sequence.length();
            for (int i = 0; i < size; i++)
                {
                    hash = hash * 31 + sequence.charAt(i);
                }
            return hash;
        }
    };
4
Paŭlo Ebermann

Est-ce que l'utilisation d'un TreeSet aiderait ici? Un TreeSet effectue en fait un ordre et un comportement basé sur Set à l'aide de compare/compareTo et vous permet de définir un comparateur personnalisé à utiliser i n l'un des constructeurs .

1
whaley

Je viens d'avoir ce problème et j'ai trouvé une solution simple. Je ne sais pas à quel point c'est gourmand en mémoire; Je suis sûr que les gens peuvent l'affiner en fin de compte.

Lorsque Comparator renvoie 0, les éléments correspondent.

public static <E> Set<E> filterSet(Set<E> set, Comparator<E> comparator){
    Set<E> output = new HashSet<E>();
    for(E eIn : set){
        boolean add = true;
        for(E eOut : output){
            if(comparator.compare(eIn, eOut) == 0){
                add = false;
                break;
            }
        }
        if(add) output.add(eIn);
    }
    return output;
}

Mon cas d'utilisation était que j'avais besoin de filtrer les URL en double, comme dans les URL qui pointent vers le même document. L'objet URL a une méthode samePage() qui retournera true si tout sauf le fragment est le même.

filtered = Misc.filterSet(filtered, (a, b) -> a.sameFile(b) ? 0 : 1);
1
ndm13

Vous ne réussirez pas à effectuer votre concaténation de déduplication avec un comparateur. Vraisemblablement, vous cherchez à faire quelque chose comme ça:

List<Object> list = new ArrayList<Object>();
list.addAll( a );
list.addAll( b );
Collections.sort( list, new MyCustomComparator() );

Le problème est que Comparator doit comparer non seulement pour égal/non-égal, mais aussi pour l'ordre relatif. Étant donné les objets x et y qui ne sont pas égaux, vous devez répondre si l'un est supérieur à l'autre. Vous ne pourrez pas faire cela, car vous n'essayez pas réellement de comparer les objets. Si vous ne donnez pas de réponse cohérente, vous enverrez l'algorithme de tri dans une boucle infinie.

J'ai une solution pour toi. Java a une classe appelée LinkedHashSet, dont la vertu est qu'elle n'autorise pas l'insertion de doublons, mais maintient l'ordre d'insertion. Plutôt que d'implémenter un comparateur, implémentez une classe wrapper pour contenir l'objet réel et implémenter hashCode/equals.