¿Se pueden subclasificar las enumeraciones para agregar nuevos elementos?

535

Quiero tomar una enumeración existente y agregarle más elementos de la siguiente manera:

enum A {a,b,c}

enum B extends A {d}

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

¿Es esto posible en Java?

Miguel
fuente
12
Una razón para hacerlo es probar la situación en la que hay un valor de enumeración no válido sin introducir un valor de enumeración no válido en el origen del núcleo.
Arquímedes Trajano
Sí, un ejemplo de pureza "lingüística". Creo que lo que se desea es la idea de ahorro de trabajo "bookeeping" de un conjunto de enteros de incremento automático como uno tiene en C ++ para que pueda comenzar un nuevo conjunto como una extensión del conjunto anterior que comienza en el 1+ el último valor del conjunto anterior, y si las entradas se nombran heredan los nombres del "subconjunto común". Aunque la enumeración de Java tiene algunas cosas buenas al respecto, carece de la ayuda simple y automática de incremento automático que declara la ayuda que proporciona la enumeración de C ++.
Peter
44
En realidad, cuando amplías tu enumeración con nuevos valores, no estás creando una subclase, sino una superclase. Puede usar valores de enumeración base en todas partes en lugar de enumeración "extendida", pero no al revés, por lo que de acuerdo con el Principio de sustitución de Liskov, la enumeración extendida es la superclase de enumeración base.
Ilya
@Ilya ... sí, eso es cierto. Señalo que la pregunta tiene casos de uso definidos en el mundo real. Por motivo de la discusión, considere una base de Enumeración de: PrimaryColours; es razonable querer súper -class esto a Enum PrimaryAndPastelColoursmediante la adición de nuevos nombres de colores. Liskov sigue siendo el elefante en la habitación. ¿Por qué no comenzar con una base de enumeración de: AllMyColours- Y entonces un poder sub -class todos los colores a: PrimaryAndPastelColoursy, posteriormente, sub -class esto: PrimaryColours(manteniendo la jerarquía en cuenta). Sin embargo, Java tampoco lo permitirá.
será

Respuestas:

451

No, no puedes hacer esto en Java. Aparte de cualquier otra cosa, dpresumiblemente sería una instancia de A(dada la idea normal de "extensiones"), pero los usuarios que solo sabían sobre Aesto no lo sabrían, lo que frustra el punto de que un enum sea un conjunto bien conocido de valores.

Si pudiera contarnos más acerca de cómo desea usar esto, podríamos sugerir soluciones alternativas.

Jon Skeet
fuente
516
Todas las enumeraciones extienden implícitamente java.lang.Enum. Como Java no admite la herencia múltiple, una enumeración no puede extender nada más.
givanse
99
La razón por la que quiero extender es porque me gustaría tener una clase base llamada, por ejemplo, IntEnum, que se ve así: stackoverflow.com/questions/1681976/enum-with-int-value-in-java/… . Entonces todas mis enumeraciones podrían extenderlo ... en este caso, solo me beneficiaría de la herencia y, por lo tanto, no tendría que duplicar este código de "enumeración basada en int" con frecuencia. Soy nuevo en Java y vengo de C #, y espero perder algo. Mi opinión actual es que las enumeraciones de Java son un dolor en comparación con C #.
Tyler Collier el
30
@ Tyler: las enumeraciones de C # son solo nombres asociados con números, sin validación automática ni nada . Las enumeraciones IMO son el único bit de Java que en realidad es mejor que C #.
Jon Skeet
21
No estoy de acuerdo con @JonSkeet aquí. En mi caso de uso, me gustaría separar toda la lógica desagradable en mi gran enumeración, y tener la lógica oculta, y definir una enumeración limpia que extienda la otra que está oculta. Las enumeraciones con mucha lógica superan la idea de tener variables limpias declaradas para que no tenga que declarar cientos de variables de cadenas estáticas para que una clase con 5 enumeraciones no se vuelva ilegible y demasiado grande en las líneas. No quiero que los otros desarrolladores se preocupen por copiar y pegar esa paz de código para el próximo proyecto tampoco y, en cambio, extiendan el base_enum ... tiene sentido para mí ...
mmm
43
@givanse ... no estoy de acuerdo contigo en el punto de la extensión implícita de java.lang.Enum es la causa de la no herencia ya que todas las clases en Java también heredan implícitamente la clase Object, pero pueden heredar alguna otra clase, ya que luego vendría en la jerarquía como en Object->A->Blugar deObject->A->B extends Object
mickeymoon
317

