Définition de Java Enum

151

Je pensais comprendre assez bien les génériques Java, mais je suis ensuite tombé sur ce qui suit dans java.lang.Enum:

class Enum<E extends Enum<E>>

Quelqu'un pourrait-il expliquer comment interpréter ce paramètre de type? Des points bonus pour fournir d'autres exemples d'utilisation d'un paramètre de type similaire.

Dónal
la source
9
Voici l'explication que j'aime le plus: Groking Enum (alias Enum & lt; E étend Enum & lt; E >>)
Alan Moore
Cette question a de meilleures réponses: stackoverflow.com/a/3068001/2413303
EpicPandaForce
jetez un œil à sitepoint.com/self-types-with-javas-generics
Arne Burmeister le

Réponses:

105

Cela signifie que l'argument de type pour enum doit dériver d'un enum qui a lui-même le même argument de type. Comment cela peut-il arriver? En faisant de l'argument de type le nouveau type lui-même. Donc, si j'ai une énumération appelée StatusCode, ce serait équivalent à:

public class StatusCode extends Enum<StatusCode>

Maintenant, si vous vérifiez les contraintes, nous avons Enum<StatusCode>- donc E=StatusCode. Vérifions: Es'étend-il Enum<StatusCode>? Oui! Nous allons bien.

Vous vous demandez peut-être à quoi cela sert :) Eh bien, cela signifie que l'API pour Enum peut se référer à elle-même - par exemple, être capable de dire qu'elle Enum<E>implémente Comparable<E>. La classe de base est capable de faire les comparaisons (dans le cas des énumérations) mais elle peut s'assurer qu'elle compare uniquement le bon type d'énumérations entre elles. (EDIT: Eh bien, presque - voir la modification en bas.)

J'ai utilisé quelque chose de similaire dans mon port C # de ProtocolBuffers. Il existe des «messages» (immuables) et des «générateurs» (mutables, utilisés pour construire un message) - et ils se présentent sous la forme de paires de types. Les interfaces concernées sont:

public interface IBuilder<TMessage, TBuilder>
  where TMessage : IMessage<TMessage, TBuilder> 
  where TBuilder : IBuilder<TMessage, TBuilder>

public interface IMessage<TMessage, TBuilder>
  where TMessage : IMessage<TMessage, TBuilder> 
  where TBuilder : IBuilder<TMessage, TBuilder>

Cela signifie qu'à partir d'un message, vous pouvez obtenir un constructeur approprié (par exemple pour prendre une copie d'un message et modifier certains bits) et d'un constructeur, vous pouvez obtenir un message approprié lorsque vous avez fini de le construire. C'est un bon travail que les utilisateurs de l'API n'ont pas besoin de se soucier de cela - c'est horriblement compliqué et il a fallu plusieurs itérations pour en arriver là.

EDIT: Notez que cela ne vous empêche pas de créer des types impairs qui utilisent un argument de type qui lui-même est correct, mais qui n'est pas du même type. Le but est de donner des avantages dans le bon cas plutôt que de vous protéger du mauvais cas.

Donc, si Enumvous n'avez pas été manipulé "spécialement" en Java de toute façon, vous pouvez (comme indiqué dans les commentaires) créer les types suivants:

public class First extends Enum<First> {}
public class Second extends Enum<First> {}

Secondmettrait en œuvre Comparable<First>plutôt que Comparable<Second>... mais Firstlui-même serait très bien.

Jon Skeet
la source
1
@artsrc: Je ne me souviens pas de pourquoi il doit être générique à la fois dans le générateur et dans le message. Je suis presque sûr que je n'aurais pas emprunté cette voie si je n'en avais pas eu besoin :)
Jon Skeet
1
@SayemAhmed: Oui, cela n'empêche pas cet aspect de mélanger les types. Je vais ajouter une note à ce sujet.
Jon Skeet
1
"J'ai utilisé quelque chose de similaire dans mon port C # de ProtocolBuffers." Mais c'est différent car les générateurs ont des méthodes d'instance qui retournent le type de paramètre de type. Enumn'a aucune méthode d'instance qui renvoie le type de paramètre de type.
newacct
1
@JonSkeet: Étant donné que les classes enum sont toujours générées automatiquement, je prétends que class Enum<E>c'est suffisant dans tous les cas. Et dans Generics, vous ne devez utiliser une limite plus restrictive que si cela est réellement nécessaire pour garantir la sécurité des types.
newacct
1
@JonSkeet: De plus, si les Enumsous - classes ne sont pas toujours générées automatiquement, la seule raison pour laquelle vous auriez besoin class Enum<E extends Enum<?>>plus class Enum<E>est la possibilité d'accéder ordinalà compareTo(). Cependant, si vous y réfléchissez, cela n'a pas de sens du point de vue du langage de vous permettre de comparer deux types différents d'énumérations via leurs ordinaux. Par conséquent, l'implémentation de Enum.compareTo()cette utilisation ordinaln'a de sens que dans le contexte de Enumsous - classes générées automatiquement. Si vous pouviez sous Enum- classer manuellement , compareTodevrait probablement l'être abstract.
newacct
27

