web-dev-qa-db-fra.com

Comment implémenter enum avec des génériques?

J'ai une interface générique comme celle-ci:

interface A<T> {
    T getValue();
}

Cette interface a des instances limitées, il serait donc préférable de les implémenter en tant que valeurs d'énumération. Le problème est que ces instances ont différents types de valeurs, j'ai donc essayé l'approche suivante mais elle ne compile pas:

public enum B implements A {
    A1<String> {
        @Override
        public String getValue() {
            return "value";
        }
    },
    A2<Integer> {
        @Override
        public Integer getValue() {
            return 0;
        }
    };
}

Une idée à ce sujet?

56
Zhao Yi

Tu ne peux pas. Java ne permet pas les types génériques sur les constantes enum. Ils sont autorisés sur les types enum, cependant:

public enum B implements A<String> {
  A1, A2;
}

Ce que vous pourriez faire dans ce cas, c'est avoir un type enum pour chaque type générique, ou 'faux' avoir un enum en faisant simplement une classe:

public class B<T> implements A<T> {
    public static final B<String> A1 = new B<String>();
    public static final B<Integer> A2 = new B<Integer>();
    private B() {};
}

Malheureusement, ils ont tous deux des inconvénients.

54
Jorn

En tant que Java concevant certaines API, nous rencontrons fréquemment ce problème. Je reconfirmais mes propres doutes lorsque je suis tombé sur ce message, mais j'ai une solution de contournement détaillée:

// class name is awful for this example, but it will make more sense if you
//  read further
public interface MetaDataKey<T extends Serializable> extends Serializable
{
    T getValue();
}

public final class TypeSafeKeys
{
    static enum StringKeys implements MetaDataKey<String>
    {
        A1("key1");

        private final String value;

        StringKeys(String value) { this.value = value; }

        @Override
        public String getValue() { return value; }
    }

    static enum IntegerKeys implements MetaDataKey<Integer>
    {
        A2(0);

        private final Integer value;

        IntegerKeys (Integer value) { this.value = value; }

        @Override
        public Integer getValue() { return value; }
    }

    public static final MetaDataKey<String> A1 = StringKeys.A1;
    public static final MetaDataKey<Integer> A2 = IntegerKeys.A2;
}

À ce stade, vous gagnez à être une valeur d’action enum vraiment constante (et tous les avantages qui vont avec), tout en étant une implémentation unique du interface, mais vous avoir l'accessibilité globale souhaitée par le enum.

De toute évidence, cela ajoute de la verbosité, ce qui crée le potentiel d'erreurs de copier/coller. Vous pouvez créer les enum s public et simplement ajouter une couche supplémentaire à leur accès.

Les conceptions qui ont tendance à utiliser ces fonctionnalités ont tendance à souffrir d'implémentations fragiles equals car elles sont généralement associées à une autre valeur unique, comme un nom, qui peut être dupliquée à son insu dans la base de code dans un but similaire mais différent. . En utilisant enum dans tous les domaines, l'égalité est un cadeau gratuit qui est immunisé contre un tel comportement fragile.

L'inconvénient majeur de tel que le système, au-delà de la verbosité, est l'idée de la conversion entre les clés uniques au monde (par exemple, le marshaling vers et depuis JSON). S'ils ne sont que des clés, ils peuvent être réinstanciés (dupliqués) en toute sécurité au détriment de la mémoire, mais en utilisant ce qui était auparavant une faiblesse - equals - comme avantage.

Il existe une solution à ce problème qui fournit l'unicité de l'implémentation globale en l'encombrant avec un type anonyme par instance globale:

public abstract class BasicMetaDataKey<T extends Serializable>
     implements MetaDataKey<T>
{
    private final T value;

    public BasicMetaDataKey(T value)
    {
        this.value = value;
    }

    @Override
    public T getValue()
    {
        return value;
    }

    // @Override equals
    // @Override hashCode
}