Las enumeraciones representan una enumeración completa de los posibles valores. Entonces la respuesta (inútil) es no.

Como ejemplo de un problema real, tome los días de semana, los días de fin de semana y, la unión, los días de la semana. Podríamos definir todos los días dentro de los días de la semana, pero no podríamos representar propiedades especiales para los días de la semana y los días de fin de semana.

Lo que podríamos hacer es tener tres tipos de enumeración con un mapeo entre días de la semana / días de fin de semana y días de la semana.

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;
}

Alternativamente, podríamos tener una interfaz abierta para el día de la semana:

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

O podríamos combinar los dos enfoques:

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 - tackline
fuente
20
¿No hay un problema con esto? Una instrucción switch no funcionará en una interfaz, pero funciona en una enumeración normal. No funciona con el tipo de interruptor mata una de las mejores cosas de las enumeraciones.
Manius
99
Estoy pensando que podría haber otro problema con esto. No hay igualdad entre Weekday.MON y DayOfWeek.MON. ¿No es ese el otro gran beneficio de las enumeraciones? No tengo una solución mejor, solo me doy cuenta de esto mientras trato de encontrar la mejor respuesta. La falta de poder usar == fuerza un poco la mano.
Snekse
2
@Crusader sí, esa es precisamente la compensación. Si desea algo expandible, no puede tener sentencias de cambio fijas, si desea un conjunto de valores conocidos fijos, tautológicamente no puede tener algo expandible.
djechlin
3
Al pasar de enum a interface, también pierde la llamada estática a los valores (). Esto dificulta la refactorización, especialmente si decide extender su enumeración y agregar la interfaz como una barrera de abstracción a una enumeración establecida.
Joshua Goldberg
44
Este enfoque de derivar una enumeración de una interfaz es utilizado por la API de Java 1.7, por ejemplo, java.nio.file.Files.write () toma una matriz de OpenOption como último argumento. OpenOption es una interfaz, pero cuando llamamos a esta función, generalmente pasamos una constante de enumeración StandardOpenOption, que se deriva de OpenOption. Esto tiene la ventaja de ser extensible, pero también tiene inconvenientes. La implementación adolece del hecho de que OpenOption es una interfaz. Crea un HashSet <OpenOption> a partir de la matriz pasada, cuando podría haber creado un EnumSet más eficiente en espacio y tiempo. Y no puede usar el interruptor.
Klitos Kyriacou
71

La solución recomendada para esto es el patrón de enumeración extensible .

Esto implica crear una interfaz y usarla donde actualmente usa la enumeración. Luego haga que la enumeración implemente la interfaz. Puede agregar más constantes haciendo que esa nueva enumeración también extienda la interfaz.

JodaStephen
fuente
Vale la pena mencionar su uso de un método de fábrica en la interfaz. Gran manera de compartir funcionalidades comunes entre Enums relacionados dado que extender no es una solución viable.
Tim Clemons
8
¿Puede proporcionar más detalles (código :)) sobre este patrón?
Dherik
3
Ese patrón no permite extender los valores de una enumeración. Cuál es el punto en la pregunta formulada.
Eria
55

Debajo de las cubiertas, su ENUM es solo una clase regular generada por el compilador. Esa clase generada se extiende java.lang.Enum. La razón técnica por la que no puede extender la clase generada es que la clase generada es final. Las razones conceptuales para que sea final se discuten en este tema. Pero agregaré la mecánica a la discusión.

Aquí hay una enumeración de prueba:

public enum TEST {  
    ONE, TWO, THREE;
}

El código resultante 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);
}

Posiblemente podría escribir esta clase por su cuenta y soltar el "final". Pero el compilador le impide extender "java.lang.Enum" directamente. Podrías decidir NO extender java.lang.Enum, pero entonces tu clase y sus clases derivadas no serían una instancia de java.lang.Enum ... ¡lo cual realmente no te importaría de ninguna manera!

