Глава 13. Бинарная совместимость
Оглавление
- 13.1. Формат бинарного файла
- 13.2. Что такое и что не является бинарной совместимостью
- 13.3. Эволюция пакетов
- 13.4. Эволюция классов
-
- 13.4.1.
abstractКлассы - 13.4.2.
finalКлассы - 13.4.3.
publicКлассы - 13.4.4. Базовые классы и интерфейсы
- 13.4.5. Параметры типа класса
- 13.4.6. Тело класса и объявления членов
- 13.4.7. Доступ к членам и конструкторам
- 13.4.8. Объявления полей
- 13.4.9.
finalПоля и константы - 13.4.10.
staticПоля - 13.4.11.
transientПоля - 13.4.12. Объявления методов и конструкторов
- 13.4.13. Параметры типа методов и конструкторов
- 13.4.14. Формальные параметры методов и конструкторов
- 13.4.15. Тип возвращаемого значения метода
- 13.4.16.
abstractМетоды - 13.4.17.
finalМетоды - 13.4.18.
nativeМетоды - 13.4.19.
staticМетоды - 13.4.20.
synchronizedМетоды - 13.4.21. Исключение методов и конструкторов
- 13.4.22. Тело метода и конструктора
- 13.4.23. Перегрузка методов и конструкторов
- 13.4.24. Переопределение методов
- 13.4.25. Статические инициализаторы
- 13.4.26. Эволюция перечислений
- 13.4.1.
- 13.5. Эволюция интерфейсов
Инструменты разработки для языка программирования Java должны поддерживать автоматическую перекомпиляцию при необходимости, когда доступен исходный код. Конкретные реализации также могут хранить исходный и бинарный код типов в базе данных с версиями и реализовывать ClassLoader механизмы целостности базы данных, чтобы предотвращать ошибки связывания, предоставляя бинарно-совместимые версии типов клиентам.
Разработчики пакетов и классов, которые должны широко распространяться, сталкиваются с другими проблемами. В Интернете, который является нашим любимым примером широко распределённой системы, часто непрактично или невозможно автоматически перекомпилировать существующие бинарные файлы, которые напрямую или косвенно зависят от типа, который должен быть изменён. Вместо этого, данная спецификация определяет набор изменений, которые разработчики могут вносить в пакет или тип класса или интерфейса, сохраняя (не нарушая) совместимость с существующими бинарными файлами.
В рамках Бинарной совместимости между выпусками в SOM (Forman, Conner, Danforth и Raper, Труды OOPSLA '95), бинарные файлы языка программирования Java являются бинарно совместимыми при всех соответствующих преобразованиях, которые авторы идентифицируют (с некоторыми оговорками относительно добавления переменных экземпляра). Используя их схему, вот список некоторых важных бинарно совместимых изменений, которые поддерживает язык программирования Java:
-
Переиспользование существующих методов, конструкторов и инициализаторов для повышения производительности.
-
Изменение методов или конструкторов для возврата значений на входах, для которых они ранее либо выбрасывали исключения, которые обычно не должны возникать, либо не работали, переходя в бесконечный цикл или вызывая тупик.
-
Добавление новых полей, методов или конструкторов в существующий класс или интерфейс.
-
Удаление
privateполей, методов или конструкторов из класса. -
При обновлении всего пакета, удаление полей, методов или конструкторов с доступом по умолчанию (только для пакета) классов и интерфейсов в пакете.
-
Переупорядочивание полей, методов или конструкторов в существующем объявлении типа.
-
Перемещение метода вверх по иерархии классов.
-
Переупорядочивание списка прямых базовых интерфейсов класса или интерфейса.
-
Вставка новых типов классов или интерфейсов в иерархию типов.
В этой главе определены минимальные стандарты бинарной совместимости, гарантируемые всеми реализациями. Язык программирования Java гарантирует совместимость, когда бинарные файлы классов и интерфейсов смешиваются, которые не известны как происходящие из совместимых источников, но исходные коды которых были изменены совместимыми способами, описанными здесь. Обратите внимание, что мы обсуждаем совместимость между выпусками приложения. Обсуждение совместимости между выпусками платформы Java SE выходит за рамки этой главы.
Мы рекомендуем системам разработки предоставлять средства, которые предупреждают разработчиков о влиянии изменений на уже существующие бинарные файлы, которые не могут быть перекомпилированы.
В этой главе сначала определены некоторые свойства, которые должен иметь любой бинарный формат для языка программирования Java (§13.1). Затем определена бинарная совместимость, объясняя, что это такое и чем она не является (§13.2). И, наконец, перечислены множество возможных изменений пакетов (§13.3), классов (§13.4) и интерфейсов (§13.5), указывая, какие из этих изменений гарантированно сохраняют бинарную совместимость, а какие нет.
Программы должны быть скомпилированы либо в формат файла class, указанный в Спецификации виртуальной машины Java, Java SE 7 Edition, либо в представление, которое может быть отображено в этот формат загрузчиком классов, написанным на языке программирования Java.
Кроме того, результирующий файл class должен иметь определённые свойства. Многие из этих свойств специально выбраны для поддержки преобразований исходного кода, которые сохраняют бинарную совместимость. Требуемые свойства:
-
Класс или интерфейс должен быть назван своим двоичным именем, которое должно соответствовать следующим ограничениям:
-
Двоичное имя типа верхнего уровня (§7.6) — это его каноническое имя (§6.7).
-
Двоичное имя вложенного типа (§8.5, §9.5) состоит из двоичного имени его непосредственно содержащего типа, за которым следует $, затем простое имя вложенного типа.
-
Двоичное имя локального класса (§14.3) состоит из двоичного имени его непосредственно содержащего типа, за которым следует $, за которым следует непустая последовательность цифр, а затем простое имя локального класса.
-
Двоичное имя анонимного класса (§15.9.5) состоит из двоичного имени его непосредственно содержащего типа, за которым следует $, а затем непустая последовательность цифр.
-
Двоичное имя параметра типа, объявленного обобщенным классом или интерфейсом (§8.1.2, §9.1.2) — это двоичное имя его непосредственно содержащего типа, за которым следует $, а затем простое имя параметра типа.
-
Двоичное имя параметра типа, объявленного обобщенным методом (§8.4.4) — это двоичное имя типа, объявляющего метод, за которым следует $, за которым следует дескриптор метода, как определено в Спецификации виртуальной машины Java, издание Java SE 7, за которым следует $, а затем простое имя параметра типа.
-
Двоичное имя параметра типа, объявленного обобщенным конструктором (§8.8.4) — это двоичное имя типа, объявляющего конструктор, за которым следует $, за которым следует дескриптор конструктора, как определено в Спецификации виртуальной машины Java, издание Java SE 7, за которым следует $, а затем простое имя параметра типа.
-
-
Ссылка на другой класс или интерфейс должна быть символической, используя двоичное имя типа.
-
Ссылки на поля, которые являются константами (§4.12.4), разрешаются во время компиляции на постоянное значение, которое обозначено. Никакая ссылка на такое поле не должна присутствовать в коде в двоичном файле (за исключением класса или интерфейса, содержащего поле, который будет иметь код для его инициализации). Такое поле всегда должно отображаться как инициализированное (§12.4.2); значение по умолчанию для типа такого поля никогда не должно наблюдаться. См. §13.4.9 для обсуждения.
-
Учитывая законное выражение, обозначающее доступ к полю в классе C, ссылающееся на поле, которое не является константой (§13.4.9) и называется
f, объявленное в (возможно, отличном) классе или интерфейсе D, мы определяем тип квалификации ссылки на поле следующим образом:-
Если выражение имеет вид Основное выражение
.f, то:-
Если тип выражения Основное выражение — это пересечение типов (§4.9) V1 & ... & Vn, то тип квалификации ссылки — V1.
-
В противном случае тип выражения Основное выражение является типом квалификации ссылки.
-
-
Если выражение имеет вид
super.f, то суперкласс C является типом квалификации ссылки. -
Если выражение имеет вид X
.super.f, то суперкласс X является типом квалификации ссылки. -
Если ссылка имеет вид X
.f, где X обозначает класс или интерфейс, то класс или интерфейс, обозначенный X, является типом квалификации ссылки. -
Если ссылка имеет вид простого имени, то если
fявляется членом текущего класса или интерфейса C, то пусть T будет C. В противном случае пусть T будет самым внутренним лексически содержащим классом, членом которого являетсяf. В любом случае T является типом квалификации ссылки.
Ссылка на
fдолжна быть скомпилирована в символическую ссылку на стёртый тип (§4.6) типа квалификации ссылки плюс простое имя поляf. Ссылка также должна включать символическую ссылку на стёртый тип объявленного типа поля, чтобы верификатор мог проверить, что тип соответствует ожиданиям. -
-
Учитывая выражение вызова метода в классе или интерфейсе C, ссылающееся на метод, названный
m, объявленный (или неявно объявленный (§9.2)) в (возможно, отличном) классе или интерфейсе D, мы определяем тип квалификации вызова метода следующим образом:-
Если D — это
Object, то тип квалификации выражения —Object. -
В противном случае:
-
Если выражение имеет вид Основное выражение
.m, то:-
Если тип выражения Основное выражение — это пересечение типов (§4.9) V1 & ... & Vn, то тип квалификации вызова метода — V1.
-
В противном случае тип выражения Основное выражение является типом квалификации вызова метода.
-
-
Если выражение имеет вид
super.m, то суперкласс C является типом квалификации вызова метода. -
Если выражение имеет вид X
.super.m, то суперкласс X является типом квалификации вызова метода. -
Если ссылка имеет вид X
.m, где X обозначает класс или интерфейс, то класс или интерфейс, обозначенный X, является типом квалификации вызова метода. -
Если метод ссылается на простое имя, то если
mявляется членом текущего класса или интерфейса C, то пусть T будет C. В противном случае пусть T будет самым внутренним лексически содержащим классом, членом которого являетсяm. В любом случае T является типом квалификации вызова метода.
-
Ссылка на метод должна быть разрешена во время компиляции до символической ссылки на стёртый тип (§4.6) типа квалификации вызова, плюс на стёртую сигнатуру (§8.4.2) метода. Подпись метода должна включать всё нижеперечисленное, как определено §15.12.3:
-
Простое имя метода
-
Количество параметров метода
-
Символическая ссылка на тип каждого параметра
Ссылка на метод также должна содержать символическую ссылку на стёртый тип возвращаемого типа обозначенного метода или указание на то, что обозначаемый метод объявлен
voidи не возвращает значения. -
-
Взяв выражение создания экземпляра класса (§15.9) или инструкцию вызова конструктора (§8.8.7.1) в классе или интерфейсе C, ссылающемся на конструктор
m, объявленный в (возможно, отличном) классе или интерфейсе D, мы определяем квалифицируемый тип вызова конструктора следующим образом:-
Если выражение имеет вид
newD(...)или X.newD(...), то квалифицируемый тип вызова — D. -
Если выражение имеет вид
newD(...){...}или X.newD(...){...}, то квалифицируемый тип выражения — тип выражения во время компиляции. -
Если выражение имеет вид
super(...)или Основной.super(...), то квалифицируемый тип выражения — непосредственный суперкласс C. -
Если выражение имеет вид
this(...), то квалифицируемый тип выражения — C.
Ссылка на конструктор должна быть разрешена во время компиляции до символической ссылки на стирание (§4.6) квалифицируемого типа вызова, плюс сигнатура конструктора (§8.8.2). Сигнатура конструктора должна включать в себя:
-
Количество параметров конструктора
-
Символическую ссылку на тип каждого формального параметра
Кроме того, конструктор не-
privateвнутреннего класса-члена должен быть скомпилирован таким образом, чтобы иметь в качестве первого параметра дополнительный неявный параметр, представляющий немедленно окружающий экземпляр (§8.1.3). -
-
Любые конструкции, введённые компилятором Java, которые не имеют соответствующей конструкции в исходном коде, должны быть помечены как синтетические, за исключением конструкторов по умолчанию, метода инициализации класса и методов
valuesиvalueOfклассаEnum.
Двоичное представление класса или интерфейса должно также содержать все следующее:
-
Если это класс, и он не класс
Object, то символическая ссылка на стирание (§4.6) непосредственного суперкласса этого класса. -
Символическая ссылка на стирание каждого непосредственного суперинтерфейса, если таковые имеются.
-
Спецификация каждого поля, объявленного в классе или интерфейсе, представленная простым именем поля и символической ссылкой на стирание типа поля.
-
Если это класс, то стёртая сигнатура каждого конструктора, как описано выше.
-
Для каждого метода, объявленного в классе или интерфейсе (исключая, для интерфейса, его неявные методы (§9.2)), его стёртая сигнатура и возвращаемый тип, как описано выше.
-
Код, необходимый для реализации класса или интерфейса:
-
Для интерфейса, код инициализаторов полей
-
Для класса, код инициализаторов полей, инициализаторов экземпляра и статики, и реализация каждого метода или конструктора
-
-
Каждый тип должен содержать достаточную информацию для восстановления его канонического имени (§6.7).
-
Каждый тип-член должен иметь достаточную информацию для восстановления его модификатора доступа уровня источника.
-
Каждый вложенный класс должен иметь символическую ссылку на его немедленно окружающий класс.
-
Каждый класс, содержащий вложенный класс, должен содержать символические ссылки на все его вложенные классы, а также на все локальные и анонимные классы, которые появляются в его методах, конструкторах и статических или экземпляризованных инициализаторах.
Следующие разделы обсуждают изменения, которые могут быть внесены в объявления типов классов и интерфейсов, без нарушения совместимости с существующими двоичными файлами. В соответствии с требованиями к переводу, приведенными выше, виртуальная машина Java и её формат файлов class поддерживают эти изменения. Любой другой допустимый двоичный формат, такой как сжатое или зашифрованное представление, которое отображается обратно в файлы class загрузчиком классов в соответствии с вышеуказанными требованиями, обязательно также будет поддерживать эти изменения.
Изменение типа совместимо с (эквивалентно, не нарушает двоичную совместимость с) существующими двоичными файлами, если существующие двоичные файлы, которые ранее связывались без ошибок, будут продолжать связываться без ошибок.
Двоичные файлы компилируются таким образом, что полагаются на доступные члены и конструкторы других классов и интерфейсов. Для сохранения двоичной совместимости класс или интерфейс должен рассматривать свои доступные члены и конструкторы, их существование и поведение, как договор с их пользователями.
Язык программирования Java разработан таким образом, чтобы предотвратить добавление пунктов к договору и случайные столкновения имён от нарушения двоичной совместимости. В частности, добавление дополнительных методов перегрузки для конкретного имени метода не нарушает совместимость с существующими двоичными файлами. Сигнатура метода, которую существующий двоичный файл будет использовать для поиска метода, выбирается алгоритмом разрешения перегрузки методов во время компиляции (§15.12.2).
Если язык программирования Java был разработан так, что конкретный метод для выполнения выбирался во время выполнения, то подобная неоднозначность могла бы быть обнаружена во время выполнения. Такое правило означало бы, что добавление дополнительного перегруженного метода, чтобы сделать возможной неоднозначность в месте вызова, могло бы нарушить совместимость с неизвестным количеством существующих двоичных файлов. Смотрите §13.4.23 для более подробного обсуждения.
Двоичная совместимость не то же самое, что исходная совместимость. В частности, пример в §13.4.6 показывает, что набор совместимых двоичных файлов может быть получен из исходных кодов, которые не будут компилироваться вместе. Этот пример типичен: новое объявление добавлено, изменяя смысл имени в неизменённой части исходного кода, в то время как существующий двоичный файл для этой неизменённой части исходного кода сохраняет полностью квалифицированное, предыдущее значение имени. Для получения согласованного набора исходного кода необходимо предоставить квалифицированное имя или выражение доступа к полю, соответствующее предыдущему значению.
Новый тип класса или интерфейса верхнего уровня может быть добавлен в пакет, без нарушения совместимости с существующими двоичными файлами, при условии, что новый тип не использует имя, ранее присвоенное несвязанному типу.
Если новый тип повторно использует имя, ранее присвоенное несвязанному типу, то может возникнуть конфликт, поскольку двоичные файлы для обоих типов не могут быть загружены одним и тем же загрузчиком классов.
Изменения в классах и интерфейсах верхнего уровня, которые не являются public и которые не являются суперклассом или суперинтерфейсом, соответственно, типа public, затрагивают только типы в пакете, в котором они объявлены. Такие типы могут быть удалены или изменены другим образом, даже если несовместимости описаны иначе, при условии, что соответствующие двоичные файлы этого пакета обновляются вместе.
В этом разделе описываются последствия изменений в объявлении класса и его членов и конструкторов для существующих бинарных файлов.
Если класс, который не был объявлен abstract, изменяется на объявление abstract, то существующие бинарные файлы, которые пытаются создать новые экземпляры этого класса, вызовут либо InstantiationError во время компоновки, либо (если используется рефлексивный метод) InstantiationException во время выполнения; поэтому такое изменение не рекомендуется для широко распространённых классов.
Изменение класса, объявленного abstract, на необъявление abstract, не нарушает совместимость с существующими бинарными файлами.
Если класс, который не был объявлен final, изменяется на объявление final, то VerifyError будет выброшена, если загружен бинарный файл подкласса этого класса, потому что final классы не могут иметь подклассов; такое изменение не рекомендуется для широко распространённых классов.
Изменение класса, объявленного final, на необъявление final, не нарушает совместимость с существующими бинарными файлами.
Изменение класса, который не объявлен public, на объявление public, не нарушает совместимость с существующими бинарными файлами.
Если класс, объявленный public, изменяется на необъявление public, то IllegalAccessError будет выброшена, если компонуется существующий бинарный файл, которому необходим, но больше нет доступа к типу класса; такое изменение не рекомендуется для широко распространённых классов.
ClassCircularityError будет выброшена во время загрузки, если класс является собственным суперклассом. Изменения в иерархии классов, которые могут привести к такой цикличности при загрузке новых скомпилированных бинарных файлов с существующими бинарными файлами, не рекомендуются для широко распространённых классов.
Изменение непосредственного суперкласса или набора непосредственных суперинтерфейсов типа класса не нарушит совместимости с существующими бинарными файлами, при условии, что полное множество суперклассов или суперинтерфейсов, соответственно, типа класса не теряет ни одного члена.
Если изменение непосредственного суперкласса или набора непосредственных суперинтерфейсов приводит к тому, что какой-либо класс или интерфейс больше не является суперклассом или суперинтерфейсом, соответственно, могут возникнуть ошибки компоновки, если существующие бинарные файлы загружаются с бинарным файлом изменённого класса. Такие изменения не рекомендуются для широко распространённых классов.
Пример 13.4.4-1. Изменение Суперкласса
Предположим, что следующая тестовая программа:
class Hyper { char h = 'h'; }
class Super extends Hyper { char s = 's'; }
class Test extends Super {
public static void printH(Hyper h) {
System.out.println(h.h);
}
public static void main(String[] args) {
printH(new Super());
}
}
скомпилирована и выполнена, выведя результат:
h
Предположим, что затем скомпилирована новая версия класса Super:
class Super { char s = 's'; }
Эта версия класса Super не является подклассом Hyper. Если мы затем запустим существующие бинарные файлы Hyper и Test с новой версией Super, то VerifyError будет выброшена во время компоновки. Верификатор возражает, потому что результат new Super() не может быть передан в качестве аргумента вместо формального параметра типа Hyper, так как Super не является подклассом Hyper.
Поучительно рассмотреть, что может произойти без этапа проверки: программа может работать и вывести:
s
Это демонстрирует, что без верификатора система типов Java может быть нарушена связыванием несовместимых бинарных файлов, даже если каждый был произведён корректным компилятором Java.
Вывод заключается в том, что реализация, которая не имеет верификатора или не использует его, не будет поддерживать безопасность типов и, следовательно, не является действительной реализацией.
Требование, чтобы альтернативы в блоке `multi-catch` (§14.20) не являлись подклассами или суперклассами друг друга, является лишь ограничением для исходного кода. Предположим, что следующий клиентский код является допустимым:
try {
throwAorB();
} catch(ExceptionA | ExceptionB e) {
...
}
где ExceptionA и ExceptionB не имеют отношения подкласс/суперкласс при компиляции клиента, он бинарно совместим с клиентом в отношении ExceptionA и ExceptionB, имеющих такое отношение при выполнении клиента.
Это аналогично другим ситуациям, где преобразование класса, бинарно совместимое для клиента, может не быть исходно совместимым для того же клиента.
Добавление или удаление параметра типа класса само по себе не имеет никаких последствий для бинарной совместимости.
Если такой параметр типа используется в типе поля или метода, это может иметь обычные последствия изменения упомянутого типа.
Переименование параметра типа класса не оказывает никакого влияния на существующие бинарные файлы.
Изменение первого ограничения параметра типа класса может изменить стирание (§4.6) любого члена, использующего этот параметр типа в собственном типе, что может повлиять на бинарную совместимость. Изменение такого ограничения аналогично изменению первого ограничения параметра типа метода или конструктора (§13.4.13).
Изменение любого другого ограничения не влияет на бинарную совместимость.
Добавление экземпляра (соответственно static) члена, имеющего то же имя и доступность (для полей), или то же имя и доступность, а также сигнатуру и возвращаемый тип (для методов), что и экземпляра (соответственно static) члена суперкласса или подкласса, не вызывает несовместимости с существующими бинарными файлами. Ошибка не возникает, даже если набор связанных классов столкнется с ошибкой во время компиляции.
Удаление члена или конструктора класса, не объявленного private, может вызвать ошибку компоновки, если член или конструктор используется существующим бинарным файлом.
Пример 13.4.6-1. Изменение Тела Класса
class Hyper {
void hello() { System.out.println("hello from Hyper"); }
}
class Super extends Hyper {
void hello() { System.out.println("hello from Super"); }
}
class Test {
public static void main(String[] args) {
new Super().hello();
}
}
Эта программа выводит результат:
hello from Super
Предположим, что произведена новая версия класса Super:
class Super extends Hyper {}
Затем, при повторной компиляции Super и выполнении этого нового бинарного файла с исходными бинарными файлами для Test и Hyper, вывод:
hello from Hyper
как ожидалось.
Ключевое слово super может использоваться для доступа к методу, объявленному в суперклассе, минуя любые методы, объявленные в текущем классе. Выражение super.Идентификатор разрешается во время компиляции на метод m в суперклассе S. Если метод m является методом экземпляра, то метод, который вызывается во время выполнения, — это метод с той же сигнатурой, что и m, который является членом непосредственного суперкласса класса, содержащего выражение, включающее super.
Пример 13.4.6-2. Изменение Суперкласса
class Hyper {
void hello() { System.out.println("hello from Hyper"); }
}
class Super extends Hyper { }
class Test extends Super {
public static void main(String[] args) {
new Test().hello();
}
void hello() {
super.hello();
}
}
Эта программа выводит результат:
hello from Hyper
Предположим, что произведена новая версия класса Super:
class Super extends Hyper {
void hello() { System.out.println("hello from Super"); }
}
Тогда, если Super и Hyper перекомпилированы, но не Test, то запуск новых бинарных файлов с существующим бинарным файлом Test даст вывод:
hello from Super
как вы могли ожидать.
Изменение объявленного доступа к члену или конструктору, чтобы разрешить меньше доступа, может нарушить совместимость с существующими двоичными файлами, вызвав ошибку компоновки при разрешении этих двоичных файлов. Меньший доступ разрешается, если модификатор доступа изменяется с доступа по умолчанию на private доступ; с доступа protected на доступ по умолчанию или private доступ; или с доступа public на доступ protected, по умолчанию или private доступ. Поэтому не рекомендуется изменять член или конструктор для разрешения меньшего доступа для широко распространенных классов.
Возможно, неожиданно, что двоичный формат определен таким образом, что изменение члена или конструктора для предоставления большего доступа не вызывает ошибку компоновки, когда подкласс (уже) определяет метод с меньшим доступом.
Пример 13.4.7-1. Изменение доступности
Если пакет points определяет класс Point:
package points;
public class Point {
public int x, y;
protected void print() {
System.out.println("(" + x + "," + y + ")");
}
}
используемый программой:
class Test extends points.Point {
public static void main(String[] args) {
Test t = new Test();
t.print();
}
protected void print() {
System.out.println("Test");
}
}
тогда эти классы компилируются и Test выполняется с получением следующего результата:
Test
Если метод print в классе Point изменяется на public, а затем компилируется только класс Point, а затем выполняется с ранее существующим двоичным файлом для Test, то ошибка компоновки не возникает. Это происходит даже несмотря на то, что в момент компиляции для метода public неправильно переопределять его методом protected (как показано тем, что класс Test не мог быть скомпилирован с использованием этого нового класса Point, если не print в Test был изменён на public.)
Разрешение суперклассам изменять методы protected на public без нарушения двоичных файлов существующих подклассов помогает сделать двоичные файлы менее хрупкими. Альтернатива, где такое изменение вызовет ошибку компоновки, создаст дополнительные несовместимости двоичных файлов.
Широко распространённые программы не должны предоставлять доступ к полям своим клиентам. Помимо проблем с совместимостью двоичных файлов, обсуждаемых ниже, это обычно хорошая практика разработки программного обеспечения. Добавление поля в класс может нарушить совместимость с существующими двоичными файлами, которые не были перекомпилированы.
Предположим ссылку на поле f с квалифицируемым типом T. Далее предположим, что f является фактически экземпляром (соответственно static) поля, объявленного в суперклассе T, S, и что тип f — X.
Если новое поле типа X с тем же именем, что и у f, добавляется в подкласс S, являющимся суперклассом T или самим T, то может возникнуть ошибка компоновки. Такая ошибка компоновки возникнет только в том случае, если, помимо вышеперечисленного, выполняются следующие условия:
-
Новое поле имеет меньший доступ, чем старое.
-
Новое поле является статическим (соответственно экземпляром) полем.
В частности, ошибка компоновки не возникнет в случае, когда класс больше не может быть перекомпилирован, потому что доступ к полю ранее ссылался на поле суперкласса с несовместимым типом. Предыдущий скомпилированный класс с такой ссылкой продолжит ссылаться на поле, объявленное в суперклассе.
Пример 13.4.8-1. Добавление объявления поля
class Hyper { String h = "hyper"; }
class Super extends Hyper { String s = "super"; }
class Test {
public static void main(String[] args) {
System.out.println(new Super().h);
}
}
Эта программа выводит:
hyper
Предположим, что создана новая версия класса Super:
class Super extends Hyper {
String s = "super";
int h = 0;
}
Затем, перекомпилировав Hyper и Super и выполнив полученные новые двоичные файлы со старым двоичным файлом Test, получим вывод:
hyper
Поле h класса Hyper выводится исходным двоичным файлом Test. Хотя это может показаться неожиданным на первый взгляд, оно служит для уменьшения количества несовместимостей, возникающих во время выполнения. (В идеальном мире все исходные файлы, которые нужно было бы перекомпилировать, перекомпилировались бы всякий раз, когда какой-либо из них изменялся, устраняя такие сюрпризы. Но такая массовая перекомпиляция часто непрактична или невозможна, особенно в Интернете. И, как отмечалось ранее, такая перекомпиляция иногда потребовала бы дополнительных изменений в исходном коде.)
В качестве другого примера, если программа:
class Hyper { String h = "Hyper"; }
class Super extends Hyper { }
class Test extends Super {
public static void main(String[] args) {
String s = new Test().h;
System.out.println(s);
}
}
скомпилирована и выполняется, она выводит:
Hyper
Предположим, что затем скомпилирована новая версия класса Super:
class Super extends Hyper { char h = 'h'; }
Если полученный двоичный файл используется с существующими двоичными файлами для Hyper и Test, то вывод останется таким:
Hyper
несмотря на то, что компиляция исходных файлов:
class Hyper { String h = "Hyper"; }
class Super extends Hyper { char h = 'h'; }
class Test extends Super {
public static void main(String[] args) {
String s = new Test().h;
System.out.println(s);
}
}
привела бы к ошибке компиляции, потому что h в исходном коде main теперь будет интерпретироваться как ссылка на поле char, объявленное в Super, и значение типа char не может быть присвоено полю типа String.
Удаление поля из класса нарушит совместимость с любыми существующими двоичными файлами, которые ссылаются на это поле, и произойдёт ошибка компоновки при ссылке на это поле из существующего двоичного файла. Только private поля могут быть безопасно удалены из широко распространённого класса.
Для обеспечения совместимости двоичных файлов добавление или удаление поля f, тип которого включает переменные типов (§4.4) или параметризованные типы (§4.5) эквивалентно добавлению (соответственно, удалению) поля с тем же именем, тип которого является стёртым типом (§4.6) типа f.
Если поле, которое не было объявлено final, изменить, чтобы оно было объявлено final, это может нарушить совместимость с существующими бинарными файлами, которые пытаются присвоить новые значения этому полю.
Пример 13.4.9-1. Изменение переменной на final
class Super { static char s; }
class Test extends Super {
public static void main(String[] args) {
s = 'a';
System.out.println(s);
}
}
Эта программа выводит:
a
Предположим, что создана новая версия класса Super:
class Super { static final char s = 'b'; }
Если Super перекомпилирован, но не Test, то запуск нового бинарного файла с существующим бинарным файлом Test приведет к IllegalAccessError.
Удаление ключевого слова final или изменение значения, к которому инициализируется поле, не нарушает совместимость с существующими бинарными файлами.
Если поле является константой (§4.12.4), то удаление ключевого слова final или изменение его значения не нарушит совместимость с существующими бинарными файлами, заставив их не работать, но они не увидят нового значения для использования поля, если не перекомпилированы. Это верно даже если само использование не является константным выражением времени компиляции (§15.28).
Этот результат является побочным эффектом решения поддерживать условную компиляцию, как обсуждалось в конце §14.21.
Пример 13.4.9-2. Условная компиляция
Если пример:
class Flags { static final boolean debug = true; }
class Test {
public static void main(String[] args) {
if (Flags.debug)
System.out.println("debug is true");
}
}
скомпилирован и выполнен, он выводит:
debug is true
Предположим, что создана новая версия класса Flags:
class Flags { static final boolean debug = false; }
Если Flags перекомпилирован, но не Test, то запуск нового бинарного файла с существующим бинарным файлом Test выводит:
debug is true
потому что значение debug было константным выражением времени компиляции и могло быть использовано при компиляции Test без ссылки на класс Flags.
Это поведение не изменится, если Flags будет изменено на интерфейс, как в измененном примере:
interface Flags { boolean debug = true; }
class Test {
public static void main(String[] args) {
if (Flags.debug)
System.out.println("debug is true");
}
}
Лучший способ избежать проблем с «изменяемыми константами» в широко используемом коде — объявлять в качестве констант времени компиляции только те значения, которые действительно вряд ли изменятся. Помимо истинно математических констант, мы рекомендуем, чтобы исходный код делал очень умеренное использование переменных класса, которые объявлены static и final. Если требуется только для чтения природа final, лучший выбор — объявить private static переменную и подходящий метод доступа для получения её значения.
Таким образом, мы рекомендуем:
private static int N;
public static int getN() { return N; }
вместо:
public static final int N = ...;
Нет проблем с:
public static int N = ...;
если N не должно быть только для чтения. Мы также рекомендуем в качестве общего правила, чтобы только истинно константные значения объявлялись в интерфейсах.
Мы отмечаем, но не рекомендуем, что если поле примитивного типа интерфейса может измениться, его значение может быть выражено следующим образом:
interface Flags {
boolean debug = new Boolean(true).booleanValue();
}
обеспечивая, что это значение не является константой. Аналогичные выражения существуют для других примитивных типов.
Еще одно примечание: поля static final, имеющие константные значения (будь то примитивного или String типа), никогда не должны иметь значение по умолчанию для своего типа (§4.12.5). Это означает, что все такие поля, по-видимому, инициализируются в первую очередь во время инициализации класса (§8.3.2.1, §9.3.1, §12.4.2).
Если поле, которое не было объявлено private, не было объявлено static и изменено на объявление static, или наоборот, произойдет ошибка связывания, конкретно ошибка IncompatibleClassChangeError, если поле используется существующим бинарным файлом, который ожидал поле другого типа. Такие изменения не рекомендуются в коде, который широко распространен.
Добавление или удаление модификатора transient поля не нарушает совместимости с существующими бинарными файлами.
Добавление объявления метода или конструктора в класс не нарушает совместимость с существующими бинарными файлами, даже в случае, когда тип больше не может быть перекомпилирован, потому что вызов ранее ссылался на метод или конструктор суперкласса с несовместимым типом. Ранее скомпилированный класс с такой ссылкой по-прежнему будет ссылаться на метод или конструктор, объявленный в суперклассе.
Предположим ссылку на метод m с квалифицируемым типом T. Предположим далее, что m — это фактически метод экземпляра (соответственно static), объявленный в суперклассе T, S.
Если новый метод типа X с тем же сигналом и возвращаемым типом, что и m, добавлен в подкласс S, который является суперклассом T или T самим, может произойти ошибка связывания. Такая ошибка связывания произойдёт только если, помимо вышесказанного, выполняются одно из следующих условий:
-
Новый метод имеет меньший доступ, чем старый.
-
Новый метод является
static(соответственно методом экземпляра).
Удаление метода или конструктора из класса может нарушить совместимость с любым существующим бинарным файлом, который ссылался на этот метод или конструктор; может быть выброшена ошибка NoSuchMethodError при связывании такой ссылки из существующего бинарного файла. Такая ошибка произойдёт только если не объявлен ни один метод с соответствующим сигналом и возвращаемым типом в суперклассе.
Если исходный код для не-внутреннего класса не содержит объявленных конструкторов, Java-компилятор автоматически предоставляет конструктор по умолчанию без параметров (§8.8.9). Добавление одного или нескольких объявлений конструкторов в исходный код такого класса предотвратит автоматическое предоставление этого конструктора по умолчанию, фактически удалив конструктор, если один из новых конструкторов также не имеет параметров, тем самым заменив конструктор по умолчанию. Автоматически предоставленный конструктор без параметров имеет тот же модификатор доступа, что и класс его объявления, поэтому любая замена должна иметь не меньший или больший доступ, если необходимо сохранить совместимость с существующими бинарными файлами.
Добавление или удаление параметра типа метода или конструктора само по себе не имеет никаких последствий для бинарной совместимости.
Если такой параметр типа используется в типе метода или конструктора, это может иметь обычные последствия для изменения указанного типа.
Переименование параметра типа метода или конструктора не оказывает никакого влияния на существующие бинарные файлы.
Изменение первого ограничения параметра типа метода или конструктора может изменить стирание (§4.6) любого члена, который использует этот параметр типа в собственном типе, и это может повлиять на бинарную совместимость. В частности:
-
Если параметр типа используется в качестве типа поля, эффект такой, как если бы поле было удалено, и добавлено поле с тем же именем, тип которого — новое стирание переменной типа.
-
Если параметр типа используется в качестве типа любого формального параметра метода, но не как возвращаемый тип, эффект такой, как если бы этот метод был удален и заменен новым методом, идентичным за исключением типов упомянутых формальных параметров, которые теперь имеют новое стирание параметра типа в качестве своего типа.
-
Если параметр типа используется в качестве возвращаемого типа метода, но не как тип какого-либо формального параметра метода, эффект такой, как если бы этот метод был удален и заменен новым методом, идентичным за исключением возвращаемого типа, который теперь является новым стиранием параметра типа.
-
Если параметр типа используется как возвращаемый тип метода и как тип одного или нескольких формальных параметров метода, эффект такой, как если бы этот метод был удален и заменен новым методом, идентичным за исключением возвращаемого типа, который теперь является новым стиранием параметра типа, и за исключением типов упомянутых формальных параметров, которые теперь имеют новое стирание параметра типа в качестве своих типов.
Изменение любого другого ограничения не влияет на бинарную совместимость.
Изменение имени формального параметра метода или конструктора не влияет на существующие бинарные файлы.
Изменение имени метода, типа формального параметра метода или конструктора, добавление или удаление параметра из объявления метода или конструктора создаёт метод или конструктор с новой сигнатурой, и имеет совокупный эффект удаления метода или конструктора со старой сигнатурой и добавления метода или конструктора с новой сигнатурой (§13.4.12).
Изменение типа последнего формального параметра метода с T[] на параметр с переменным числом аргументов (§8.4.1) типа T (т.е. на T...), и наоборот, не повлияет на существующие бинарные файлы.
Для целей бинарной совместимости, добавление или удаление метода или конструктора m, чья сигнатура включает переменные типов (§4.4) или параметризованные типы (§4.5), эквивалентно добавлению (соответственно, удалению) аналогичного метода, чья сигнатура является стиранием (§4.6) сигнатуры m.
Изменение типа результата метода, или замена типа результата на void, или замена void типом результата, имеет совокупный эффект удаления старого метода и добавления нового метода с новым типом результата или новым void результатом (см. §13.4.12).
Для целей бинарной совместимости, добавление или удаление метода или конструктора m, чьё возвращаемое значение включает переменные типов (§4.4) или параметризованные типы (§4.5), эквивалентно добавлению (соответственно, удалению) аналогичного метода, чьё возвращаемое значение является стиранием (§4.6) возвращаемого значения m.
Изменение метода, объявленного как abstract, на метод, не объявленный как abstract, не нарушает совместимость с существующими бинарными файлами.
Изменение метода, не объявленного как abstract, на метод, объявленный как abstract, нарушит совместимость с существующими бинарными файлами, которые ранее вызывали этот метод, вызвав AbstractMethodError.
Пример 13.4.16-1. Изменение метода на abstract
class Super { void out() { System.out.println("Out"); } }
class Test extends Super {
public static void main(String[] args) {
Test t = new Test();
System.out.println("Way ");
t.out();
}
}
Эта программа выводит:
Way Out
Предположим, что создана новая версия класса Super:
abstract class Super {
abstract void out();
}
Если Super перекомпилирован, но не Test, то запуск нового бинарного файла с существующим бинарным файлом Test приведёт к AbstractMethodError, потому что у класса Test нет реализации метода out и, следовательно, он (или она) является (или должна быть) abstract.
Изменение метода, объявленного как final, на метод, не объявленный как final, не нарушает совместимость с существующими бинарными файлами.
Изменение нестатического метода, не объявленного как final, на метод, объявленный как final, может нарушить совместимость с существующими бинарными файлами, которые зависят от возможности переопределения метода.
Пример 13.4.17-1. Изменение метода на final
class Super { void out() { System.out.println("out"); } }
class Test extends Super {
public static void main(String[] args) {
Test t = new Test();
t.out();
}
void out() { super.out(); }
}
Эта программа выводит:
out
Предположим, что создана новая версия класса Super:
class Super { final void out() { System.out.println("!"); } }
Если Super перекомпилирован, но не Test, то запуск нового бинарного файла с существующим бинарным файлом Test приведёт к VerifyError, потому что класс Test пытается неправильно переопределить нестатический метод out.
Изменение метода класса (static), не объявленного как final, на метод, объявленный как final, не нарушает совместимость с существующими бинарными файлами, поскольку метод не мог быть переопределён.
Добавление или удаление модификатора native метода не нарушает совместимость с существующими бинарными файлами.
Влияние изменений типов на существующие native методы, которые не перекомпилированы, выходит за рамки этой спецификации и должно быть указано в описании реализации. Реализации рекомендуется, но не требуется, реализовывать native методы таким образом, чтобы ограничить это влияние.
Если метод, не объявленный как private, также объявлен как static (то есть, метод класса) и изменён на метод, не объявленный как static (то есть, на нестатический метод), или наоборот, совместимость с существующими бинарными файлами может быть нарушена, что приведёт к ошибке времени компоновки, а именно IncompatibleClassChangeError, если эти методы используются существующими бинарными файлами. Такие изменения не рекомендуются в широко распространённом коде.
Добавление или удаление модификатора synchronized метода не нарушает совместимость с существующими бинарными файлами.
Изменения в разделе throws методов или конструкторов не нарушают совместимость с существующими бинарными файлами; эти разделы проверяются только на этапе компиляции.
Изменения в теле метода или конструктора не нарушают совместимость с существующими бинарными файлами.
Ключевое слово final в методе не означает, что метод может быть безопасно инлайнирован; оно означает только то, что метод не может быть переопределён. Вполне возможно, что в момент компоновки будет предоставлена новая версия этого метода. Кроме того, для целей рефлексии необходимо сохранить структуру исходной программы.
Таким образом, мы отмечаем, что Java-компилятор не может расширять метод inline во время компиляции. В общем случае мы рекомендуем реализациям использовать генерацию и оптимизацию кода с поздней связью (во время выполнения).
Добавление новых методов или конструкторов, перегружающих существующие методы или конструкторы, не нарушает совместимость с существующими бинарными файлами. Сигнатура, которая должна использоваться для каждого вызова, определялась во время компиляции этих существующих бинарных файлов; поэтому вновь добавленные методы или конструкторы не будут использоваться, даже если их сигнатуры и применимы, и более специфичны, чем изначально выбранная сигнатура.
Хотя добавление нового перегруженного метода или конструктора может вызвать ошибку времени компиляции при следующем компилировании класса или интерфейса, поскольку нет метода или конструктора, который является наиболее специфичным (§15.12.2.5), такая ошибка не возникает во время выполнения программы, так как разрешение перегрузки не выполняется во время выполнения.
Пример 13.4.23-1. Добавление перегруженного метода
class Super {
static void out(float f) {
System.out.println("float");
}
}
class Test {
public static void main(String[] args) {
Super.out(2);
}
}
Эта программа выводит:
float
Предположим, что создана новая версия класса Super:
class Super {
static void out(float f) { System.out.println("float"); }
static void out(int i) { System.out.println("int"); }
}
Если Super перекомпилирован, но не Test, то запуск нового бинарного файла с существующим бинарным файлом Test всё ещё выводит:
float
Однако, если Test перекомпилирован, используя этот новый Super, вывод будет следующим:
int
как, возможно, наивно ожидалось в предыдущем случае.
Если метод экземпляра добавляется в подкласс и переопределяет метод в суперклассе, то метод подкласса будет найден вызовами методов в существующих бинарниках, и эти бинарники не будут затронуты.
Если метод класса добавляется в класс, то этот метод не будет найден, если квалифицируемый тип ссылки не является типом подкласса.
Добавление, удаление или изменение статического инициализатора (§8.7) класса не повлияет на существующие бинарники.
Добавление или изменение порядка констант в типе перечисления не нарушит совместимость с существующими бинарниками.
Если существующий бинарник пытается получить доступ к константе перечисления, которая больше не существует, клиент потерпит неудачу во время выполнения с NoSuchFieldError. Поэтому такое изменение не рекомендуется для широко распространенных перечислений.
Во всех других отношениях правила бинарной совместимости для перечислений идентичны правилам для классов.
Этот раздел описывает влияние изменений в объявлении интерфейса и его членов на существующие бинарники.
Изменение интерфейса, который не объявлен public, на объявленный public, не нарушает совместимость с существующими бинарниками.
Если интерфейс, объявленный public, изменится на не объявленный public, то при связывании существующего бинарника, которому требуется, но больше нет доступа к типу интерфейса, будет выброшено IllegalAccessError. Поэтому такое изменение не рекомендуется для широко распространенных интерфейсов.
Изменения в иерархии интерфейсов вызывают ошибки аналогичным образом, как изменения в иерархии классов, как описано в §13.4.4. В частности, изменения, которые приводят к тому, что любой предыдущий суперинтерфейс класса больше не является суперинтерфейсом, могут нарушить совместимость с существующими бинарниками, что приведет к VerifyError.
Добавление метода в интерфейс не нарушает совместимость с существующими бинарниками.
Поле, добавленное в суперинтерфейс C, может скрыть поле, унаследованное от суперкласса C. Если исходная ссылка была на поле экземпляра, произойдет IncompatibleClassChangeError. Если исходная ссылка была присваиванием, произойдет IllegalAccessError.
Удаление члена из интерфейса может привести к ошибкам связывания в существующих бинарниках.
Пример 13.5.3-1. Удаление члена интерфейса
interface I { void hello(); }
class Test implements I {
public static void main(String[] args) {
I anI = new Test();
anI.hello();
}
public void hello() { System.out.println("hello"); }
}
Эта программа выводит:
hello
Предположим, что новая версия интерфейса I скомпилирована:
interface I {}
Если I перекомпилируется, но не Test, то запуск нового бинарника с существующим бинарником для Test приведет к NoSuchMethodError.
Воздействие изменений в параметрах типа интерфейса аналогично изменениям в параметрах типа класса.
Соображения по изменению объявлений полей в интерфейсах аналогичны соображениям для полей static final в классах, как описано в §13.4.8 и §13.4.9.
Соображения по изменению объявлений методов abstract в интерфейсах аналогичны соображениям для методов abstract в классах, как описано в §13.4.14, §13.4.15, §13.4.21 и §13.4.23.
Типы аннотаций ведут себя точно так же, как и любой другой интерфейс. Добавление или удаление элемента из типа аннотации аналогично добавлению или удалению метода. Существуют важные соображения, регулирующие другие изменения в типах аннотаций, но эти изменения не влияют на связывание бинарников виртуальной машиной Java. Скорее, такие изменения влияют на поведение рефлексивных API, которые манипулируют аннотациями. Документация этих API определяет их поведение при различных изменениях в базовых типах аннотаций.
Добавление или удаление аннотаций не влияет на правильное связывание бинарных представлений программ на языке программирования Java.
© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.