public final class TypeSafeKeys
{
    public static final MetaDataKey<String> A1 =
        new BasicMetaDataKey<String>("value") {};
    public static final MetaDataKey<Integer> A2 =
        new BasicMetaDataKey<Integer>(0) {};
}

Notez que chaque instance utilise une implémentation anonyme, mais rien d'autre n'est nécessaire pour l'implémenter, donc le {} sont vides. C'est à la fois déroutant et ennuyeux, mais cela fonctionne si les références d'instance sont préférables et l'encombrement est réduit au minimum, bien qu'il puisse être un peu cryptique pour les développeurs moins expérimentés Java Java, ce qui le rend plus difficile à maintenir.

Enfin, la seule façon de fournir l'unicité et la réaffectation globale est d'être un peu plus créatif avec ce qui se passe. L'utilisation la plus courante pour les interfaces partagées globalement que j'ai vu concerne les compartiments MetaData qui ont tendance à mélanger beaucoup de valeurs différentes, avec différents types (le T, sur une base par clé):

public interface MetaDataKey<T extends Serializable> extends Serializable
{
    Class<T> getType();
    String getName();
}

public final class TypeSafeKeys
{
    public static enum StringKeys implements MetaDataKey<String>
    {
        A1;

        @Override
        public Class<String> getType() { return String.class; }

        @Override
        public String getName()
        {
            return getDeclaringClass().getName() + "." + name();
        }
    }

    public static enum IntegerKeys implements MetaDataKey<Integer>
    {
        A2;

        @Override
        public Class<Integer> getType() { return Integer.class; }

        @Override
        public String getName()
        {
            return getDeclaringClass().getName() + "." + name();
        }
    }

    public static final MetaDataKey<String> A1 = StringKeys.A1;
    public static final MetaDataKey<Integer> A2 = IntegerKeys.A2;
}

Cela offre la même flexibilité que la première option, et il fournit un mécanisme pour obtenir une référence par réflexion, si cela devient nécessaire plus tard, évitant ainsi le besoin d'instanciables plus tard. Cela évite également beaucoup d'erreurs de copier/coller sujettes aux erreurs que la première option fournit, car elle ne se compilera pas si la première méthode est incorrecte, et la deuxième méthode n'a pas besoin de changer. La seule remarque est que vous devez vous assurer que les enum destinés à être utilisés de cette manière sont public pour éviter que quiconque n'ait des erreurs d'accès car il n'a pas accès à l'intérieur enum; si vous ne vouliez pas que ces MetaDataKey traversent un fil marshalé, alors les garder cachés des packages extérieurs pourrait être utilisé pour les supprimer automatiquement (pendant le marshaling, vérifiez de manière réfléchie pour voir si le enum est accessible, et si ce n'est pas le cas, ignorez la clé/valeur). Il n'y a rien à gagner ou à perdre en le rendant public, sauf en fournissant deux façons d'accéder à l'instance, si les références static les plus évidentes sont conservées (car les instances enum ne sont que cela en tous cas).

Je souhaite juste qu'ils aient fait en sorte que enum puisse étendre des objets en Java. Peut-être en Java 9?

La dernière option ne résout pas vraiment votre besoin, car vous demandiez des valeurs, mais je soupçonne que cela va vers l'objectif réel.

33
pickypg

Si JEP 301: Enumérations améliorées est accepté, puis vous pourrez utiliser une syntaxe comme celle-ci (tirée de la proposition):

enum Primitive<X> {
    INT<Integer>(Integer.class, 0) {
        int mod(int x, int y) { return x % y; }
        int add(int x, int y) { return x + y; }
    },
    FLOAT<Float>(Float.class, 0f)  {
        long add(long x, long y) { return x + y; }
    }, ... ;

    final Class<X> boxClass;
    final X defaultValue;

    Primitive(Class<X> boxClass, X defaultValue) {
        this.boxClass = boxClass;
        this.defaultValue = defaultValue;
    }
}
10
Tobogganski