ChrisCantrell
fuente
1
¿Qué está haciendo el bloque estático vacío? 'static {};'
hollín
1
No tiene código en él. El programa "javap" muestra el bloque vacío.
ChrisCantrell
Es extraño tenerlo allí si no está haciendo nada, ¿no?
hollín
44
¡Tienes razón! Mi error. NO es un bloque de código vacío. Si ejecuta "javap -c" verá el código real dentro del bloque estático. El bloque estático crea todas las instancias ENUM (UNA, DOS y TRES aquí). Lo siento por eso.
ChrisCantrell
1
Gracias por afirmar el hecho: porque java.lang.Enum se declara final.
Benjamin
26
enum A {a,b,c}
enum B extends A {d}
/*B is {a,b,c,d}*/

Se puede escribir como:

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

Cómo puede ser útil: Digamos que queremos algo como: Tenemos eventos y estamos usando enumeraciones. Esas enumeraciones se pueden agrupar por procesamiento similar. Si tenemos operación con muchos elementos, algunos eventos comienzan a funcionar, algunos son solo pasos y otros finalizan la operación. Para recopilar dicha operación y evitar un caso de cambio largo, podemos agruparlos como en el ejemplo y usar:

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

Ejemplo:

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      
}  

Agregue algunos más avanzados:

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),
(...)

En el caso anterior, si tenemos algún error (myEvent.is (State_StatusGroup.FAIL)), iterando por eventos anteriores podemos verificar fácilmente si debemos revertir la transferencia de dinero:

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

Puede ser útil para:

  1. incluyendo metadatos explícitos sobre lógica de procesamiento, menos para recordar
  2. implementando algunos de herencia múltiple
  3. no queremos usar estructuras de clase, ej. para enviar mensajes cortos de estado
Waldemar Wosiński
fuente
13

Aquí hay una forma en que descubrí cómo extender una enumeración a otra enumeración, es un enfoque muy directo:

Supongamos que tienes una enumeración con constantes comunes:

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;
    }
}

entonces puedes intentar hacer un manual extendido de esta manera:

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;
    }
}

por supuesto, cada vez que necesite extender una constante, debe modificar sus archivos SubEnum.

Juan pablo g
fuente
interesante, podríamos usar también la enumeración toString (), y al final comparar cadenas; y para usar el interruptor, solo tendríamos que lanzar el objeto a una enumeración conocida; el único problema sería que 2 desarrolladores extendieran y crearan una identificación de enumeración idéntica, y luego trataran de fusionar ambos códigos :), ahora creo que entiendo por qué la enumeración no debe ser extensible.
Acuario Power
11

En caso de que te lo hayas perdido, hay un capítulo en el excelente libro de Joshua Bloch " Java Effective, 2nd edition ".

  • Capítulo 6 - Enums y anotaciones
    • Artículo 34: emule enumeraciones extensibles con interfaces

Extraer aquí .

Solo la conclusión:

Una desventaja menor del uso de interfaces para emular enumeraciones extensibles es que las implementaciones no pueden heredarse de un tipo de enumeración a otro. En el caso de nuestro ejemplo de Operación, la lógica para almacenar y recuperar el símbolo asociado con una operación se duplica en BasicOperation y ExtendedOperation. En este caso no importa porque se duplica muy poco código. Si hubiera una mayor cantidad de funcionalidad compartida, podría encapsularla en una clase auxiliar o un método auxiliar estático para eliminar la duplicación de código.

En resumen, aunque no puede escribir un tipo de enumeración extensible, puede emularlo escribiendo una interfaz que vaya con un tipo de enumeración básico que implemente la interfaz. Esto permite a los clientes escribir sus propias enumeraciones que implementan la interfaz. Estas enumeraciones se pueden usar siempre que se pueda usar el tipo de enumeración básico, suponiendo que las API se escriban en términos de la interfaz.

Guillaume Husta
fuente
6

Tiendo a evitar las enumeraciones, porque no son extensibles. Para seguir con el ejemplo del OP, si A está en una biblioteca y B en su propio código, no puede extender A si es una enumeración. Así es como a veces reemplazo las enumeraciones:

// 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; };
}

Hay algunos hoyos para evitar, vea los comentarios en el código. Dependiendo de sus necesidades, esta es una alternativa sólida y extensible a las enumeraciones.

sulai
fuente
1
puede estar bien si solo necesita algún ordinal para las instancias. Pero las enumeraciones también tienen una propiedad de nombre que es bastante útil.
desde el
6

Así es como mejoro el patrón de herencia enum con la verificación de tiempo de ejecución en el inicializador estático. Las BaseKind#checkEnumExtendercomprobaciones de que la "extensión" enum declara todos los valores de la base enum exactamente de la misma manera #name()y #ordinal()siguen siendo totalmente compatibles.