Ce qui suit est une version modifiée de l'explication du livre Java Generics and Collections : Nous avons Enumdéclaré

enum Season { WINTER, SPRING, SUMMER, FALL }

qui sera étendu à une classe

final class Season extends ...

...doit être la classe de base paramétrée pour Enums. Voyons ce que cela doit être. Eh bien, l'une des conditions requises Seasonest qu'elle doit être mise en œuvre Comparable<Season>. Donc nous allons avoir besoin

Season extends ... implements Comparable<Season>

Que pourriez-vous utiliser pour ...que cela fonctionne? Étant donné qu'il doit s'agir d'un paramétrage de Enum, le seul choix est Enum<Season>, de sorte que vous puissiez avoir:

Season extends Enum<Season>
Enum<Season> implements Comparable<Season>

Ainsi Enumest paramétré sur des types comme Season. Abstrait de Seasonet vous obtenez que le paramètre de Enumest n'importe quel type qui satisfait

 E extends Enum<E>

Maurice Naftalin (co-auteur, Java Generics and Collections)

Maurice Naftalin
la source
1
@newacct OK, je comprends maintenant: vous aimeriez que toutes les énumérations soient des instances d'Enum <E>, non? (Parce que si ce sont des instances d'un sous-type Enum, l'argument ci-dessus s'applique.) Mais alors vous n'aurez plus d'énumérations de type sécurisé, vous perdez donc l'intérêt d'avoir des énumérations.
Maurice Naftalin
1
@newacct Vous ne voulez pas insister sur les Seasonimplémentations Comparable<Season>?
Maurice Naftalin
2
@newacct Regardez la définition de Enum. Afin de comparer une instance avec une autre, il doit comparer leurs ordinaux. Donc, l'argument de la compareTométhode doit avoir été déclaré comme un Enumsous-type, sinon le compilateur dira (correctement) qu'il n'a pas d'ordinal.
Maurice Naftalin
2
@MauriceNaftalin: Si Java n'interdisait pas le sous-classement manuel Enum, alors il serait possible de l'avoir class OneEnum extends Enum<AnotherEnum>{}, même avec comment Enumest déclaré maintenant. Il ne serait pas beaucoup de sens pour être en mesure de comparer un type de ENUM avec un autre, donc alors Enumest compareTone serait pas logique déclarée de toute façon. Les limites n'y apportent aucune aide.
newacct
2
@MauriceNaftalin: Si l'ordinal était la raison, alors public class Enum<E extends Enum<?>>cela suffirait également.
newacct
6

Ceci peut être illustré par un exemple simple et une technique qui peut être utilisée pour implémenter des appels de méthode chaînée pour des sous-classes. Dans un exemple ci - dessous setNamerenvoie un Nodeenchaînant donc ne fonctionnera pas pour le City:

class Node {
    String name;

    Node setName(String name) {
        this.name = name;
        return this;
    }
}

class City extends Node {
    int square;

    City setSquare(int square) {
        this.square = square;
        return this;
    }
}

public static void main(String[] args) {
    City city = new City()
        .setName("LA")
        .setSquare(100);    // won't compile, setName() returns Node
}

Nous pourrions donc référencer une sous-classe dans une déclaration générique, de sorte que le Cityrenvoie maintenant le type correct:

abstract class Node<SELF extends Node<SELF>>{
    String name;

    SELF setName(String name) {
        this.name = name;
        return self();
    }

    protected abstract SELF self();
}

class City extends Node<City> {
    int square;

    City setSquare(int square) {
        this.square = square;
        return self();
    }

    @Override
    protected City self() {
        return this;
    }

