Что такое двоичная совместимость в Java?

Я читал " Эффективную Яву " Джошуа Блоха.

В пункте 17: "Использовать интерфейсы только для определения типов" я натолкнулся на объяснение, в котором не рекомендуется использовать интерфейсы для хранения констант. Я помещаю объяснение ниже.

"Хуже того, он представляет собой обязательство: если в будущем выпуске класс будет изменен так, что ему больше не нужно будет использовать константы, он все равно должен реализовать интерфейс для обеспечения двоичной совместимости".

Что означает двоичная совместимость здесь?

Может ли кто-нибудь привести меня с примером на Java, чтобы показать, что код двоично совместим.

5 ответов

Решение

Короче говоря, двоичная совместимость означает, что при изменении вашего класса вам не нужно перекомпилировать классы, которые его используют. Например, вы удалили или переименовали публичный или защищенный метод из этого класса

public class Logger implements Constants {
   public Logger getLogger(String name) {
         return LogManager.getLogger(name);
   }
}

из вашей библиотеки log-1.jar и выпустил новую версию как log-2.jar. Когда пользователи вашего log-1.jar загружают новую версию, они ломают свои приложения, когда они пытаются использовать отсутствующий метод getLogger(String name).

И если вы удалите интерфейс Constants (элемент 17), это также нарушит двоичную совместимость по той же причине.

Но вы можете удалить / переименовать закрытый или пакетный закрытый член этого класса, не нарушая двоичную совместимость, потому что внешние приложения не могут (или не должны) его использовать.

Чтобы лучше понять концепцию, интересно видеть, что двоичная совместимость НЕ подразумевает совместимость API, и наоборот.

API-совместимый, но НЕ бинарный: статическое удаление

Версия 1 библиотеки:

public class Lib {
    public static final int i = 1;
}

Код клиента:

public class Main {
    public static void main(String[] args) {
        if ((new Lib()).i != 1) throw null;
    }
}

Скомпилируйте код клиента с версией 1:

javac Main.java

Заменить версию 1 на версию 2: удалить static:

public class Lib {
    public final int i = 1;
}

Перекомпилируйте только версию 2, а не код клиента, и запустите java Main:

javac Lib.java
java Main

Мы получаем:

Exception in thread "main" java.lang.IncompatibleClassChangeError: Expected static field Lib.i
        at Main.main(Main.java:3)

Это происходит потому, что хотя мы можем написать (new Lib()).i в Java для обоих static и методы-члены, он компилируется в две разные инструкции VM в зависимости от Lib: getstatic или же getfield, Этот разрыв упоминается в JLS 7 13.4.10:

Если поле, которое не было объявлено закрытым, не было объявлено статическим и было изменено на объявленное статическим, или наоборот, то возникнет ошибка компоновки, в частности IncompatibleClassChangeError, если поле используется уже существующим двоичным файлом, который ожидал поле другого рода.

Нам нужно будет перекомпилировать Main с javac Main.java чтобы он работал с новой версией.

Заметки:

  • вызов статических членов из экземпляров классов, таких как (new Lib()).i плохой стиль, вызывает предупреждение, и никогда не должно быть сделано
  • этот пример придуман, потому что нестатический final примитивы бесполезны: всегда используйте static final для примитивов: приватный финальный статический атрибут против приватного финального атрибута
  • отражение можно использовать, чтобы увидеть разницу. Но рефлексия также может видеть приватные поля, что, очевидно, приводит к перерывам, которые не должны считаться перерывами, поэтому не учитываются.

Бинарная совместимость, но НЕ совместимая с API: усиление нулевого условия

Версия 1:

public class Lib {
    /** o can be null */
    public static void method(Object o) {
        if (o != null) o.hashCode();
    }
}

Версия 2:

public class Lib {
    /** o cannot be null */
    public static void method(Object o) {
        o.hashCode();
    }
}

Клиент:

public class Main {
    public static void main(String[] args) {
        Lib.method(null);
    }
}

На этот раз, даже если перекомпилировать Main после обновления Lib, второй вызов бросит, но не первый.

Это потому, что мы изменили контракт method способом, который не проверяется во время компиляции Java: прежде чем это может занять nullпосле не больше.

Заметки:

  • Eclipse Wiki - отличный источник для этого: https://wiki.eclipse.org/Evolving_Java-based_APIs
  • создание API, которые принимают null ценности сомнительная практика
  • гораздо проще внести изменения, которые нарушают совместимость API, но не двоичные, чем наоборот, так как легко изменить внутреннюю логику методов

Бинарная совместимость

Бинарная совместимость Java предписывает условия, при которых модификация и повторная компиляция классов не требуют повторной компиляции других классов, импортирующих измененные классы. Бинарная совместимость - это новая концепция языкового дизайна.

Спецификация языка Java [7] описывает двоичные совместимые изменения следующим образом:

Изменение типа двоично совместимо с (эквивалентно, не нарушает совместимость) с уже существующими двоичными файлами, если существующие ранее двоичные файлы, которые ранее были связаны без ошибок, будут продолжать ссылаться без ошибок.

Если в будущем мы хотим изменить интерфейс, который реализуют некоторые классы (например, добавление некоторых новых методов).

Если мы добавим абстрактные методы (дополнительные методы), то классы (реализующие интерфейс) должны реализовать дополнительный метод, создающий ограничение зависимости и накладные расходы, чтобы выполнить то же самое.

Чтобы преодолеть это, мы можем добавить методы по умолчанию в интерфейс.

Это удалит зависимость для реализации дополнительных методов.

Нам не нужно модифицировать реализующий класс для включения изменений. Это называется двоичной совместимостью.

Пожалуйста, обратитесь к примеру ниже:

Интерфейс, который мы будем использовать

    //Interface       
    interface SampleInterface
            {
                // abstract method
                public void abstractMethod(int side);

                // default method
                default void defaultMethod() {
                   System.out.println("Default Method Block");
                }

                // static method
                static void staticMethod() {
                    System.out.println("Static Method Block");
                }
            }


//The Class that implements the above interface.

    class SampleClass implements SampleInterface
    {
        /* implementation of abstractMethod abstract method, if not implemented 
        will throw compiler error. */
        public void abstractMethod(int side)
        {System.out.println(side*side);}

        public static void main(String args[])
        {
            SampleClass sc = new SampleClass();
            sc.abstractMethod(4);

            // default method executed
            sc.defaultMethod();

            // Static method executed
            SampleInterface.staticMethod();

        }
    }

Примечание: для более подробной информации, пожалуйста, обратитесь к методам по умолчанию

Чтобы все выглядело просто:

Компьютер, который может выполнять тот же двоичный код, предназначенный для запуска на другом компьютере, называется двоично-совместимым. Это отличается от совместимости исходного кода, где может потребоваться перекомпиляция.

Бинарная совместимость является основным преимуществом при разработке компьютерных программ, которые должны выполняться в нескольких операционных системах.

Другие вопросы по тегам