Todavía hay que copiar y pegar para declarar valores, pero el programa falla rápidamente si alguien agrega o modifica un valor en la clase base sin actualizar los que se extienden.

Comportamiento común para diferentes enumeraciones que se extienden entre sí:

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() ;
}

Base enum, con método de verificación:

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 ) ;
    }
  }

}

Muestra de extensión:

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
fuente
4

Basado en @Tom Hawtin - respuesta tackline agregamos soporte de interruptor,

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
fuente
¿De qué sirve el valueOf()método?
Axel Advento
@AxelAdvento La idea aquí es que dependemos de la interfaz Dayque tiene el método valueOf()a continuación switch(Day.valueOf()), se implementa mediante WeekDay, WeekEndDayenumeraciones.
Khaled Lela
3

Le sugiero que tome el enfoque inverso.

En lugar de extender la enumeración existente, cree una más grande y cree un subconjunto de la misma. Por ejemplo, si tenía una enumeración llamada PET y desea extenderla a ANIMAL, debe hacer esto en su lugar:

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

Tenga cuidado, las mascotas no son colecciones inmutables, es posible que desee utilizar Guava o Java9 para mayor seguridad.

Guillaume Robbe
fuente
2

Habiendo tenido este mismo problema, me gustaría publicar mi perspectiva. Creo que hay un par de factores motivadores para hacer algo como esto:

  • Desea tener algunos códigos de enumeración relacionados, pero en diferentes clases. En mi caso, tenía una clase base con varios códigos definidos en una enumeración asociada. En una fecha posterior (¡hoy!), Quería proporcionar una nueva funcionalidad a la clase base, lo que también significaba nuevos códigos para la enumeración.
  • La clase derivada admitiría tanto la enumeración de las clases base como la suya. ¡No hay valores de enumeración duplicados! Entonces: cómo tener una enumeración para la subclase que incluye las enumeraciones de su padre junto con sus nuevos valores.

El uso de una interfaz realmente no es suficiente: accidentalmente puede obtener valores de enumeración duplicados. No deseable.

Terminé simplemente combinando las enumeraciones: esto garantiza que no pueda haber valores duplicados, a expensas de estar menos estrechamente vinculado a su clase asociada. Pero, pensé que el problema duplicado era mi principal preocupación ...

dsummersl
fuente
2

Como ayuda para comprender por qué extender una Enum no es razonable en el nivel de implementación del lenguaje para considerar lo que sucedería si pasara una instancia de la Enum extendida a una rutina que solo comprende la Enum base. Un cambio que el compilador prometió tenía todos los casos cubiertos, de hecho, no cubriría esos valores extendidos de Enum.

Esto enfatiza aún más que los valores de Java Enum no son enteros como los de C, por ejemplo: para usar un Java Enum como un índice de matriz, debe solicitar explícitamente su miembro ordinal (), para darle a un Java Enum un valor entero arbitrario que debe agregar un campo explícito para eso y referencia a ese miembro nombrado.

Este no es un comentario sobre el deseo del OP, solo sobre por qué Java nunca lo va a hacer.

usuario2543191
fuente
1

Con la esperanza de que esta elegante solución de un colega mío se vea incluso en esta larga publicación, me gustaría compartir este enfoque para la subclasificación que sigue el enfoque de la interfaz y más allá.

Tenga en cuenta que aquí utilizamos excepciones personalizadas y este código no se compilará a menos que lo reemplace con sus excepciones.

La documentación es extensa y espero que sea comprensible para la mayoría de ustedes.

La interfaz que cada enumeración subclase necesita implementar.

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 implementación de la clase base ENUM.

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);
  }
}

El ENUM subclasificado que "hereda" de la clase 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);
  }
}

Finalmente el genérico ParameterImpl para agregar algunas utilidades.

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
fuente
0

Mi forma de codificar sería la siguiente:

// 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', '♯', '♭' }));
}

LinkedHashSetproporciona tanto que cada entrada solo existe una vez, y que su orden se conserva. Si el orden no importa, puedes usarlo HashSeten su lugar. El siguiente código no es posible 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.");
    }
}

El código se puede escribir de la siguiente manera:

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.");
    }
}

Desde Java 7 en adelante, incluso puede hacer lo mismo con 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");
}

Usando el reemplazo de enumeración:

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
fuente