    public static void main(String[] args) {
       City city = new City()
            .setName("LA")
            .setSquare(100);                 // ok!
    }
}
Andrey Chaschev
la source
Votre solution a une distribution non vérifiée : pensez àreturn (CHILD) this; ajouter une méthode getThis (): protected CHILD getThis() { return this; }Voir: angelikalanger.com/GenericsFAQ/FAQSections
Roland
@Roland merci pour un lien, j'en ai emprunté une idée. Pourriez-vous m'expliquer ou vous diriger vers un article expliquant pourquoi est-ce une mauvaise pratique dans ce cas particulier? La méthode dans le lien nécessite plus de frappe et c'est le principal argument pour lequel j'évite cela. Je n'ai jamais vu d'erreurs de conversion dans ce cas + Je sais qu'il y a des erreurs de conversion inévitables - c'est-à-dire quand on stocke des objets de plusieurs types dans une même collection. Donc, si les moulages non vérifiés ne sont pas critiques et que la conception est quelque peu complexe (ce Node<T>n'est pas le cas), je les ignore pour gagner du temps.
Andrey Chaschev
Votre modification n'est pas si différente d'avant en plus d'ajouter un peu de sucre syntaxique, considérez que le code suivant compilera réellement mais générera une erreur d'exécution: `Node <City> node = new Node <City> () .setName (" node "). setSquare (1); `Si vous regardez le code d'octet java, vous verrez qu'en raison de l'effacement de type, l'instruction return (SELF) this;est compilée return this;, vous pouvez donc simplement la laisser de côté.
Roland
@Roland merci, c'est ce dont j'avais besoin - mettra à jour l'exemple quand je serai libre.
Andrey Chaschev
Le lien suivant est également bon: angelikalanger.com/GenericsFAQ/FAQSections/…
Roland
3

Vous n'êtes pas le seul à vous demander ce que cela signifie; voir le blog Chaotic Java .

«Si une classe étend cette classe, elle doit passer un paramètre E. Les limites du paramètre E sont pour une classe qui étend cette classe avec le même paramètre E».

kpirkkal
la source
1

Cet article m'a totalement clarifié le problème des «types génériques récursifs». Je voulais juste ajouter un autre cas où cette structure particulière est nécessaire.

Supposons que vous ayez des nœuds génériques dans un graphe générique:

public abstract class Node<T extends Node<T>>
{
    public void addNeighbor(T);

    public void addNeighbors(Collection<? extends T> nodes);

    public Collection<T> getNeighbor();
}

Ensuite, vous pouvez avoir des graphiques de types spécialisés:

public class City extends Node<City>
{
    public void addNeighbor(City){...}

    public void addNeighbors(Collection<? extends City> nodes){...}

    public Collection<City> getNeighbor(){...}
}
nozebacle
la source
Cela me permet toujours de créer un lieu class Foo extends Node<City>où Foo n'est pas lié à City.
newacct
1
Bien sûr, et est-ce faux? Je ne pense pas. Le contrat de base fourni par Node <City> est toujours respecté, seule votre sous-classe Foo est moins utile puisque vous commencez à travailler avec Foos mais que vous sortez Cities de l'ADT. Il peut y avoir un cas d'utilisation pour cela, mais très probablement plus simple et plus utile pour simplement rendre le paramètre générique identique à la sous-classe. Mais de toute façon, le concepteur a ce choix.
mdma
@mdma: Je suis d'accord. Alors, quelle utilité la borne fournit-elle, au-delà de juste class Node<T>?
newacct
1
@nozebacle: Votre exemple ne démontre pas que "cette structure particulière est nécessaire". class Node<T>est tout à fait conforme à votre exemple.
newacct
1

Si vous regardez le Enumcode source, il contient les éléments suivants:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {

    public final int compareTo(E o) {
        Enum<?> other = (Enum<?>)o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() && // optimization
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }

    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }

    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    } 
}

Tout d'abord, qu'est-ce que E extends Enum<E> signifie? Cela signifie que le paramètre type est quelque chose qui s'étend à partir de Enum, et n'est pas paramétré avec un type brut (il est paramétré par lui-même).

Ceci est pertinent si vous avez une énumération

public enum MyEnum {
    THING1,
    THING2;
}

qui, si je sais bien, se traduit par

public final class MyEnum extends Enum<MyEnum> {
    public static final MyEnum THING1 = new MyEnum();
    public static final MyEnum THING2 = new MyEnum();
}

Cela signifie donc que MyEnum reçoit les méthodes suivantes:

public final int compareTo(MyEnum o) {
    Enum<?> other = (Enum<?>)o;
    Enum<MyEnum> self = this;
    if (self.getClass() != other.getClass() && // optimization
        self.getDeclaringClass() != other.getDeclaringClass())
        throw new ClassCastException();
    return self.ordinal - other.ordinal;
}

Et plus important encore,

    @SuppressWarnings("unchecked")
    public final Class<MyEnum> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<MyEnum>)clazz : (Class<MyEnum>)zuper;
    }

Cela rend le getDeclaringClass()casting au bonClass<T> objet .

Un exemple bien plus clair est celui auquel j'ai répondu sur cette question où vous ne pouvez pas éviter cette construction si vous souhaitez spécifier une borne générique.

EpicPandaForce
la source
Rien de ce que vous avez montré compareToou ne getDeclaringClassnécessite la extends Enum<E>liaison.
newacct
0

Selon wikipedia, ce modèle est appelé modèle de modèle curieusement récurrent . Fondamentalement, en utilisant le modèle CRTP, nous pouvons facilement faire référence au type de sous-classe sans conversion de type, ce qui signifie qu'en utilisant le modèle, nous pouvons imiter la fonction virtuelle.

Hanzhou Tang
la source