【问题标题】:Using Enum's ordinal value in switch-case statement在 switch-case 语句中使用 Enum 的序数值
【发布时间】:2011-10-11 13:36:20
【问题描述】:

对于我的项目,我正在使用枚举,我需要实现 switch-case 语句,其中检查特定枚举的值的序数,如下所示:

        switch ( variable )
        {
        case MyEnum.A.ordinal():
            return true;
        case MyEnum.B.ordinal():
            return true;
        default:
            return false;
        }

注意:返回值只是一个例子。

不幸的是,Eclipse(我使用的是 1.6 JDK)给出了我的编译错误“案例 表达式必须是常量表达式”。我应该怎么做?除了静态查找表之外还有其他方法吗,在这里描述:Convert from enum ordinal to enum type

【问题讨论】:

  • 我想我不明白为什么你必须使用序数。为什么不能只打开枚举本身?
  • 只是好奇,但是如果你有使用序数值的变量,你为什么要使用枚举?枚举的重点是它们的类型安全,但你的变量不会是类型安全的。无论如何,它可能就像将序数值设为公共常量属性而不是通过 getter 方法访问它一样简单。
  • @emory 因为我需要序数值 - 枚举值对于特定目的没有用,因为它们与 switch 语句的结果无关。
  • 我必须承认这对我来说毫无意义 - 枚举的值如何与开关值的结果无关?序数只是枚举值的索引(如果添加新的枚举值,索引可以更改) - 序数本质上只不过是枚举的瞬态属性。
  • @DoktorNo,顺便说一句,你问的是 EnumSet

标签: java enums switch-statement


【解决方案1】:

