Can enums be subclassed to add new elements?

前端 未结 15 1834
臣服心动
臣服心动 2020-11-22 12:02

I want to take an existing enum and add more elements to it as follows:

enum A {a,b,c}

enum B extends A {d}

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

Is this po

相关标签:
15条回答
  • 2020-11-22 12:29
    enum A {a,b,c}
    enum B extends A {d}
    /*B is {a,b,c,d}*/
    

    can be written as:

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

    How it can be useful: Let say we want something like: We have events and we are using enums. Those enums can be grouped by similar processing. If we have operation with many elements, then some events starts operation, some are just step and other end the operation. To gather such operation and avoid long switch case we can group them as in example and use:

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

    Example:

    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      
    }  
    

    Add some more advanced:

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

    At above if we have some fail (myEvent.is(State_StatusGroup.FAIL)) then iterating by previous events we can easily check if we must revert money transfer by:

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

    It can be useful for:

    1. including explicite meta-data about processing logic, less to remember
    2. implementing some of multi-inheritance
    3. we don't want to use class structures, ex. for sending short status messages
    0 讨论(0)
  • 2020-11-22 12:30

    Based on @Tom Hawtin - tackline answer we add switch support,

    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:
    }
    
    0 讨论(0)
  • 2020-11-22 12:32

    I suggest you take the other way around approach.

    Instead of extending the existing enumeration, create a larger one and create a subset of it. For exemple if you had an enumeration called PET and you wanted to extend it to ANIMAL you should do this instead:

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

    Be careful, pets is not an immutable collections, you might want to use Guava or Java9 for more safety.

    0 讨论(0)
  • 2020-11-22 12:35

    As an aid to understanding why extending an Enum is not reasonable at the language implementation level to consider what would happen if you passed an instance of the extended Enum to a routine that only understands the base Enum. A switch that the compiler promised had all cases covered would in fact not cover those extended Enum values.

    This further emphasizes that Java Enum values are not integers such as C's are, for instances: to use a Java Enum as an array index you must explicitly ask for its ordinal() member, to give a java Enum an arbitrary integer value you must add an explicit field for that and reference that named member.

    This is not a comment on the OP's desire, just on why Java ain't never going to do it.

    0 讨论(0)
  • 2020-11-22 12:36

    My way to code that would be as follows:

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

    LinkedHashSet provides both that each entry only exists once, and that their order is preserved. If order doesn’t matter, you can use HashSet instead. The following code is not possible in 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.");
        }
    }
    

    The code can be written as follows:

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

    From Java 7 onwards you can even do the same with 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");
    }
    

    Using the enum replacement:

    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.");
        }
    }
    
    0 讨论(0)
  • 2020-11-22 12:37

    In the hopes this elegant solution of a colleague of mine is even seen in this long post I'd like to share this approach for subclassing which follows the interface approach and beyond.

    Please be aware that we use custom exceptions here and this code won't compile unless you replace it with your exceptions.

    The documentation is extensive and I hope it's understandable for most of you.

    The interface that every subclassed enum needs to implement.

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

    The implementing ENUM base class.

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

    The subclassed ENUM which "inherits" from base class.

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

    Finally the generic ParameterImpl to add some utilities.

    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());
      }
    }
    
    0 讨论(0)
提交回复
热议问题