Изменяет ли перечисление на двоичную совместимость класса break

Если у меня есть перечисление вроде этого:

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

и добавить все другие методы перечисления, такие как «значения» и «порядковый» в новый класс? Таким образом, другие банки смогут продолжать использовать его без перекомпиляции?

java,class,enums,binary-compatibility,

2

Ответов: 2


2

Я провел некоторое тестирование с этим перечислением, чтобы начать с

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 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 остается прежним после перекомпиляции. Таким образом, это действительно бинарное совместимое изменение.


0

Это не обратное совместимое изменение, потому что новый класс не будет подклассом java.lang.Enum.

Это действительно имеет значение, потому что в любое время, когда у вас есть общий <E extends Enum<E>>тип, определенный как , байт-код потребует, чтобы фактический тип был подклассом Enum. В частности, EnumSet и EnumMap не будут работать. (Другие, подобные классы также могут сломаться, но эти два достаточно распространены, что, я думаю, заслуживают особого упоминания).

Чтобы проверить это, я начал с класса enum и четырех пользователей:

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 действительно работал! Оказывается, что коммутатор фактически обрабатывается анонимным классом, созданным компилятором для поиска, который сводится к ординалам и статическим полям. Это меня немного удивило!

Но три других случая использования взорвались эффектно и с сообщениями об ошибках, которые были бы чрезвычайно запутанными, если бы я еще не знал первопричины. Ниже приведено сообщение об ошибке 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<SoEnum>):

Это ошибка времени компиляции, если ClassType называет класс Enum или любой вызов Enum (A§.8.9).

Java, класс, перечислений, бинарная совместимость,
Похожие вопросы