【问题标题】:Does changing a enum to a class break binary compat是否将枚举更改为分类中断二进制兼容
【发布时间】:2018-07-08 14:58:03
【问题描述】:

如果我有这样的枚举:

public enum Test {
    TEST_VALUE(42), OTHER_TEST_VALUE(1337);

    private int val;
    Test(int val) {
        this.val = val;
    }

    public void increment() {
        val++;
    }

    public int getVal() {
        return val;
    }

如果我把它改成这样,我会破坏二进制兼容性吗:

public class Test {
    public static final Test TEST_VALUE = new Test(42);
    public static final Test OTHER_TEST_VALUE = new Test(1337);

    private int val;
    private final String oldEnumName;
    private Test(int val, String oldEnumName) {
        this.val = val;
        this.oldEnumName = oldEnumName;
    }

    public void increment() {
        val++;
    }

    public int getVal() {
        return val;
    }

    public String name() {
        return oldEnumName;
    }

   public static Test valueOf(String name)
   ...

并将所有其他枚举方法(如“值”和“序数”)添加到新类?那么其他jar不用重新编译就可以继续使用?

【问题讨论】:

  • 一方面,您的课程没有(也不能)扩展Enum
  • 我猜是的,但我不知道一个绝对的事实。不过,感觉测试它需要一两分钟。 (当然,像开关这样的特殊枚举结构会损坏。)
  • 你的构造函数不一样,所以不一样
  • @cricket_007 构造函数并不重要,因为它是私有的;其他 jar 无论如何都不会直接调用它。

标签: java class enums binary-compatibility


【解决方案1】:

我首先用这个枚举做了一些测试

public enum Test {
    TEST_VALUE(42), OTHER_TEST_VALUE(1337);

    private int val;
    Test(int val) {
        this.val = val;
    }

    public void increment() {
        val++;
    }

    public int getVal() {
        return val;
    }
}

然后用这个类来测试:

公共类 EnumTesting {

    public static void main(String[] args) {
        for (Test enumClass : Test.values()) {
            enumClass.increment();
        }
        System.out.println(Test.OTHER_TEST_VALUE.name() + Test.OTHER_TEST_VALUE.getVal());
        System.out.println(Test.TEST_VALUE.name() + Test.TEST_VALUE.getVal());
        System.out.println("For name : " + Test.valueOf("TEST_VALUE"));
        System.out.println(Test.OTHER_TEST_VALUE.ordinal());
    }
}

然后我编译了这些类,并复制了 EnumTesting 类以供将来参考。 我将测试枚举更改为:

 public class Test {
    private static int globalOrdinal = 0;
    public static final Test TEST_VALUE = new Test(42, "TEST_VALUE");
    public static final Test OTHER_TEST_VALUE = new Test(1337, "OTHER_TEST_VALUE");

    private int val;
    private final String oldEnumName;
    private int ordinal;

    private Test(int val, String oldEnumName) {
        this.val = val;
        this.oldEnumName = oldEnumName;
        this.ordinal = globalOrdinal++;
    }

    public void increment() {
        val++;
    }

    public int getVal() {
        return val;
    }

    public String name() {
        return oldEnumName;
    }

    @Override
    public String toString() {
        return oldEnumName;
    }

    public int ordinal() {
        return this.ordinal;
    }

    public static final Test[] VALUES = new Test[] {TEST_VALUE, OTHER_TEST_VALUE};

    public static Test[] values() {
        return VALUES;
    }

    public static Test valueOf(String oldEnumName) {
        for (Test enumClass : VALUES) {
            if (enumClass.oldEnumName.equals(oldEnumName))
            {
                return enumClass;
            }
        }
        throw new IllegalArgumentException();
    }
}

并重新编译所有内容。 EnumTesting 类的字节码在重新编译后保持不变。所以这确实是一个二进制兼容的变化。

【讨论】:

    【解决方案2】:

    不是向后兼容的更改,因为新类不会是 java.lang.Enum 的子类。

    这确实很重要,因为任何时候将泛型定义为<E extends Enum<E>>,字节码都需要实际类型是 Enum 的子类。特别是,EnumSet 和 EnumMap 不起作用。 (其他类似的课程也可能会中断——但这两个很常见,我认为它们值得特别提及。)