首先,你不应该那么依赖序数。如果可能的话,将您的变量设为String(并使用Enum.valueOf(string) 转换为enum,或者最好将其设为enum

如果实在不行,就用enum.values()[ordinal]。然后用开关中的枚举。

【讨论】:

  • 在这种情况下迭代很糟糕,你需要的是 enum.values()[variable] 。由于values() 克隆了数组,所以最好将它放在静态变量中。
  • 不客气,我的意思是,迭代完全违背了考虑性能的 case/switch 语句的目的。
  • 同意。我将它添加为最后一个选项(尽管看起来 OP 想要那个)
【解决方案2】:

只需使用枚举常量:

MyEnum variable;
...
switch ( variable ) {
    case A:
        return true;
    case B:
        return true;
    default:
        return false;
}

假设类似:

public enum MyEnum {
    A, B
}

但要小心NullPointerException(如果variablenull

【讨论】:

    【解决方案3】:

    您要求的可能是: 如果您需要在枚举本身的方法中进行切换:

    switch ( this )
            {
            case A:
                return true;
            case B:
                return true;
            default:
                return false;
            }
    

    在不同的班级:

    switch ( variable )  //Variable of type myEnum
            {
            case A:
                return true;
            case B:
                return true;
            default:
                return false;
            }
    

    如果你添加另一个枚举,很容易忘记更新 switch 语句,所以更好的办法是将这样的方法放在枚举本身中并使用特定于常量的方法实现:

    public enum MyEnum
        A { boolean getBooleanValue(){ return true; },
        B { boolean getBooleanValue(){ return true; },
        C { boolean getBooleanValue(){ return false; };
        abstract boolean getBooleanValue();
    }
    

    这样,如果你添加了一个新的枚举值,编译器会提醒你声明 getBooleanValue 方法,你只需在需要的地方使用A.getBooleanValue();

    正如 cmets 中所指出的,另一种选择是:

    public enum MyEnumAlt {
        A (true), 
        B (true),
        C (false);
        private final boolean isTrue;
        MyEnumAlt( boolean isTrue){ this.isTrue = isTrue; }
        boolean getBooleanValue(){ return isTrue; };
    }
    

    这是一个偏好问题,会因具体情况而异。如果您只是为每个枚举返回一个值,则构造函数版本是合理的,但我发现它的可读性较差。通过测试可以看出,对这种表现更好的担忧是没有根据的:

    public void testMain() {
            System.out.println("Testing with constructor: ");
            long start = System.currentTimeMillis();
            for(int i=0; i<1000*1000; i++){
                MyEnum tmpEnum = null;
                if(i%3==0){ tmpEnum = MyEnum.A;
                }else if(i%4==0){ tmpEnum = MyEnum.B;
                }else{ tmpEnum = MyEnum.C; }
                String tmp = Integer.toString(i)+" "+tmpEnum.getBooleanValue();
            }
            long time = System.currentTimeMillis()-start;
            System.out.println("Constructor version took "+time);
    
            System.out.println("Testing with Constant specific method implementation: ");
            long start2 = System.currentTimeMillis();
            for(int i=0; i<1000*1000; i++){
                MyEnumAlt tmpEnum2 = null;
                if(i%3==0){ tmpEnum2 = MyEnumAlt.A;
                }else if(i%4==0){ tmpEnum2 = MyEnumAlt.B;
                }else{ tmpEnum2 = MyEnumAlt.C; }
                String tmp2 = Integer.toString(i)+" "+tmpEnum2.getBooleanValue();
            }
            long time2 = System.currentTimeMillis()-start2;
            System.out.println("Constant specific method version took "+time2);
        }
    

    【讨论】:

    • 当然,您需要记住,打开枚举值并不是最好的主意,因为维护此代码并不容易。如果你添加一个新的枚举值,你需要记得更新所有的 switch 语句...
    • 在 Enum 中添加类似的函数会适得其反。第一:最好有一个单独的布尔字段和初始化枚举的c-tor(看起来更好,性能更好 - 内存/时间,每个像这样的常量都是一个新类);第二次添加更多依赖于枚举的案例(函数)涉及更改域模型,例如,如果您需要另一个返回布尔值的函数。这家伙只需要一个 EnumSet。
    • 视情况而定。如果它像返回特定值一样简单,那么是的,你可以只做一个构造函数和字段,但无论如何它看起来都非常相似。关于性能,每个枚举字段只有一个实例,热点编译器最终会内联大部分代码,所以我认为这段代码比尝试在枚举之外的方法中编写功能要好。任何一种情况下的性能差异都非常小,以至于在任何情况下都不会对 99.9% 的情况产生影响。
    • 我非常担心基准测试是排序的——另一个微基准测试不正确。基准测试需要预热,因为它的完成方式包括编译时间。第二次测试也可能包括一些垃圾收集。顺便说一句,MyEnum 无法编译。我确实重写了基准,常量在 40M 元素上稍快,基准在模数和字符串创建上花费的时间最多,因此需要非常高的数字才能产生任何效果。
    • 即使是行业标准的基准测试也常常不合时宜,听起来并不令人反感:azulsystems.com/events/javaone_2002/microbenchmarks.pdf
    【解决方案4】:

    这就是如何完成的,前提是你在某个地方有一个序列化的序数。保持枚举的通常方法是通过它的名称,而不是序数。此外,除非尝试实现 EnumMap/Set 之类的东西,否则您不应在正常情况下使用序数。当然,枚举可以只是 C 类东西的一个端口,处理不可避免的 int,需要转换为 Enum 对象。

    只要用Enum.values()得到一个ordinal()排序的数组就行了,因为每次都会克隆数组,保持一个ref指向就可以了。

    enum E{
     A, B, C...   
    }
    
    final static E[] vals = E.values();//copy the values(), calling values() clones the array
    boolean f(int variable){
      switch(vals[variable]){
      case A:
    ...
      case B:
    ...
    //break;
      default:
    ...
       }
    }
    

    刚刚注意到您只需要 true 和 false,这是一种 Set 类型的行为。你可以使用 java.util.EnumSet 或简单的long,如果你觉得勇敢(并且没有超过 64 个枚举常量)。例如:

    private static <E extends Enum> long ord(E e){
      return 1L<<e.ordinal();
    }
    
    static final long positiveSet = ord(E.A)+ord(E.B);
    boolean f(int ordinal){
      return 0!=(positiveSet&(1L<<ordinal));
    }
    

    【讨论】:

      【解决方案5】:

      答案针对@Riaan 对常量与方法枚举和性能原因的评论,它不直接回答 OP 问题,因此我认为它可以被视为噪音。 但是,我认为了解内部工作原理是一件重要的事情。

      我从他的示例中删除了基准并对其进行了改进,以消除占用超过 90% 执行时间的垃圾收集和字符串创建。添加了预热阶段以确保热点实际编译方法。

      还有更多,基准是有效的调用站点测试。调用站点的优化对于 1 来说是完全不同的,对于 2 来说更多一些和更多更多。调用点是对抽象(或只是被覆盖)方法的调用。

      下面是 6 个枚举常量的测试:

      package t1;
      
      public class ZEnums {
      
          public enum MyEnum {
        A { boolean getBooleanValue(){ return true; }},
        B { boolean getBooleanValue(){ return true; }},
        C { boolean getBooleanValue(){ return false; }},
        D { boolean getBooleanValue(){ return false; }},
        E { boolean getBooleanValue(){ return false; }},
        F { boolean getBooleanValue(){ return false; }}, 
      
        ;
        abstract boolean getBooleanValue();
        }
      
        public enum MyEnumAlt {
          A (true), 
          B (true),
          C (false),
          D (false),
          E (false),
          F (false),
          ;
          private final boolean isTrue;
          MyEnumAlt( boolean isTrue){ this.isTrue = isTrue; }
          boolean getBooleanValue(){ return isTrue; };
        }
      
        public static void main(String[] args) {
          log("Warming up...");
          //10k iterations won't do since not all paths for MyEnum are invoked 10k (default) times to warrant compilations 
          long warmum = testEnum(100000 )+ testAlt(100000)+testEnum(100000 )+ testAlt(100000);
          log("Warm up: %d", warmum);     
          //no info from +XX:+PrintCompilation below this one, or the test is invalid
          testMain();
      
          }
          public static void testMain() {
              int iterations = (int)4e7;
      
              log("Testing %d iterations%n", iterations);
              log("====");
      
              log("Testing with Overridden method...");       
          System.gc();
          {
          long start = System.currentTimeMillis();
          long len = 0;
          len = testEnum(iterations);
          long time = System.currentTimeMillis()-start;
          log("Overridden method version took %dms, length: %d ", time, len);
          }
      ////////////
          System.gc();
          {
          log("Testing with Constant in c-tor... ");
          long start = System.currentTimeMillis();
          long len = testAlt(iterations);
      
          long time = System.currentTimeMillis()-start;
          log("Constant in c-tor version took %dms, length: %d ", time, len);
          }
          }
          private static long testEnum(int iterations) {
              long len = 0;
              for(int i=0; i<iterations; i++){
              MyEnum tmpEnum = MyEnum.A;
              if(i%3==0){ tmpEnum = MyEnum.A;        
              }else if(i%4==0){ tmpEnum = MyEnum.B;
              }else if(i%5==0){ tmpEnum = MyEnum.C;
              }else if(i%6==0){ tmpEnum = MyEnum.D;
              }else if(i%6==0){ tmpEnum = MyEnum.E;
              }else{ tmpEnum = MyEnum.F; 
              }
              String tmp = tmpEnum.getBooleanValue()?"XXX":"ABCDE";
              len+=tmp.length();
          }
              return len;
          }
          private static long testAlt(int iterations) {
              long len =0;
              for(int i=0; i<iterations; i++){
              MyEnumAlt tmpEnum = MyEnumAlt.A;
              if(i%3==0){ tmpEnum = MyEnumAlt.A;
              }else if(i%4==0){ tmpEnum = MyEnumAlt.B;
              }else if(i%5==0){ tmpEnum = MyEnumAlt.C;
              }else if(i%6==0){ tmpEnum = MyEnumAlt.D;
              }else if(i%6==0){ tmpEnum = MyEnumAlt.E;
              }else{ tmpEnum = MyEnumAlt.F; 
              }
              String tmp = tmpEnum.getBooleanValue()?"XXX":"ABCDE";
              len+=tmp.length();
          }
              return len;
          }
          static void log(String msg, Object... params){ 
              String s = params.length>0?String.format(msg, params):msg;
              System.out.printf("%tH:%<tM:%<tS.%<tL %s%n", new Long(System.currentTimeMillis()), s);
          }
      }
      
      21:08:46.685 热身... 148 1% t1.ZEnums::testEnum @ 7 (125 字节) 150 1 t1.ZEnums$MyEnum$6::getBooleanValue (2 字节) 152 2 t1.ZEnums$MyEnum$1::getBooleanValue(2 字节) 154 3 t1.ZEnums$MyEnum$2::getBooleanValue(2 字节) 155 4 t1.ZEnums$MyEnum$3::getBooleanValue(2 字节) 158 2% t1.ZEnums::testAlt @ 7 (125 字节) 162 5 t1.ZEnums::testEnum (125 字节) 164 6 t1.ZEnums::testAlt (125 字节) 21:08:46.716 热身:1600000 21:08:46.716 测试 40000000 次迭代 21:08:46.716 ==== 21:08:46.716 使用覆盖方法进行测试... 21:08:47.513 重写方法版本耗时 781ms,长度:160000000 21:08:47.513 在 c-tor 中使用常量进行测试... 21:08:48.138 c-tor 版本中的常量耗时 625ms,长度:160000000

      代码使用-server -XX:+PrintCompilation 选项运行。 当然,差别并不大。然而,这不是有趣的问题。但是,如果您使用 2 个枚举常量测试版本,结果可能会大不相同。对于 2 个调用站点,编译器通过内联相关方法来生成代码。在上面的测试中,这将删除对 booleanValue 的整个调用,甚至可以在 O(1) 中执行测试。

      然而,最有趣的部分是当编译器开始使用内联缓存然后是常量时,从 2 到 3 个枚举常量,WOW 神奇地改变了一切。


      底线是:正确的基准测试确实很难,并且需要了解 JIT 如何编译、GC 何时可能成为问题(删除它或接受它)等等。
      链接

      【讨论】:

        【解决方案6】:

        更好的解决方案是这样的:

        枚举:

        public interface ACServices {
        
            public static enum MessageType {
        
                // periodic needs to saved in DB
                PIPE_INFO_TYPE_AC_DEVICE_LIST, // periodic from littlecloud
                PIPE_INFO_TYPE_DEV_ONLINE,
                PIPE_INFO_TYPE_DEV_OFFLINE,
                PIPE_INFO_TYPE_EVENT_LOG,
                PIPE_INFO_TYPE_DEV_DETAIL,
            };
        

        实现:

        ACServices.MessageType msgType = ACServices.MessageType.valueOf(acResponse.getType());
        switch (msgType){
            case INT_INFO_DEV_STATUS:
                break;
            case INT_INFO_DEV_TZ:
                break;
            case PIPE_INFO_DEV_COUNT:
                break;
            case PIPE_INFO_TYPE_AC_DEVICE_LIST:
                break;
            case PIPE_INFO_TYPE_CONFIG_GET_TEXT:
                break;
            default:
                break;
        }
        

        戴夫文柏宏 (manpakhong@hotmail.com)

        【讨论】:

          【解决方案7】:

          这是因为编译器看到了差异。比如这个枚举代码,我们可以看到:

          public enum TrafficLight {RED, YELLOW, GREEN}
          
          TrafficLight trafficLights = ...
          switch (trafficLights) {
            case RED: {/* do stuff */}
            case YELLOW: {/* do stuff */}
            case GREEN: {/* do stuff */}
          }
          

          但是编译器看到:

          switch (trafficLights.ordinal()) {
                case 0: {/* do stuff */}
                case 1: {/* do stuff */}
                case 2: {/* do stuff */}
              }
          

          这就是为什么当 trafficLights 为 NULL 时它会抛出 NPE,即使我们没有调用该方法,您也不知道为什么它会在 ordinal() 函数中抛出 NPE。

          SOLUTION 在 ENUM 到达 switch-case 之前检查它是否为 NULL。

          if (trafficLights != null) {
            switch (trafficLights) {
            case RED: {/* do stuff */}
            case YELLOW: {/* do stuff */}
            case GREEN: {/* do stuff */}
            }
          } 
          

          【讨论】:

            猜你喜欢
            • 2017-02-17
            • 1970-01-01
            • 1970-01-01
            • 2011-08-15
            • 2013-09-24
            • 2021-02-19
            • 2016-06-27
            • 2014-01-06
            • 1970-01-01
            相关资源
            最近更新 更多