Je travaille sur un projet où de nombreuses classes nécessitent des implémentations typiques appropriées de equals et hashCode: chaque classe a un ensemble de champs finaux initialisés à la construction avec des objets "profondément" immuables (null s sont destinés à être acceptés dans certains cas) pour être utilisés pour le hachage et la comparaison.

Pour réduire la quantité de code standard, j'ai pensé à écrire une classe abstraite fournissant des implémentations communes d'un tel comportement.

public abstract class AbstractHashable {

    /** List of fields used for comparison. */
    private final Object[] fields;

    /** Precomputed hash. */
    private final int hash;

    /**
     * Constructor to be invoked by subclasses.
     * @param fields list of fields used for comparison between objects of this
     * class, they must be in constant number for each class
     */
    protected AbstractHashable(Object... fields) {
        this.fields = fields;
        hash = 31 * getClass().hashCode() + Objects.hash(fields);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj == null || !getClass().equals(obj.getClass())) {
            return false;
        }
        AbstractHashable other = (AbstractHashable) obj;
        if (fields.length != other.fields.length) {
            throw new UnsupportedOperationException(
                    "objects of same class must have the same number of fields");
        }
        for (int i=0; i<fields.length; i++) {
            if (!fields[i].equals(other.fields[i])) {
                return false;
            }
        }
        return true;
    }

    @Override
    public int hashCode() {
        return hash;
    }

}

Ceci est destiné à être utilisé comme ceci:

public class SomeObject extends AbstractHashable {

    // both Foo and Bar have no mutable state
    private final Foo foo;
    private final Bar bar;

    public SomeObject(Foo foo, Bar bar) {
        super(foo, bar);
        this.foo = Objects.requireNonNull(foo);
        this.bar = bar; // null supported
    }

    // other methods, no equals or hashCode needed

}

C'est essentiellement ce que propose ici avec quelques différences.

Cela me semble une approche simple mais efficace pour réduire la verbosité tout en conservant des implémentations efficaces de equals et hashCode. Cependant, comme je ne me souviens pas avoir jamais vu quelque chose de similaire (sauf dans la réponse liée ci-dessus), je voudrais demander spécifiquement s'il y a un point contre cette approche (ou éventuellement une amélioration qui pourrait être appliquée), avant d'appliquer tout au long du projet.

5
rrobby86 25 janv. 2017 à 12:18

4 réponses

Meilleure réponse

Je vois déjà deux problèmes avec cette approche:

  1. Deux objets seront considérés comme égaux si et seulement si tous leurs champs correspondent. Ce n'est pas toujours le cas dans le monde réel.
  2. En créant vos classes extend depuis AbstractHashable, vous ne pouvez plus extend depuis d'autres classes. C'est un prix assez élevé à payer pour réutiliser equals et hashCode.

Le premier problème pourrait être résolu en transmettant plus de métadonnées à votre classe AbstractHashable qui lui permettent d'identifier les champs facultatifs. Par exemple, vous pouvez passer un autre tableau à AbstractHashTable qui contient des positions d'index à ignorer comme ses éléments via un setter. Le deuxième problème peut être résolu en utilisant Composition . Au lieu d'étendre AbstractHashTable, refactorisez-le pour qu'il puisse établir une relation HAS-A avec ses utilisateurs plutôt qu'une relation IS-A .

Cependant, comme je ne me souviens pas avoir jamais vu quelque chose de similaire (sauf dans la réponse liée ci-dessus), je voudrais spécifiquement demander s'il y a un point contre cette approche

Cette approche aura certainement un impact sur l'aspect lisibilité de votre code. Si vous pouvez trouver une approche plus lisible (par exemple en utilisant des annotations), je ne vois rien de mal à vouloir réutiliser les implémentations equals et hashCode.

Cela étant dit, les IDE modernes tels qu'eclipse peuvent facilement générer les implémentations equals et hashCode pour vous, alors est-il vraiment nécessaire de trouver une solution de ce type? Je crois non .

5
CKing 25 janv. 2017 à 10:03

Je suggérerais de jeter un coup d'œil à HashCodeBuilder et EqualsBuilder classes. Il a également des méthodes basées sur la réflexion telles que ReflectionHashCode et ReflectionEquals.

0
shailendra1118 26 janv. 2017 à 11:41

D'après mon expérience, cela semble mauvais car vous augmenterez l'erreur que vous pouvez obtenir au moment de l'exécution par rapport à la compilation (rappelez-vous que toute utilisation de ces objets dans des listes, etc. peut maintenant vous donner un comportement non explicité). Que faire si l'ordre des champs dans les classes est différent? Deuxièmement, abusez-vous de l'héritage? Maybee opter pour un cadre (https://projectlombok.org/) qui génère du hashcode et des égaux basés sur l'annotation?

0
HoleInVoid 25 janv. 2017 à 09:41

La solution devrait fonctionner.

Cependant, cela semble un peu faible du point de vue de la POO:

  • vous dupliquez des données (champs dans la superclasse vs champ dans l'objet), donc votre objet est deux fois plus grand; aussi si jamais vous avez besoin de les rendre mutables, vous devrez maintenir l'état à deux endroits
  • la hiérarchie des classes est forcée, uniquement pour fournir equals / hashCode

Les IDE modernes génèrent de telles méthodes assez facilement, si vous le souhaitez, vous pouvez également utiliser des frameworks (comme Lombok) ou des bibliothèques (comme les objets de Guava / les objets de Java 8) pour simplifier ces méthodes.

0
Adam Kotwasinski 25 janv. 2017 à 13:38