    为了测试这一点,我从一个枚举类和它的四个用户开始:

    public enum SoEnum {
      FOO, BAR
    }
    
    public class SoUser {
      public static void main(String[] args) {
        SoEnum e = SoEnum.FOO;
        switch (e) {
          case FOO:
            System.out.println("a foo");
            break;
          default:
            System.out.println(e.name());
        }
      }
    }
    
    import java.util.*;
    
    public class SoUserEnumMap {
      public static void main(String[] args) {
        EnumMap<SoEnum,String> map = new EnumMap<>(SoEnum.class);
        map.put(SoEnum.FOO, "the foo");
        System.out.println(map);
      }
    }
    
    import java.util.*;
    
    public class SoUserEnumSet {
      public static void main(String[] args) {
        EnumSet<SoEnum> set = EnumSet.of(SoEnum.FOO);
        System.out.println(set);
      }
    }
    
    public class SoUserGeneric {
      public static void main(String[] args) {
        String s = getName(SoEnum.FOO);
        System.out.println(s);
      }
    
      static <E extends Enum<E>> String getName(E e) {
        return e.name();
      }
    }
    

    我编译了所有这些,然后将 SoEnum.java 替换为使用普通类的版本。 (我写了一个简单的例子,所以有点难看:)):

    public class SoEnum {
      private SoEnum() {}
    
      public static SoEnum FOO = new SoEnum();
      public static SoEnum BAR = new SoEnum();
    
      public static SoEnum[] values() {
        return new SoEnum[] { FOO, BAR };
      }
    
      public int ordinal() {
        return this == FOO ? 0 : 1;
      }
    
      public String name() {
        return this == FOO ? "FOO" : "BAR";
      }
    
      @Override
      public String toString() {
        return name();
      }
    }
    

    我只重新编译了 SoEnum.java,然后运行了其余的。有趣的是,SoUser 确实工作了!事实证明,switch 实际上是由编译器为查找创建的匿名类处理的,它归结为序数和静态字段。这让我有点吃惊!

    但是其他三个用例爆炸性地爆炸了,如果我还不知道根本原因,错误消息会非常令人困惑。以下是来自java SoUserEnumSet 的错误信息。其他两个用户类以类似的方式失败(即使是 SoUserGeneric,我认为它会在稍微更标准的 ClassCastException 中失败)。

    Error: A JNI error has occurred, please check your installation and try again
    Exception in thread "main" java.lang.VerifyError: Bad type on operand stack
    Exception Details:
      Location:
        SoUserEnumSet.main([Ljava/lang/String;)V @3: invokestatic
      Reason:
        Type 'SoEnum' (current frame, stack[0]) is not assignable to 'java/lang/Enum'
      Current Frame:
        bci: @3
        flags: { }
        locals: { '[Ljava/lang/String;' }
        stack: { 'SoEnum' }
      Bytecode:
        0x0000000: b200 02b8 0003 4cb2 0004 2bb6 0005 b1
    
      at java.lang.Class.getDeclaredMethods0(Native Method)
      at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
      at java.lang.Class.privateGetMethodRecursive(Class.java:3048)
      at java.lang.Class.getMethod0(Class.java:3018)
      at java.lang.Class.getMethod(Class.java:1784)
      at sun.launcher.LauncherHelper.validateMainClass(LauncherHelper.java:544)
      at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:526)
    

    请注意,由于这是一个链接错误,“行”号 (@3) 甚至与源代码行不对应——它是 .class 文件中的操作码偏移量。

    另请注意,JLS 8.1.4 指定您不能创建显式扩展 Enum 的类(也就是说,您不能通过将 class SoEnum 更改为 class SoEnum extends Enum&lt;SoEnum&gt; 来修复它):

    如果 ClassType 将类命名为 Enum 或对 Enum 的任何调用(第 8.9 节),则会出现编译时错误。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-01-28
      • 2013-12-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-24
      • 2014-01-23
      • 1970-01-01
      相关资源
      最近更新 更多