Les énumérations peuvent-elles être sous-classées pour ajouter de nouveaux éléments?

535

Je veux prendre une énumération existante et y ajouter plus d'éléments comme suit:

enum A {a,b,c}

enum B extends A {d}

/*B is {a,b,c,d}*/

Est-ce possible en Java?

Mike
la source
12
Une raison pour cela consiste à tester la situation où il existe une valeur d'énumération non valide sans introduire une valeur d'énumération non valide dans la source principale.
Archimedes Trajano
Oui un exemple de pureté "linguistique". Je pense que ce qui est souhaité est pour l'idée d'économie de main-d'œuvre "d'un ensemble incrémenté automatique d'entiers comme celui que l'on a en C ++ afin que vous puissiez démarrer un nouvel ensemble comme une extension de l'ancien ensemble commençant à 1+ la dernière valeur de la ensemble précédent, et si les entrées sont nommées, héritez les noms du "sous-ensemble commun". Bien que l'énumération java ait de bonnes choses à ce sujet, il lui manque l'aide de déclaration d'entier à incrémentation automatique simple fournie par l'énumération C ++.
peterk
4
En fait, lorsque vous étendez votre énumération avec de nouvelles valeurs, vous créez non pas une sous-classe, mais une superclasse. Vous pouvez utiliser des valeurs d'énumération de base partout au lieu de l'énumération "étendue", mais pas l'inverse, donc selon le principe de substitution de Liskov, l'énumération étendue est une superclasse de l'énumération de base.
Ilya
@Ilya ... oui c'est vrai. Je souligne que la question a des cas d'utilisation précis dans le monde réel. Par souci d'arguments, envisager une base de ENUM: PrimaryColours; il est raisonnable de vouloir super -class cela Enum PrimaryAndPastelColoursen ajoutant de nouveaux noms de couleurs. Liskov est toujours l'éléphant dans la pièce. Alors pourquoi ne pas commencer par une énumération de base de: AllMyColours- Et puis on pourrait sous- classer toutes les couleurs à: PrimaryAndPastelColourset ensuite sous- classer ceci à: PrimaryColours(en gardant la hiérarchie à l'esprit). Java ne le permettra pas non plus.
le

Réponses:

451

Non, vous ne pouvez pas faire cela en Java. En dehors de toute autre chose, dserait alors vraisemblablement une instance de A(étant donné l'idée normale de "s'étend"), mais les utilisateurs qui ne connaissaient que Ane le sauraient pas - ce qui va à l'encontre de l'énumération étant un ensemble bien connu de valeurs.

Si vous pouviez nous en dire plus sur la façon dont vous souhaitez l' utiliser , nous pourrions potentiellement suggérer des solutions alternatives.

Jon Skeet
la source
516
Toutes les énumérations étendent implicitement java.lang.Enum. Comme Java ne prend pas en charge l'héritage multiple, une énumération ne peut pas étendre autre chose.
givanse
9
La raison pour laquelle je veux étendre est parce que j'aimerais avoir une classe de base appelée par exemple IntEnum, qui ressemble à ceci: stackoverflow.com/questions/1681976/enum-with-int-value-in-java/… . Ensuite, toutes mes énumérations pourraient l'étendre ... dans ce cas, bénéficiant simplement de l'héritage et donc je n'aurais pas à dupliquer fréquemment ce code "énumération basée sur l'intégralité". Je suis nouveau sur Java et je viens de C #, et j'espère qu'il me manque quelque chose. Mon opinion actuelle est que les énumérations Java sont une douleur par rapport à C #.
Tyler Collier
30
@Tyler: les énumérations C # ne sont que des noms associés à des nombres, sans validation automatique ou quoi que ce soit . Les énumérations IMO sont le seul bit de Java qui est en fait meilleur que C #.
Jon Skeet
21
Pas d'accord avec @JonSkeet ici. Dans mon cas d'utilisation, je voudrais séparer toute la logique désagréable de mon grand énumération, avoir la logique cachée et définir une énumération propre qui étend l'autre qui est cachée. Des énumérations avec beaucoup de logique battent l'idée d'avoir des variables propres déclarées afin que vous n'ayez pas à déclarer des centaines de variables de chaînes statiques, donc une classe avec 5 énumérations ne devient pas illisible et trop grande en lignes. Je ne veux pas non plus que les autres développeurs se préoccupent de copier et de coller cette paix de code pour le prochain projet et d'étendre plutôt le base_enum ... cela a du sens pour moi ...
mmm
43
@givanse ... n'est pas d'accord avec vous sur le point de l'extension implicite de java.lang.Enum étant la cause du non-héritage car chaque classe en java hérite également implicitement de la classe Object mais elle peut hériter d'une autre classe comme elle viendrait ensuite dans la hiérarchie comme Object->A->Bau lieu deObject->A->B extends Object
mickeymoon
317

Les énumérations représentent une énumération complète des valeurs possibles. La réponse (inutile) est donc non.

Comme exemple de problème réel, prenez les jours de la semaine, les jours de week-end et, le syndicat, les jours de semaine. Nous pourrions définir tous les jours dans les jours de la semaine, mais nous ne serions pas en mesure de représenter des propriétés spéciales pour les jours de la semaine et les week-ends.

Ce que nous pourrions faire, c'est avoir trois types d'énumération avec une correspondance entre les jours de la semaine / week-end et les jours de la semaine.

public enum Weekday {
    MON, TUE, WED, THU, FRI;
    public DayOfWeek toDayOfWeek() { ... }
}
public enum WeekendDay {
    SAT, SUN;
    public DayOfWeek toDayOfWeek() { ... }
}
public enum DayOfWeek {
    MON, TUE, WED, THU, FRI, SAT, SUN;
}

Alternativement, nous pourrions avoir une interface ouverte pour le jour de la semaine:

interface Day {
    ...
}
public enum Weekday implements Day {
    MON, TUE, WED, THU, FRI;
}
public enum WeekendDay implements Day {
    SAT, SUN;
}

Ou nous pourrions combiner les deux approches:

interface Day {
    ...
}
public enum Weekday implements Day {
    MON, TUE, WED, THU, FRI;
    public DayOfWeek toDayOfWeek() { ... }
}
public enum WeekendDay implements Day {
    SAT, SUN;
    public DayOfWeek toDayOfWeek() { ... }
}
public enum DayOfWeek {
    MON, TUE, WED, THU, FRI, SAT, SUN;
    public Day toDay() { ... }
}
Tom Hawtin - sellerie
la source
20
N'y a-t-il pas un problème avec ça? Une instruction switch ne fonctionnera pas sur une interface, mais elle fonctionne sur une énumération régulière. Ne pas fonctionner avec un commutateur tue une des choses les plus agréables des énumérations.
Manius
9
Je pense qu'il pourrait y avoir un autre problème avec cela. Il n'y a pas d'égalité entre Weekday.MON et DayOfWeek.MON. N'est-ce pas là l'autre gros avantage des énumérations? Je n'ai pas de meilleure solution, je m'en rends compte alors que j'essaie de trouver la meilleure réponse. Le fait de ne pas pouvoir utiliser == force un peu la main.
Snekse
2
@Crusader oui, c'est précisément le compromis. Si vous voulez quelque chose d'extensible, vous ne pouvez pas avoir des instructions de commutation fixes, si vous voulez un ensemble de valeurs connues fixes, vous ne pouvez pas tautologiquement avoir quelque chose d'extensible.
djechlin
3
En passant de l'énumération à l'interface, vous perdez également l'appel statique à values ​​(). Cela rend la refactorisation difficile, surtout si vous décidez d'étendre votre énumération et d'ajouter l'interface en tant que barrière d'abstraction à une énumération établie.
Joshua Goldberg
4
Cette approche consistant à dériver une énumération à partir d'une interface est utilisée par l'API Java 1.7, par exemple java.nio.file.Files.write () prend un tableau d'OpenOption comme dernier argument. OpenOption est une interface, mais lorsque nous appelons cette fonction, nous passons généralement une constante d'énumération StandardOpenOption, qui est dérivée d'OpenOption. Cela a l'avantage d'être extensible, mais il présente également des inconvénients. L'implémentation souffre du fait qu'OpenOption est une interface. Il crée un HashSet <OpenOption> à partir du tableau passé, alors qu'il aurait pu créer un EnumSet plus efficace en termes d'espace et de temps. Et il ne peut pas utiliser de commutateur.
Klitos Kyriacou
71

La solution recommandée est le modèle d'énumération extensible .

Cela implique la création d'une interface et son utilisation là où vous utilisez actuellement l'énumération. Ensuite, faites enum implémenter l'interface. Vous pouvez ajouter plus de constantes en faisant en sorte que cette nouvelle énumération étende également l'interface.

JodaStephen
la source
Cela vaut la peine d'appeler leur utilisation d'une méthode d'usine dans l'interface. Excellent moyen de partager des fonctionnalités communes entre des énumérations liées, car l'extension n'est pas une solution viable.
Tim Clemons
8
Pouvez-vous fournir plus de détails (code :)) sur ce modèle?
Dherik
3
Ce modèle ne permet pas d'étendre les valeurs d'une énumération. Quel est le point de la question posée.
Eria
55

Sous les couvertures, votre ENUM n'est qu'une classe régulière générée par le compilateur. Cette classe générée s'étend java.lang.Enum. La raison technique pour laquelle vous ne pouvez pas étendre la classe générée est que la classe générée l'est final. Les raisons conceptuelles pour lesquelles il est définitif sont discutées dans cette rubrique. Mais je vais ajouter la mécanique à la discussion.

Voici une énumération de tests:

public enum TEST {  
    ONE, TWO, THREE;
}

Le code résultant de javap:

public final class TEST extends java.lang.Enum<TEST> {
  public static final TEST ONE;
  public static final TEST TWO;
  public static final TEST THREE;
  static {};
  public static TEST[] values();
  public static TEST valueOf(java.lang.String);
}

En théorie, vous pouvez taper cette classe par vous-même et supprimer la "finale". Mais le compilateur vous empêche d'étendre directement "java.lang.Enum". Vous pourriez décider de NE PAS étendre java.lang.Enum, mais votre classe et ses classes dérivées ne seraient pas une instance de java.lang.Enum ... ce qui pourrait ne pas vraiment vous intéresser!

ChrisCantrell
la source
1
Que fait le bloc statique vide? 'static {};'
soote
1
Il ne contient aucun code. Le programme "javap" montre le bloc vide.
ChrisCantrell
C'est étrange de l'avoir là-bas s'il ne fait rien, n'est-ce pas?
soote
4
Tu as raison! Mon erreur. Ce n'est PAS un bloc de code vide. Si vous exécutez "javap -c", vous verrez le code réel à l'intérieur du bloc statique. Le bloc statique crée toutes les instances ENUM (UN, DEUX et TROIS ici). Désolé pour ça.
ChrisCantrell
1
Merci d'avoir déclaré le fait: parce que java.lang.Enum est déclaré final.
Benjamin
26
enum A {a,b,c}
enum B extends A {d}
/*B is {a,b,c,d}*/

peut s'écrire:

public enum All {
    a       (ClassGroup.A,ClassGroup.B),
    b       (ClassGroup.A,ClassGroup.B),
    c       (ClassGroup.A,ClassGroup.B),
    d       (ClassGroup.B) 
...
  • ClassGroup.B.getMembers () contient {a, b, c, d}

Comment cela peut être utile: Disons que nous voulons quelque chose comme: Nous avons des événements et nous utilisons des énumérations. Ces énumérations peuvent être regroupées par un traitement similaire. Si nous avons une opération avec de nombreux éléments, certains événements démarrent l'opération, certains ne sont qu'une étape et d'autres mettent fin à l'opération. Pour rassembler une telle opération et éviter un long commutateur, nous pouvons les regrouper comme dans l'exemple et utiliser:

if(myEvent.is(State_StatusGroup.START)) makeNewOperationObject()..
if(myEnum.is(State_StatusGroup.STEP)) makeSomeSeriousChanges()..
if(myEnum.is(State_StatusGroup.FINISH)) closeTransactionOrSomething()..

Exemple:

public enum AtmOperationStatus {
STARTED_BY_SERVER       (State_StatusGroup.START),
SUCCESS             (State_StatusGroup.FINISH),
FAIL_TOKEN_TIMEOUT      (State_StatusGroup.FAIL, 
                    State_StatusGroup.FINISH),
FAIL_NOT_COMPLETE       (State_StatusGroup.FAIL,
                    State_StatusGroup.STEP),
FAIL_UNKNOWN            (State_StatusGroup.FAIL,
                    State_StatusGroup.FINISH),
(...)

private AtmOperationStatus(StatusGroupInterface ... pList){
    for (StatusGroupInterface group : pList){
        group.addMember(this);
    }
}
public boolean is(StatusGroupInterface with){
    for (AtmOperationStatus eT : with.getMembers()){
        if( eT .equals(this))   return true;
    }
    return false;
}
// Each group must implement this interface
private interface StatusGroupInterface{
    EnumSet<AtmOperationStatus> getMembers();
    void addMember(AtmOperationStatus pE);
}
// DEFINING GROUPS
public enum State_StatusGroup implements StatusGroupInterface{
    START, STEP, FAIL, FINISH;

    private List<AtmOperationStatus> members = new LinkedList<AtmOperationStatus>();

    @Override
    public EnumSet<AtmOperationStatus> getMembers() {
        return EnumSet.copyOf(members);
    }

    @Override
    public void addMember(AtmOperationStatus pE) {
        members.add(pE);
    }
    static { // forcing initiation of dependent enum
        try {
            Class.forName(AtmOperationStatus.class.getName()); 
        } catch (ClassNotFoundException ex) { 
            throw new RuntimeException("Class AtmEventType not found", ex); 
        }
    }
}
}
//Some use of upper code:
if (p.getStatus().is(AtmOperationStatus.State_StatusGroup.FINISH)) {
    //do something
}else if (p.getStatus().is(AtmOperationStatus.State_StatusGroup.START)) {
    //do something      
}  

Ajoutez un peu plus avancé:

public enum AtmEventType {

USER_DEPOSIT        (Status_EventsGroup.WITH_STATUS,
              Authorization_EventsGroup.USER_AUTHORIZED,
              ChangedMoneyAccountState_EventsGroup.CHANGED,
              OperationType_EventsGroup.DEPOSIT,
              ApplyTo_EventsGroup.CHANNEL),
SERVICE_DEPOSIT     (Status_EventsGroup.WITH_STATUS,
              Authorization_EventsGroup.TERMINAL_AUTHORIZATION,
              ChangedMoneyAccountState_EventsGroup.CHANGED,
              OperationType_EventsGroup.DEPOSIT,
              ApplyTo_EventsGroup.CHANNEL),
DEVICE_MALFUNCTION  (Status_EventsGroup.WITHOUT_STATUS,
              Authorization_EventsGroup.TERMINAL_AUTHORIZATION,
              ChangedMoneyAccountState_EventsGroup.DID_NOT_CHANGED,
              ApplyTo_EventsGroup.DEVICE),
CONFIGURATION_4_C_CHANGED(Status_EventsGroup.WITHOUT_STATUS,
              ApplyTo_EventsGroup.TERMINAL,
              ChangedMoneyAccountState_EventsGroup.DID_NOT_CHANGED),
(...)

Au-dessus si nous avons un échec (myEvent.is (State_StatusGroup.FAIL)) puis en itérant par les événements précédents, nous pouvons facilement vérifier si nous devons annuler le transfert d'argent en:

if(myEvent2.is(ChangedMoneyAccountState_EventsGroup.CHANGED)) rollBack()..

Il peut être utile pour:

  1. y compris des métadonnées explicites sur la logique de traitement, moins à retenir
  2. mettre en œuvre une partie de l'héritage multiple
  3. nous ne voulons pas utiliser les structures de classe, ex. pour l'envoi de courts messages d'état
Waldemar Wosiński
la source
13

Voici comment j'ai trouvé comment étendre un enum à un autre enum, c'est une approche très directe:

Supposons que vous ayez une énumération avec des constantes communes:

public interface ICommonInterface {

    String getName();

}


public enum CommonEnum implements ICommonInterface {
    P_EDITABLE("editable"),
    P_ACTIVE("active"),
    P_ID("id");

    private final String name;

    EnumCriteriaComun(String name) {
        name= name;
    }

    @Override
    public String getName() {
        return this.name;
    }
}

alors vous pouvez essayer de faire un manuel s’étend de cette façon:

public enum SubEnum implements ICommonInterface {
    P_EDITABLE(CommonEnum.P_EDITABLE ),
    P_ACTIVE(CommonEnum.P_ACTIVE),
    P_ID(CommonEnum.P_ID),
    P_NEW_CONSTANT("new_constant");

    private final String name;

    EnumCriteriaComun(CommonEnum commonEnum) {
        name= commonEnum.name;
    }

    EnumCriteriaComun(String name) {
        name= name;
    }

    @Override
    public String getName() {
        return this.name;
    }
}

bien sûr, chaque fois que vous devez étendre une constante, vous devez modifier vos fichiers SubEnum.

Juan Pablo G
la source
intéressant, nous pourrions également utiliser le très énumération toString (), et à la fin comparer les chaînes; et pour utiliser switch, il suffirait de convertir l'objet en une énumération connue; le seul problème serait que 2 développeurs étendent et créent un identifiant d'énumération identique, puis essaient de fusionner les deux codes :), je pense maintenant comprendre pourquoi l'énumération doit rester non extensible.
Aquarius Power
11

Au cas où vous l'auriez manqué, il y a un chapitre dans l'excellent livre de Joshua Bloch " Java Effective, 2nd edition ".

  • Chapitre 6 - Énumérations et annotations
    • Point 34: émuler des énumérations extensibles avec des interfaces

Extrayez ici .

Juste la conclusion:

Un inconvénient mineur de l'utilisation d'interfaces pour émuler des énumérations extensibles est que les implémentations ne peuvent pas être héritées d'un type d'énumération à un autre. Dans le cas de notre exemple d'opération, la logique de stockage et de récupération du symbole associé à une opération est dupliquée dans BasicOperation et ExtendedOperation. Dans ce cas, cela n'a pas d'importance car très peu de code est dupliqué. S'il y avait une plus grande quantité de fonctionnalités partagées, vous pouvez l'encapsuler dans une classe d'assistance ou une méthode d'assistance statique pour éliminer la duplication de code.

En résumé, bien que vous ne puissiez pas écrire un type d'énumération extensible, vous pouvez l'émuler en écrivant une interface pour aller avec un type d'énumération de base qui implémente l'interface. Cela permet aux clients d'écrire leurs propres énumérations qui implémentent l'interface. Ces énumérations peuvent ensuite être utilisées partout où le type d'énumération de base peut être utilisé, en supposant que les API sont écrites en termes d'interface.

Guillaume Husta
la source
6

J'ai tendance à éviter les énumérations, car elles ne sont pas extensibles. Pour rester avec l'exemple de l'OP, si A est dans une bibliothèque et B dans votre propre code, vous ne pouvez pas étendre A s'il s'agit d'une énumération. Voici comment je remplace parfois les énumérations:

// access like enum: A.a
public class A {
    public static final A a = new A();
    public static final A b = new A();
    public static final A c = new A();
/*
 * In case you need to identify your constant
 * in different JVMs, you need an id. This is the case if
 * your object is transfered between
 * different JVM instances (eg. save/load, or network).
 * Also, switch statements don't work with
 * Objects, but work with int.
 */
    public static int maxId=0;
    public int id = maxId++;
    public int getId() { return id; }
}

public class B extends A {
/*
 * good: you can do like
 * A x = getYourEnumFromSomeWhere();
 * if(x instanceof B) ...;
 * to identify which enum x
 * is of.
 */
    public static final A d = new A();
}

public class C extends A {
/* Good: e.getId() != d.getId()
 * Bad: in different JVMs, C and B
 * might be initialized in different order,
 * resulting in different IDs.
 * Workaround: use a fixed int, or hash code.
 */
    public static final A e = new A();
    public int getId() { return -32489132; };
}

Il y a quelques trous à éviter, voir les commentaires dans le code. Selon vos besoins, il s'agit d'une alternative solide et extensible aux énumérations.

sulai
la source
1
cela peut être bien si vous avez juste besoin d'un ordinal pour les instances. Mais les énumérations ont également une propriété de nom qui est assez utile.
2018
6

C'est ainsi que j'améliore le modèle d'héritage enum avec la vérification d'exécution dans l'initialiseur statique. La BaseKind#checkEnumExtendervérification que l'énumération "extensible" déclare toutes les valeurs de l'énumération de base exactement de la même manière #name()et #ordinal()reste entièrement compatible.

Il y a toujours du copier-coller impliqué pour déclarer des valeurs mais le programme échoue rapidement si quelqu'un a ajouté ou modifié une valeur dans la classe de base sans mettre à jour les extensions.

Comportement courant pour différentes énumérations s'étendant les unes aux autres:

public interface Kind {
  /**
   * Let's say we want some additional member.
   */
  String description() ;

  /**
   * Standard {@code Enum} method.
   */
  String name() ;

  /**
   * Standard {@code Enum} method.
   */
  int ordinal() ;
}

Enum de base, avec méthode de vérification:

public enum BaseKind implements Kind {

  FIRST( "First" ),
  SECOND( "Second" ),

  ;

  private final String description ;

  public String description() {
    return description ;
  }

  private BaseKind( final String description ) {
    this.description = description ;
  }

  public static void checkEnumExtender(
      final Kind[] baseValues,
      final Kind[] extendingValues
  ) {
    if( extendingValues.length < baseValues.length ) {
      throw new IncorrectExtensionError( "Only " + extendingValues.length + " values against "
          + baseValues.length + " base values" ) ;
    }
    for( int i = 0 ; i < baseValues.length ; i ++ ) {
      final Kind baseValue = baseValues[ i ] ;
      final Kind extendingValue = extendingValues[ i ] ;
      if( baseValue.ordinal() != extendingValue.ordinal() ) {
        throw new IncorrectExtensionError( "Base ordinal " + baseValue.ordinal()
            + " doesn't match with " + extendingValue.ordinal() ) ;
      }
      if( ! baseValue.name().equals( extendingValue.name() ) ) {
        throw new IncorrectExtensionError( "Base name[ " + i + "] " + baseValue.name()
            + " doesn't match with " + extendingValue.name() ) ;
      }
      if( ! baseValue.description().equals( extendingValue.description() ) ) {
        throw new IncorrectExtensionError( "Description[ " + i + "] " + baseValue.description()
            + " doesn't match with " + extendingValue.description() ) ;
      }
    }
  }


  public static class IncorrectExtensionError extends Error {
    public IncorrectExtensionError( final String s ) {
      super( s ) ;
    }
  }

}

Exemple d'extension:

public enum ExtendingKind implements Kind {
  FIRST( BaseKind.FIRST ),
  SECOND( BaseKind.SECOND ),
  THIRD( "Third" ),
  ;

  private final String description ;

  public String description() {
    return description ;
  }

  ExtendingKind( final BaseKind baseKind ) {
    this.description = baseKind.description() ;
  }

  ExtendingKind( final String description ) {
    this.description = description ;
  }

}
Laurent Caillette
la source
4

Basé sur @Tom Hawtin - réponse sur la ligne de touche, nous ajoutons la prise en charge du commutateur

interface Day<T> {
    ...
  T valueOf();
}

public enum Weekday implements Day<Weekday> {
    MON, TUE, WED, THU, FRI;
   Weekday valueOf(){
     return valueOf(name());
   }
}

public enum WeekendDay implements Day<WeekendDay> {
    SAT, SUN;
   WeekendDay valueOf(){
     return valueOf(name());
   }
}

Day<Weekday> wds = Weekday.MON;
Day<WeekendDay> wends = WeekendDay.SUN;

switch(wds.valueOf()){
    case MON:
    case TUE:
    case WED:
    case THU:
    case FRI:
}

switch(wends.valueOf()){
    case SAT:
    case SUN:
}
Khaled Lela
la source
Quelle est l'utilité de la valueOf()méthode?
Axel Advento
@AxelAdvento L'idée ici est que nous dépendons de l'interface Dayqui a valueOf()alors la méthode switch(Day.valueOf()), elle est implémentée par des WeekDay, WeekEndDayénumérations.
Khaled Lela
3

Je vous suggère d'adopter l'approche inverse.

Au lieu d'étendre l'énumération existante, créez-en une plus grande et créez-en un sous-ensemble. Par exemple, si vous aviez une énumération appelée PET et que vous vouliez l'étendre à ANIMAL, vous devriez le faire à la place:

public enum ANIMAL {
    WOLF,CAT, DOG
} 
EnumSet<ANIMAL> pets = EnumSet.of(ANIMAL.CAT, ANIMAL.DOG);

Attention, les animaux de compagnie ne sont pas des collections immuables, vous voudrez peut-être utiliser Guava ou Java9 pour plus de sécurité.

Guillaume Robbe
la source
2

Ayant moi-même eu ce même problème, je voudrais publier mon point de vue. Je pense qu'il y a quelques facteurs de motivation pour faire quelque chose comme ça:

  • Vous voulez avoir des codes énumérés liés, mais dans différentes classes. Dans mon cas, j'avais une classe de base avec plusieurs codes définis dans une énumération associée. À une date ultérieure (aujourd'hui!), Je voulais fournir de nouvelles fonctionnalités à la classe de base, ce qui signifiait également de nouveaux codes pour l'énumération.
  • La classe dérivée prendrait en charge à la fois l'énumération des classes de base et la sienne. Pas de valeurs d'énumération en double! Donc: comment avoir une énumération pour la sous-classe qui inclut l'énumération de son parent avec ses nouvelles valeurs.

L'utilisation d'une interface ne le coupe pas vraiment: vous pouvez accidentellement obtenir des valeurs d'énumération en double. Pas souhaitable.

J'ai fini par combiner les énumérations: cela garantit qu'il ne peut y avoir de valeurs en double, au détriment d'être moins étroitement lié à sa classe associée. Mais, je pensais que le problème des doublons était ma principale préoccupation ...

dsummersl
la source
2

Pour comprendre pourquoi l'extension d'une énumération n'est pas raisonnable au niveau de l'implémentation du langage, il faut considérer ce qui se passerait si vous passiez une instance de l'énumération étendue à une routine qui ne comprend que l'énumération de base. Un commutateur promis par le compilateur dans tous les cas ne couvrirait en fait pas ces valeurs Enum étendues.

Cela souligne en outre que les valeurs Java Enum ne sont pas des entiers tels que les C le sont, par exemple: pour utiliser un Java Enum comme index de tableau, vous devez explicitement demander son membre ordinal (), pour donner à java Enum une valeur entière arbitraire que vous devez ajouter un champ explicite pour cela et référence ce membre nommé.

Ce n'est pas un commentaire sur le désir du PO, juste sur la raison pour laquelle Java ne le fera jamais.

user2543191
la source
1

Dans l'espoir que cette élégante solution d'un de mes collègues soit même vue dans ce long post, je voudrais partager cette approche pour le sous-classement qui suit l'approche d'interface et au-delà.

Veuillez noter que nous utilisons des exceptions personnalisées ici et que ce code ne sera pas compilé à moins que vous ne le remplaciez par vos exceptions.

La documentation est vaste et j'espère qu'elle est compréhensible pour la plupart d'entre vous.

L'interface que chaque énumération sous-classée doit implémenter.

public interface Parameter {
  /**
   * Retrieve the parameters name.
   *
   * @return the name of the parameter
   */
  String getName();

  /**
   * Retrieve the parameters type.
   *
   * @return the {@link Class} according to the type of the parameter
   */
  Class<?> getType();

  /**
   * Matches the given string with this parameters value pattern (if applicable). This helps to find
   * out if the given string is a syntactically valid candidate for this parameters value.
   *
   * @param valueStr <i>optional</i> - the string to check for
   * @return <code>true</code> in case this parameter has no pattern defined or the given string
   *         matches the defined one, <code>false</code> in case <code>valueStr</code> is
   *         <code>null</code> or an existing pattern is not matched
   */
  boolean match(final String valueStr);

  /**
   * This method works as {@link #match(String)} but throws an exception if not matched.
   *
   * @param valueStr <i>optional</i> - the string to check for
   * @throws ArgumentException with code
   *           <dl>
   *           <dt>PARAM_MISSED</dt>
   *           <dd>if <code>valueStr</code> is <code>null</code></dd>
   *           <dt>PARAM_BAD</dt>
   *           <dd>if pattern is not matched</dd>
   *           </dl>
   */
  void matchEx(final String valueStr) throws ArgumentException;

  /**
   * Parses a value for this parameter from the given string. This method honors the parameters data
   * type and potentially other criteria defining a valid value (e.g. a pattern).
   *
   * @param valueStr <i>optional</i> - the string to parse the parameter value from
   * @return the parameter value according to the parameters type (see {@link #getType()}) or
   *         <code>null</code> in case <code>valueStr</code> was <code>null</code>.
   * @throws ArgumentException in case <code>valueStr</code> is not parsable as a value for this
   *           parameter.
   */
  Object parse(final String valueStr) throws ArgumentException;

  /**
   * Converts the given value to its external form as it is accepted by {@link #parse(String)}. For
   * most (ordinary) parameters this is simply a call to {@link String#valueOf(Object)}. In case the
   * parameter types {@link Object#toString()} method does not return the external form (e.g. for
   * enumerations), this method has to be implemented accordingly.
   *
   * @param value <i>mandatory</i> - the parameters value
   * @return the external form of the parameters value, never <code>null</code>
   * @throws InternalServiceException in case the given <code>value</code> does not match
   *           {@link #getType()}
   */
  String toString(final Object value) throws InternalServiceException;
}

La classe de base ENUM implémentée.

public enum Parameters implements Parameter {
  /**
   * ANY ENUM VALUE
   */
  VALUE(new ParameterImpl<String>("VALUE", String.class, "[A-Za-z]{3,10}"));

  /**
   * The parameter wrapped by this enum constant.
   */
  private Parameter param;

  /**
   * Constructor.
   *
   * @param param <i>mandatory</i> - the value for {@link #param}
   */
  private Parameters(final Parameter param) {
    this.param = param;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String getName() {
    return this.param.getName();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Class<?> getType() {
    return this.param.getType();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean match(final String valueStr) {
    return this.param.match(valueStr);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void matchEx(final String valueStr) {
    this.param.matchEx(valueStr);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Object parse(final String valueStr) throws ArgumentException {
    return this.param.parse(valueStr);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString(final Object value) throws InternalServiceException {
    return this.param.toString(value);
  }
}

ENUM sous-classé qui "hérite" de la classe de base.

public enum ExtendedParameters implements Parameter {
  /**
   * ANY ENUM VALUE
   */
  VALUE(my.package.name.VALUE);

  /**
   * EXTENDED ENUM VALUE
   */
  EXTENDED_VALUE(new ParameterImpl<String>("EXTENDED_VALUE", String.class, "[0-9A-Za-z_.-]{1,20}"));

  /**
   * The parameter wrapped by this enum constant.
   */
  private Parameter param;

  /**
   * Constructor.
   *
   * @param param <i>mandatory</i> - the value for {@link #param}
   */
  private Parameters(final Parameter param) {
    this.param = param;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String getName() {
    return this.param.getName();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Class<?> getType() {
    return this.param.getType();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean match(final String valueStr) {
    return this.param.match(valueStr);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void matchEx(final String valueStr) {
    this.param.matchEx(valueStr);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Object parse(final String valueStr) throws ArgumentException {
    return this.param.parse(valueStr);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString(final Object value) throws InternalServiceException {
    return this.param.toString(value);
  }
}

Enfin le ParameterImpl générique pour ajouter quelques utilitaires.

public class ParameterImpl<T> implements Parameter {
  /**
   * The default pattern for numeric (integer, long) parameters.
   */
  private static final Pattern NUMBER_PATTERN = Pattern.compile("[0-9]+");

  /**
   * The default pattern for parameters of type boolean.
   */
  private static final Pattern BOOLEAN_PATTERN = Pattern.compile("0|1|true|false");

  /**
   * The name of the parameter, never <code>null</code>.
   */
  private final String name;

  /**
   * The data type of the parameter.
   */
  private final Class<T> type;

  /**
   * The validation pattern for the parameters values. This may be <code>null</code>.
   */
  private final Pattern validator;

  /**
   * Shortcut constructor without <code>validatorPattern</code>.
   *
   * @param name <i>mandatory</i> - the value for {@link #name}
   * @param type <i>mandatory</i> - the value for {@link #type}
   */
  public ParameterImpl(final String name, final Class<T> type) {
    this(name, type, null);
  }

  /**
   * Constructor.
   *
   * @param name <i>mandatory</i> - the value for {@link #name}
   * @param type <i>mandatory</i> - the value for {@link #type}
   * @param validatorPattern - <i>optional</i> - the pattern for {@link #validator}
   *          <dl>
   *          <dt style="margin-top:0.25cm;"><i>Note:</i>
   *          <dd>The default validation patterns {@link #NUMBER_PATTERN} or
   *          {@link #BOOLEAN_PATTERN} are applied accordingly.
   *          </dl>
   */
  public ParameterImpl(final String name, final Class<T> type, final String validatorPattern) {
    this.name = name;
    this.type = type;
    if (null != validatorPattern) {
      this.validator = Pattern.compile(validatorPattern);

    } else if (Integer.class == this.type || Long.class == this.type) {
      this.validator = NUMBER_PATTERN;
    } else if (Boolean.class == this.type) {
      this.validator = BOOLEAN_PATTERN;
    } else {
      this.validator = null;
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean match(final String valueStr) {
    if (null == valueStr) {
      return false;
    }
    if (null != this.validator) {
      final Matcher matcher = this.validator.matcher(valueStr);
      return matcher.matches();
    }
    return true;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void matchEx(final String valueStr) throws ArgumentException {
    if (false == this.match(valueStr)) {
      if (null == valueStr) {
        throw ArgumentException.createEx(ErrorCode.PARAM_MISSED, "The value must not be null",
            this.name);
      }
      throw ArgumentException.createEx(ErrorCode.PARAM_BAD, "The value must match the pattern: "
          + this.validator.pattern(), this.name);
    }
  }

  /**
   * Parse the parameters value from the given string value according to {@link #type}. Additional
   * the value is checked by {@link #matchEx(String)}.
   *
   * @param valueStr <i>optional</i> - the string value to parse the value from
   * @return the parsed value, may be <code>null</code>
   * @throws ArgumentException in case the parameter:
   *           <ul>
   *           <li>does not {@link #matchEx(String)} the {@link #validator}</li>
   *           <li>cannot be parsed according to {@link #type}</li>
   *           </ul>
   * @throws InternalServiceException in case the type {@link #type} cannot be handled. This is a
   *           programming error.
   */
  @Override
  public T parse(final String valueStr) throws ArgumentException, InternalServiceException {
    if (null == valueStr) {
      return null;
    }
    this.matchEx(valueStr);

    if (String.class == this.type) {
      return this.type.cast(valueStr);
    }
    if (Boolean.class == this.type) {
      return this.type.cast(Boolean.valueOf(("1".equals(valueStr)) || Boolean.valueOf(valueStr)));
    }
    try {
      if (Integer.class == this.type) {
        return this.type.cast(Integer.valueOf(valueStr));
      }
      if (Long.class == this.type) {
        return this.type.cast(Long.valueOf(valueStr));
      }
    } catch (final NumberFormatException e) {
      throw ArgumentException.createEx(ErrorCode.PARAM_BAD, "The value cannot be parsed as "
          + this.type.getSimpleName().toLowerCase() + ".", this.name);
    }

    return this.parseOther(valueStr);
  }

  /**
   * Field access for {@link #name}.
   *
   * @return the value of {@link #name}.
   */
  @Override
  public String getName() {
    return this.name;
  }

  /**
   * Field access for {@link #type}.
   *
   * @return the value of {@link #type}.
   */
  @Override
  public Class<T> getType() {
    return this.type;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public final String toString(final Object value) throws InternalServiceException {
    if (false == this.type.isAssignableFrom(value.getClass())) {
      throw new InternalServiceException(ErrorCode.PANIC,
          "Parameter.toString(): Bad type of value. Expected {0} but is {1}.", this.type.getName(),
          value.getClass().getName());
    }
    if (String.class == this.type || Integer.class == this.type || Long.class == this.type) {
      return String.valueOf(value);
    }
    if (Boolean.class == this.type) {
      return Boolean.TRUE.equals(value) ? "1" : "0";
    }

    return this.toStringOther(value);
  }

  /**
   * Parse parameter values of other (non standard types). This method is called by
   * {@link #parse(String)} in case {@link #type} is none of the supported standard types (currently
   * String, Boolean, Integer and Long). It is intended for extensions.
   * <dl>
   * <dt style="margin-top:0.25cm;"><i>Note:</i>
   * <dd>This default implementation always throws an InternalServiceException.
   * </dl>
   *
   * @param valueStr <i>mandatory</i> - the string value to parse the value from
   * @return the parsed value, may be <code>null</code>
   * @throws ArgumentException in case the parameter cannot be parsed according to {@link #type}
   * @throws InternalServiceException in case the type {@link #type} cannot be handled. This is a
   *           programming error.
   */
  protected T parseOther(final String valueStr) throws ArgumentException, InternalServiceException {
    throw new InternalServiceException(ErrorCode.PANIC,
        "ParameterImpl.parseOther(): Unsupported parameter type: " + this.type.getName());
  }

  /**
   * Convert the values of other (non standard types) to their external form. This method is called
   * by {@link #toString(Object)} in case {@link #type} is none of the supported standard types
   * (currently String, Boolean, Integer and Long). It is intended for extensions.
   * <dl>
   * <dt style="margin-top:0.25cm;"><i>Note:</i>
   * <dd>This default implementation always throws an InternalServiceException.
   * </dl>
   *
   * @param value <i>mandatory</i> - the parameters value
   * @return the external form of the parameters value, never <code>null</code>
   * @throws InternalServiceException in case the given <code>value</code> does not match
   *           {@link #getClass()}
   */
  protected String toStringOther(final Object value) throws InternalServiceException {
    throw new InternalServiceException(ErrorCode.PANIC,
        "ParameterImpl.toStringOther(): Unsupported parameter type: " + this.type.getName());
  }
}
Dr4gon
la source
0

Ma façon de coder ce serait la suivante:

// enum A { a, b, c }
static final Set<Short> enumA = new LinkedHashSet<>(Arrays.asList(new Short[]{'a','b','c'}));

// enum B extends A { d }
static final Set<Short> enumB = new LinkedHashSet<>(enumA);
static {
    enumB.add((short) 'd');
    // If you have to add more elements:
    // enumB.addAll(Arrays.asList(new Short[]{ 'e', 'f', 'g', '♯', '♭' }));
}

LinkedHashSetprévoit à la fois que chaque entrée n'existe qu'une seule fois et que leur ordre est conservé. Si l'ordre n'a pas d'importance, vous pouvez utiliser à la HashSetplace. Le code suivant n'est pas possible en Java:

for (A a : B.values()) { // enum B extends A { d }
    switch (a) {
        case a:
        case b:
        case c:
            System.out.println("Value is: " + a.toString());
        break;
        default:
            throw new IllegalStateException("This should never happen.");
    }
}

Le code peut être écrit comme suit:

for (Short a : enumB) {
    switch (a) {
        case 'a':
        case 'b':
        case 'c':
            System.out.println("Value is: " + new String(Character.toChars(a)));
        break;
        default:
            throw new IllegalStateException("This should never happen.");
    }
}

À partir de Java 7, vous pouvez même faire de même avec String:

// enum A { BACKWARDS, FOREWARDS, STANDING }
static final Set<String> enumA = new LinkedHashSet<>(Arrays.asList(new String[] {
        "BACKWARDS", "FOREWARDS", "STANDING" }));

// enum B extends A { JUMP }
static final Set<String> enumB = new LinkedHashSet<>(enumA);
static {
    enumB.add("JUMP");
}

Utilisation du remplacement d'énumération:

for (String a : enumB) {
    switch (a) {
        case "BACKWARDS":
        case "FOREWARDS":
        case "STANDING":
            System.out.println("Value is: " + a);
        break;
        default:
            throw new IllegalStateException("This should never happen.");
    }
}
Matthias Ronge
la source