Глава 13. Бинарная совместимость
Содержание
- 13.1. Формат бинарного файла
- 13.2. Что такое и что не является бинарной совместимостью
- 13.3. Эволюция пакетов и модулей
- 13.4. Эволюция классов
-
- 13.4.1.
abstractклассы - 13.4.2.
sealed,non-sealedиfinalклассы - 13.4.3.
publicклассы - 13.4.4. Суперклассы и суперинтерфейсы
- 13.4.5. Параметры типа класса
- 13.4.6. Тело класса и объявления членов
- 13.4.7. Доступ к членам и конструкторам
- 13.4.8. Объявления полей
- 13.4.9.
finalполя иstaticконстантные переменные - 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.27. Эволюция record-классов
- 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), определяя, какие из этих изменений гарантируют сохранение бинарной совместимости, а какие нет.
Иногда используются ссылки вида: (JVMS §x.y), чтобы указать на понятия из Спецификации виртуальной машины Java, Java SE 17 издание.
END_OF_DOCUMENT_MARKER Программы должны быть скомпилированы либо в формат файлов class, указанный в спецификации Java Virtual Machine Specification, Java SE 17 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) — это двоичное имя класса или интерфейса, объявляющего метод, за которым следует
$, дескриптор метода (JVMS §4.3.3),$и, наконец, простое имя параметра типа. -
Двоичное имя параметра типа, объявленного в дженерическом конструкторе (§8.8.4) — это двоичное имя класса, объявляющего конструктор, за которым следует
$, дескриптор конструктора (JVMS §4.3.3),$и простое имя параметра типа.
-
-
Ссылка на другой класс или интерфейс должна быть символьной, используя двоичное имя класса или интерфейса.
-
Ссылка на поле, являющееся константой (§4.12.4), должна быть разрешена во время компиляции до значения V, заданного инициализатором константы.
Если такое поле является
static, то любая ссылка на него в двоичном файле, включая класс или интерфейс, который объявил поле, должна отсутствовать. Такое поле всегда должно казаться инициализированным (§12.4.2); значение по умолчанию для поля (если оно отличается от V) не должно наблюдаться.Если такое поле является не-
static, то ссылка на него в двоичном файле не должна присутствовать, за исключением класса, содержащего это поле. (Это будет класс, а не интерфейс, так как у интерфейсов есть толькоstaticполя.) Класс должен содержать код для установки значения поля в V во время создания экземпляра (§12.5). -
В случае законного выражения, обозначающего доступ к полю в классе C, ссылающегося на поле, названное
f, которое не является константой и объявлено в (возможно, другом) классе или интерфейсе D, квалифицирующий класс или интерфейс ссылки на поле определяется следующим образом:-
Если выражение ссылается через простое имя, и если
fявляется членом текущего класса или интерфейса C, то пусть Q будет C. Иначе, пусть Q будет самым вложенным лексически охватывающим классом или интерфейсом, членом которого являетсяf. В любом случае, Q — квалифицирующий класс или интерфейс ссылки. -
Если ссылка имеет вид ИмяТипа
.f, где ИмяТипа обозначает класс или интерфейс, то класс или интерфейс, обозначаемый ИмяТипа, и есть квалифицирующий класс или интерфейс ссылки. -
Если выражение имеет вид ИмяВыражения
.fили Первичное.f, то:-
Если тип во время компиляции ИмяВыражения или Первичное — это тип пересечения V1
&...&Vn (§4.9), то квалифицирующим классом или интерфейсом ссылки является стирание (§4.6) V1. -
В противном случае, стирание типа во время компиляции ИмяВыражения или Первичное — это квалифицирующий класс или интерфейс ссылки.
-
-
Если выражение имеет вид
super.f, то суперкласс C — квалифицирующий класс или интерфейс ссылки. -
Если выражение имеет вид ИмяТипа
.super.f, то суперкласс класса, обозначаемого ИмяТипа, — квалифицирующий класс или интерфейс ссылки.
Ссылка на
fдолжна быть скомпилирована в символьную ссылку на квалифицирующий класс или интерфейс ссылки, плюс простое имя поляf.Ссылка должна также включать символьные ссылки на стирание объявленного типа поля, чтобы верификатор мог проверить, что тип соответствует ожидаемому.
-
-
Дано выражение вызова метода или выражение ссылки на метод в классе или интерфейсе C, ссылающееся на метод под именем
m, объявленный (или неявно объявленный (§9.2)) в (возможно, отличном) классе или интерфейсе D, мы определяем квалифицирующий класс или интерфейс вызова метода следующим образом:-
Если D является
Object, то квалифицирующий класс или интерфейс вызова метода являетсяObject. -
В противном случае:
-
Если метод ссылается с помощью простого имени, то если
mявляется членом текущего класса или интерфейса C, пусть Q будет C; в противном случае, пусть Q будет внутренним лексически окружающим классом или интерфейсом, объявление которого является членомm. В любом случае, Q является квалифицирующим классом или интерфейсом вызова метода. -
Если выражение имеет вид ТипИмя
.mили ТипСсылка::m, то класс или интерфейс, обозначаемый ТипИмя, или стирание ТипСсылка, является квалифицирующим классом или интерфейсом вызова метода. -
Если выражение имеет вид ИмяВыражения
.mили Первичное.mили ИмяВыражения::mили Первичное::m, то:-
Если тип времени компиляции ИмяВыражения или Первичное является пересечением типов V1
&...&Vn, то квалифицирующий класс или интерфейс вызова метода является стиранием V1. -
В противном случае, стирание типа времени компиляции ИмяВыражения или Первичное является квалифицирующим классом или интерфейсом вызова метода.
-
-
Если выражение имеет вид
super.mилиsuper::m, то суперкласс C является квалифицирующим классом или интерфейсом вызова метода. -
Если выражение имеет вид ТипИмя
.super.mили ТипИмя.super::m, то если ТипИмя обозначает класс X, суперкласс X является квалифицирующим классом или интерфейсом вызова метода; если ТипИмя обозначает интерфейс X, то X является квалифицирующим классом или интерфейсом вызова метода.
-
Ссылка на метод должна быть разрешена во время компиляции в символическую ссылку на квалифицирующий класс или интерфейс вызова метода, плюс стирание объявленной сигнатуры (§8.4.2) метода. Подпись метода должна включать все указанное в §15.12.3:
-
Простое имя метода
-
Количество параметров метода
-
Символическая ссылка на тип каждого параметра
Ссылка на метод также должна включать либо символическую ссылку на стирание типа возвращаемого значения обозначенного метода, либо указание, что обозначенный метод объявлен
voidи не возвращает значение. -
-
Дано выражение создания экземпляра класса (§15.9) или явное утверждение вызова конструктора (§8.8.7.1) или выражение ссылки на метод вида ТипКласс
::new(§15.13) в классе или интерфейсе C, ссылающееся на конструкторm, объявленный в (возможно, отличном) классе или интерфейсе D, мы определяем квалифицирующий класс вызова конструктора следующим образом:-
Если выражение имеет вид
newD(...)или ИмяВыражения.newD(...)или Первичное.newD(...)или D::new, то квалифицирующим классом вызова конструктора является D. -
Если выражение имеет вид
newD(...){...}или ИмяВыражения.newD(...){...}или Первичное.newD(...){...}, то квалифицирующим классом вызова конструктора является анонимный класс, объявленный выражением. -
Если выражение имеет вид
super(...)или ИмяВыражения.super(...)или Первичное.super(...), то квалифицирующим классом вызова конструктора является непосредственный суперкласс C. -
Если выражение имеет вид
this(...), то квалифицирующим классом вызова конструктора является C.
Ссылка на конструктор должна быть разрешена во время компиляции в символическую ссылку на квалифицирующий класс вызова конструктора, плюс объявленная подпись конструктора (§8.8.2). Подпись конструктора должна включать следующее:
-
Количество параметров конструктора
-
Символическая ссылка на тип каждого формального параметра
-
Бинарное представление класса или интерфейса также должно содержать следующее:
-
Если это класс и он не
Object, то символическая ссылка на непосредственный суперкласс этого класса. -
Символическая ссылка на каждый непосредственный суперинтерфейс, если таковые имеются.
-
Спецификация каждого поля, объявленного в классе или интерфейсе, представленная простым именем поля и символической ссылкой на стирание типа поля.
-
Если это класс, то стёртый сигнатура каждого конструктора, как описано выше.
-
Для каждого метода, объявленного в классе или интерфейсе (исключая, для интерфейса, его неявно объявленные методы (§9.2)), его стёртый сигнатура и возвращаемый тип, как описано выше.
-
Код, необходимый для реализации класса или интерфейса:
-
Каждый класс или интерфейс должен содержать достаточную информацию для восстановления его канонического имени (§6.7).
-
Каждый вложенный класс или интерфейс должен содержать достаточную информацию для восстановления его модификатора доступа уровня исходного кода (§6.6).
-
Каждый вложенный класс или интерфейс должен содержать символическую ссылку на его непосредственно окружающий класс или интерфейс (§8.1.3).
-
Каждый класс или интерфейс должен содержать символические ссылки на все его вложенные классы и интерфейсы (§8.5, §9.5), а также на все другие вложенные классы и интерфейсы, объявленные внутри его тела.
-
Конструкт, выпущенный компилятором Java, должен быть помечен как синтетический, если он не соответствует конструкту, явно или неявно объявленному в исходном коде, за исключением метода инициализации класса (JVMS §2.9).
-
Конструкт, выпущенный компилятором Java, должен быть помечен как обязательный, если он соответствует формальному параметру, неявно объявленному в исходном коде (§8.8.1, §8.8.9, §8.9.3, §15.9.5.1).
Следующие формальные параметры объявлены неявно в исходном коде:
-
Первый формальный параметр конструктора не-
privateвложенного члена класса (§8.8.1, §8.8.9). -
Первый формальный параметр анонимного конструктора анонимного класса, суперкласс которого является вложенным классом (не в статическом контексте) (§15.9.5.1).
-
Формальный параметр
nameметодаvalueOf, который неявно объявлен в классе перечисления (§8.9.3). -
Формальные параметры компактного конструктора класса записи (§8.10.4).
Для справки, следующие конструкции объявлены неявно в исходном коде, но не помечены как обязательные, потому что только формальные параметры и модули могут быть помечены как обязательные в файле class (JVMS §4.7.24, JVMS §4.7.25):
-
Конструкторы по умолчанию обычных и перечислительных классов (§8.8.9, §8.9.2)
-
Канонические конструкторы классов записи (§8.10.4)
-
Анонимные конструкторы (§15.9.5.1)
-
Методы
valuesиvalueOfперечислительных классов (§8.9.3) -
Определённые
publicполя перечислительных классов (§8.9.3) -
Определённые
privateполя иpublicметоды классов записи (§8.10.3) -
Определённые
publicметоды интерфейсов (§9.2) -
Контейнерные аннотации (§9.7.5)
Файл class, соответствующий объявлению модуля, должен иметь свойства файла class для класса, бинарное имя которого module-info, и который не имеет суперклассов, суперинтерфейсов, полей и методов. Кроме того, двоичное представление модуля должно содержать все следующее:
-
Спецификация имени модуля, заданная как символическая ссылка на имя, указанное после
module. Также спецификация должна включать, является ли модуль обычным или открытым (§7.7). -
Спецификация каждой зависимости, обозначенной директивой
requires, заданная как символическая ссылка на имя модуля, указанное в директиве (§7.7.1). Также спецификация должна включать, является ли зависимостьtransitiveи является ли зависимостьstatic. -
Спецификация каждого пакета, обозначенного директивой
exportsилиopens, заданная как символическая ссылка на имя пакета, указанного в директиве (§7.7.2). Также, если директива была квалифицирована, спецификация должна предоставить символические ссылки на имена модулей, указанные вto-разделе директивы. -
Спецификация каждой службы, обозначенной директивой
uses, заданная как символическая ссылка на имя класса или интерфейса, указанного в директиве (§7.7.3). -
Спецификация поставщиков услуг, обозначенных директивой
provides, заданная как символические ссылки на имена классов и интерфейсов, указанные вwith-разделе директивы (§7.7.4). Также спецификация должна предоставить символическую ссылку на имя класса или интерфейса, указанного как служба в директиве.
В следующих разделах обсуждаются изменения, которые могут быть внесены в объявления классов и интерфейсов без нарушения совместимости с существующими бинарными файлами. В соответствии с требованиями к переводу, виртуальная машина Java и её формат файла class поддерживают эти изменения. Любой другой допустимый бинарный формат, такой как сжатое или зашифрованное представление, которое отображается обратно в файлы class загрузчиком класса в соответствии с указанными выше требованиями, также будет поддерживать эти изменения.
Изменение типа является бинарно совместимым с (эквивалентно, не нарушает бинарную совместимость с) ранее существующими бинарными файлами, если ранее существующие бинарные файлы, которые ранее связывались без ошибок, будут продолжать связываться без ошибок.
Бинарные файлы компилируются таким образом, чтобы полагаться на доступные члены и конструкторы других классов и интерфейсов. Для сохранения бинарной совместимости класс или интерфейс должен рассматривать свои доступные члены и конструкторы, их существование и поведение, как договор с пользователями.
Язык программирования Java разработан таким образом, чтобы предотвращать добавление пунктов в контракты и случайные столкновения имен, которые могли бы нарушить бинарную совместимость. В частности, добавление дополнительных методов перегрузки с определенным именем метода не нарушает совместимость с ранее существующими бинарными файлами. Подпись метода, которую будет использовать ранее существующий бинарный файл для поиска метода, выбирается алгоритмом разрешения перегрузки во время компиляции (§15.12.2).
Если язык программирования Java был разработан таким образом, чтобы конкретный метод, который должен быть выполнен, выбирался во время выполнения, то такая неоднозначность могла бы быть обнаружена во время выполнения. Такое правило подразумевало бы, что добавление дополнительного перегруженного метода, чтобы сделать неоднозначность возможной в месте вызова, могло бы нарушить совместимость с неизвестным количеством ранее существующих бинарных файлов. См. §13.4.23 для более подробного обсуждения.
Бинарная совместимость не то же самое, что и совместимость исходного кода. В частности, пример в §13.4.6 показывает, что набор совместимых бинарных файлов может быть создан из исходных кодов, которые не будут компилироваться вместе. Этот пример типичен: добавляется новое объявление, изменяющее значение имени в неизмененной части исходного кода, в то время как ранее существующий бинарный файл для этой неизмененной части исходного кода сохраняет полностью квалифицированное, предыдущее значение имени. Для создания согласованного набора исходного кода необходимо предоставить квалифицированное имя или выражение доступа к полю, соответствующее предыдущему значению.
Новый верхнеуровневый класс или интерфейс может быть добавлен в пакет без нарушения совместимости с ранее существующими бинарными файлами, при условии, что новый класс или интерфейс не повторно использует имя, ранее присвоенное несвязанному классу или интерфейсу. Если новый класс или интерфейс повторно использует имя, ранее присвоенное несвязанному классу или интерфейсу, может возникнуть конфликт, поскольку бинарные файлы для обоих классов или интерфейсов не могут быть загружены одним и тем же загрузчиком классов.
Изменения в верхнеуровневых классах и интерфейсах, которые не являются public и которые не являются надклассом или надинтерфейсом, соответственно, для public класса или интерфейса, влияют только на классы и интерфейсы в пакете, в котором они объявлены. Такие классы и интерфейсы могут быть удалены или иным образом изменены, даже если несовместимости описаны иначе здесь, при условии, что соответствующие бинарные файлы этого пакета обновляются вместе.
Если модуль, который был объявлен для экспорта или открытия пакета, изменен так, что не экспортирует или не открывает пакет, или экспортирует или открывает пакет для другого набора друзей, то выбрасывается IllegalAccessError, если связан ранее существующий бинарный файл, которому необходим, но который больше не имеет доступа к public и protected классам и интерфейсам пакета. Такое изменение не рекомендуется для модулей, которые широко распространяются.
Если модуль не был объявлен для экспорта или открытия данного пакета, то изменение модуля на экспорт или открытие пакета не нарушает совместимость с ранее существующими бинарными файлами. Однако изменение модуля на экспорт пакета может помешать запуску программы, поскольку любой модуль, который считывает модуль, может также считывать какой-либо другой модуль, который экспортирует пакет с тем же именем.
Добавление requires директивы в объявление модуля или добавление модификатора transitive к requires директиве не нарушает совместимости с ранее существующими бинарными файлами. Однако это может помешать запуску программы, так как модуль теперь может считывать несколько модулей, которые экспортируют пакеты с одинаковым именем.
Удаление requires директивы в объявлении модуля или удаление модификатора transitive из requires директивы может нарушить совместимость с любым ранее существующим бинарным файлом, который полагался на директиву или модификатор для чтения данного модуля при ссылке на классы и интерфейсы, экспортируемые этим модулем. При такой ссылке из ранее существующего бинарного файла может быть сгенерировано IllegalAccessError.
Добавление или удаление uses или provides директивы в объявлении модуля не нарушает совместимости с ранее существующими бинарными файлами.
В этом разделе описываются последствия изменений в объявлении класса и его членов и конструкторов на существующие двоичные файлы.
Если класс, который не был объявлен abstract, изменяется на объявление abstract, то существующие двоичные файлы, которые пытаются создать новые экземпляры этого класса, будут выбрасывать либо InstantiationError во время линковки, либо (если используется рефлексивный метод) InstantiationException во время выполнения; поэтому такое изменение не рекомендуется для широко распространённых классов.
Изменение класса, объявленного abstract, на необъявление abstract не нарушает совместимость с существующими двоичными файлами.
Если класс, который был свободно расширяемым (§8.1.1.2), изменяется на объявление sealed, то IncompatibleClassChangeError выбрасывается, если двоичный файл существующего подкласса этого класса загружается и не является разрешенным прямым подклассом этого класса (§8.1.6); такое изменение не рекомендуется для широко распространённых классов.
Изменение класса, объявленного final, на объявление sealed не нарушает совместимость с существующими двоичными файлами.
Добавление класса в набор разрешенных прямых подклассов sealed класса не нарушает совместимость с существующими двоичными файлами.
Если класс удаляется из набора разрешенных прямых подклассов sealed класса, то IncompatibleClassChangeError выбрасывается, если загружен существующий двоичный файл удаленного класса.
Удаление модификатора sealed из класса, который не имеет sealed прямого суперкласса или sealed прямого суперинтерфейса, не нарушает совместимость с существующими двоичными файлами.
Если у запечатанного класса C был sealed прямой суперкласс или sealed прямой суперинтерфейс, то удаление модификатора sealed предотвратит перекомпиляцию C, так как каждый класс с sealed прямым суперклассом или sealed прямым суперинтерфейсом должен быть либо final, либо sealed, либо non-sealed.
Изменение класса, объявленного sealed, на объявление non-sealed не нарушает совместимость с существующими двоичными файлами.
Изменение класса, объявленного final, на объявление non-sealed не нарушает совместимость с существующими двоичными файлами.
non-sealed класс C должен иметь sealed прямой суперкласс или sealed прямой суперинтерфейс (§8.1.1.2). Удаление модификатора non-sealed предотвратит перекомпиляцию C, так как каждый класс с sealed прямым суперклассом или sealed прямым суперинтерфейсом должен быть либо final, либо sealed, либо non-sealed.
Если класс, не объявленный final, изменяется на объявление final, то IncompatibleClassChangeError выбрасывается, если загружен двоичный файл существующего подкласса этого класса, так как final классы не могут иметь подклассы; такое изменение не рекомендуется для широко распространённых классов.
Удаление модификатора final из класса, который не имеет sealed прямого суперкласса или sealed прямого суперинтерфейса, не нарушает совместимость с существующими двоичными файлами.
Если final класс C имел sealed прямой суперкласс или sealed прямой суперинтерфейс, то удаление модификатора final предотвратит перекомпиляцию C, так как каждый класс с sealed прямым суперклассом или sealed прямым суперинтерфейсом должен быть либо final, либо sealed, либо non-sealed (§8.1.1.2).
Изменение класса, не объявленного 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.
Вывод: реализация, в которой отсутствует верификатор или она не используется, не будет поддерживать безопасность типов и, следовательно, не является валидной реализацией.
Пример 13.4.4-2. Введение суперкласса
В общих чертах, существуют различные ситуации, когда преобразование класса, совместимое с двоичными файлами для клиента, может быть несовместимым с исходным кодом для этого клиента.
Например, требование, чтобы альтернативы в многократном catch предложении (§14.20) не были подклассами или суперклассами друг друга, является только ограничением исходного кода. Следующий код:
try {
failByThrowingAorB();
} catch (A|B e) {
...
}
является законным, при условии, что A и B не имеют отношения подкласс/суперкласс во время компиляции. После этого для A и B будет двоичная совместимость в отношении этого клиента, если они будут изменены таким образом, чтобы иметь такое отношение. Предыдущий скомпилированный код будет продолжать выполняться, но поскольку изменение не совместимо с исходным кодом для этого клиента, код не может быть перекомпилирован.
Добавление или удаление параметра типа класса не оказывает влияния на бинарную совместимость.
Если такой параметр типа используется в типе поля или метода, это может иметь обычные последствия изменения указанного типа.
Переименование параметра типа класса не влияет на существующие бинарные файлы.
Изменение первого ограничения параметра типа класса может изменить стирание (§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 с квалифицирующим классом C. Далее предположим, что f — это фактически поле-инстанс (соответственно static), объявленное в суперклассе C, S, и что тип f — X.
Если новое поле типа X с тем же именем, что и f, добавлено в подкласс S, который является суперклассом C или C самим по себе, может возникнуть ошибка линковки. Такая ошибка линковки произойдёт только в том случае, если, помимо вышеперечисленного, выполняется одно из следующих условий:
-
Новое поле имеет меньшую доступность, чем старое.
-
Новое поле является полем
static(соответственно, полем-инстансом).
В частности, ошибка линковки не возникнет в случае, когда класс больше не может быть перекомпилирован, потому что доступ к полю ранее ссылался на поле суперкласса с несовместимым типом. Ранее скомпилированный класс с такой ссылкой будет продолжать ссылаться на поле, объявленное в суперклассе.
Пример 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.
Удаление поля из класса нарушит совместимость с любыми существующими бинарными файлами, которые ссылаются на это поле, и будет выброшена ошибка NoSuchFieldError при линковке такой ссылки из существующего бинарного файла. Только поля private могут быть безопасно удалены из широко распространённого класса.
Для целей бинарной совместимости добавление или удаление поля f, тип которого включает переменные типов (§4.4) или параметризованные типы (§4.5), эквивалентно добавлению (соответственно удалению) поля с тем же именем, тип которого является стиранием (§4.6) типа f.
Если поле, которое не было объявлено final, изменяется на объявление final, это может нарушить совместимость с существующими двоичными файлами, которые пытаются присвоить новые значения полю.
Пример 13.4.9-1. Изменение переменной на final
class Super { char s; }
class Test extends Super {
public static void main(String[] args) {
Super x = new Super();
x.s = 'a';
System.out.println(x.s);
}
}
Эта программа выводит:
a
Предположим, что создана новая версия класса Super:
class Super { final char s = 'b'; }
Если Super перекомпилирован, но не Test, то запуск нового двоичного файла с существующим двоичным файлом Test приведет к IllegalAccessError.
Удаление ключевого слова final или изменение значения, к которому инициализировано поле, не нарушает совместимость с существующими двоичными файлами.
Если поле является константой (§4.12.4), и, более того, является static, то удаление ключевого слова final или изменение его значения не нарушит совместимость с существующими двоичными файлами, вызывая их неработоспособность, но они не увидят новых значений для использования поля, если не перекомпилированы. Этот результат является побочным эффектом решения по поддержке условной компиляции (§14.22). (Можно предположить, что новое значение не видно, если используется в константном выражении (§15.29), но видно в противном случае. Это не так; существующие двоичные файлы вообще не видят нового значения.)
Лучший способ избежать проблем с «непостоянными константами» в широко распространенном коде — использовать static константные переменные только для значений, которые, по всей вероятности, никогда не изменятся. Помимо истинных математических констант, мы рекомендуем, чтобы исходный код очень экономно использовал static константные переменные.
Если требуется только для чтения final, лучшим выбором является объявление private static переменной и соответствующего метода доступа для получения ее значения. Таким образом, мы рекомендуем:
private static int N;
public static int getN() { return N; }
вместо:
public static final int N = ...;
Нет проблем с:
public static int N = ...;
если N не обязательно должен быть только для чтения.
Если поле, которое не объявлено private, не было объявлено static и изменено на объявление static, или наоборот, то произойдет ошибка связи, конкретно ошибка IncompatibleClassChangeError, если поле используется существующим двоичным файлом, который ожидал поле другого типа. Такие изменения не рекомендуются в коде, который широко распространен.
Добавление или удаление модификатора transient поля не нарушает совместимость с существующими двоичными файлами.
Добавление метода или конструктора в класс не нарушит совместимость с любыми существующими двоичными файлами, даже в том случае, когда класс больше не может быть перекомпилирован, потому что вызов ранее ссылался на метод или конструктор суперкласса с несовместимым типом. Предыдущий скомпилированный класс с такой ссылкой будет по-прежнему ссылаться на метод или конструктор, объявленный в суперклассе.
Предположим ссылку на метод m с квалифицирующим классом C. Предположим также, что m является фактически методом экземпляра (соответственно static) метода, объявленного в суперклассе C, S.
Если к подклассу S, который является суперклассом C или самим C, добавлен новый метод типа X с той же сигнатурой и возвращаемым типом, что и m, может произойти ошибка связи. Такая ошибка связи произойдет только в том случае, если, помимо вышесказанного, верно одно из следующих утверждений:
-
Новый метод менее доступен, чем старый.
-
Новый метод является статическим (соответственно методом экземпляра) методом.
Удаление метода или конструктора из класса может нарушить совместимость с любым существующим двоичным файлом, который ссылался на этот метод или конструктор; может быть выброшена ошибка NoSuchMethodError при связывании такой ссылки из существующего двоичного файла. Такая ошибка произойдёт только если в суперклассе нет метода с совпадающей сигнатурой и возвращаемым типом.
Если исходный код для класса, не являющегося внутренним классом, не содержит объявленных конструкторов, то неявно объявляется конструктор по умолчанию без параметров (§8.8.9). Добавление одного или нескольких объявлений конструкторов в исходный код такого класса предотвратит неявное объявление этого конструктора по умолчанию, эффективно удалив конструктор, за исключением случая, когда один из новых конструкторов также не имеет параметров, тем самым заменяя конструктор по умолчанию. Конструктор по умолчанию без параметров получает тот же модификатор доступа, что и класс объявления, поэтому любая замена должна иметь не меньший или больший доступ, чтобы сохранить совместимость с существующими двоичными файлами.
Добавление или удаление параметра типа метода или конструктора само по себе не имеет никакого влияния на двоичную совместимость.
Если такой параметр типа используется в типе метода или конструктора, это может иметь обычные последствия изменения указанного типа.
Переименование параметра типа метода или конструктора не влияет на существующие двоичные файлы.
Изменение первой границы параметра типа метода или конструктора может изменить стирание (§4.6) любого члена, который использует этот параметр типа в своем типе, и это может повлиять на двоичную совместимость. В частности:
-
Если параметр типа используется в качестве типа поля, эффект такой, как если бы поле было удалено, и было добавлено поле с тем же именем, тип которого является новым стиранием переменной типа.
-
Если параметр типа используется в качестве типа любого формального параметра метода, но не в качестве возвращаемого типа, то эффект такой, как если бы этот метод был удален, и заменен новым методом, который идентичен, за исключением типов упомянутых формальных параметров, которые теперь имеют новое стирание параметра типа в качестве своего типа.
-
Если параметр типа используется в качестве возвращаемого типа метода, но не в качестве типа ни одного формального параметра метода, то эффект такой, как если бы этот метод был удален, и заменен новым методом, который идентичен, за исключением возвращаемого типа, который теперь является новым стиранием параметра типа.
-
Если параметр типа используется в качестве возвращаемого типа метода и в качестве типа одного или нескольких формальных параметров метода, то эффект такой, как если бы этот метод был удален, и заменен новым методом, который идентичен, за исключением возвращаемого типа, который теперь является новым стиранием параметра типа, и за исключением типов упомянутых формальных параметров, которые теперь имеют новое стирание параметра типа в качестве своего типа.
Изменение любой другой границы не влияет на двоичную совместимость.
Изменение имени формального параметра метода или конструктора не влияет на существующие двоичные файлы.
Изменение имени метода или типа формального параметра метода или конструктора, или добавление параметра к объявлению метода или конструктора, или удаление параметра из объявления метода или конструктора создает метод или конструктор с новой сигнатурой и имеет совокупный эффект удаления метода или конструктора со старой сигнатурой и добавления метода или конструктора с новой сигнатурой (§13.4.12).
Изменение типа последнего формального параметра метода с T[] на параметр с переменным числом аргументов типа T, то есть на T... (§8.4.1), и наоборот, не влияет на существующие двоичные файлы.
Для целей двоичной совместимости добавление или удаление метода или конструктора 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 приведёт к IncompatibleClassChangeError, потому что класс Test неправильно пытается переопределить инстансный метод out.
Изменение метода класса (static), который не объявлен final, на объявленный final, не нарушает совместимость с существующими бинарными файлами, так как метод не мог быть переопределён.
Добавление или удаление модификатора native метода не нарушает совместимость с существующими бинарными файлами.
Влияние изменений типов на существующие методы native, которые не перекомпилированы, выходит за рамки данного спецификации и должно быть предоставлено в описании реализации. Реализации рекомендуются, но не требуются, чтобы реализовать методы native таким образом, чтобы ограничить такое влияние.
Если метод, который не объявлен private, также объявлен как static (то есть, метод класса) и изменяется на не объявленный static (то есть, инстансный метод), или наоборот, то совместимость с существующими бинарными файлами может быть нарушена, что приведёт к ошибке времени компоновки, а именно IncompatibleClassChangeError, если эти методы используются существующими бинарными файлами. Такие изменения не рекомендуются в коде, который широко распространён.
Добавление или удаление модификатора synchronized метода не нарушает совместимость с существующими бинарными файлами.
Изменения в части throws методов или конструкторов не нарушают совместимость с существующими бинарными файлами; эти части проверяются только на этапе компиляции.
Изменения в теле метода или конструктора не нарушают совместимость с существующими бинарными файлами.
Ключевое слово final для метода не означает, что метод может быть безопасно встроен; оно означает только, что метод не может быть переопределён. Всё ещё возможно, что новая версия этого метода будет предоставлена во время компоновки. Кроме того, структура исходной программы должна быть сохранена для целей рефлексии.
Поэтому отметим, что Java-компилятор не может встроить метод в режиме компиляции. В общем, мы рекомендуем реализациям использовать генерацию и оптимизацию кода на поздних этапах (во время выполнения).
Добавление новых методов или конструкторов, которые перегружают существующие методы или конструкторы, не нарушает совместимость с существующими бинарными файлами. Подпись, которая должна использоваться для каждого вызова, определялась во время компиляции этих существующих бинарных файлов; поэтому вновь добавленные методы или конструкторы не будут использоваться, даже если их подписи одновременно применимы и более специфичны, чем изначально выбранная подпись.
Хотя добавление нового перегруженного метода или конструктора может вызвать ошибку времени компиляции в следующий раз, когда класс или интерфейс компилируется, потому что нет метода или конструктора, который является наиболее специфичным (§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) класса не повлияет на существующие бинарные файлы.
Добавление или изменение порядка констант перечисления в классе перечисления не нарушит совместимость с существующими бинарными файлами.
Удаление константы перечисления из класса перечисления удалит поле public, соответствующее константе перечисления (§8.9.3). Последствия описаны в §13.4.8. Такое изменение не рекомендуется для широко распространённых классов перечислений.
Во всех остальных отношениях правила бинарной совместимости для классов перечислений идентичны правилам для обычных классов.
Добавление, удаление, изменение или переупорядочивание компонентов записи в классе записи может нарушить совместимость с существующими двоичными файлами, которые не были перекомпилированы; такое изменение не рекомендуется для широко распространенных классов записей.
Более точно, добавление, удаление, изменение или переупорядочивание компонентов записи может изменить соответствующие неявные объявления полей компонентов и методов-доступов, а также изменить сигнатуру и реализацию канонического конструктора и других поддерживающих методов, со следствиями, указанными в §13.4.8 и §13.4.12.
Во всех других отношениях правила двоичной совместимости для классов записей идентичны правилам для обычных классов.
В этом разделе описывается влияние изменений в объявлении интерфейса и его членов на существующие двоичные файлы.
Изменение интерфейса, который не объявлен public, на интерфейс, объявленный public, не нарушает совместимость с существующими двоичными файлами.
Если интерфейс, объявленный public, изменяется на интерфейс, не объявленный public, то при подключении существующего двоичного файла, которому нужен, но больше недоступен тип интерфейса, выбрасывается IllegalAccessError. Поэтому такие изменения не рекомендуются для широко распространённых интерфейсов.
Если интерфейс, свободно расширяемый (§9.1.1.4), изменяется на интерфейс, объявленный sealed, то при загрузке двоичного файла подкласса или подинтерфейса этого интерфейса, который не является разрешённым прямым подклассом или подинтерфейсом этого интерфейса (§9.1.4), выбрасывается IncompatibleClassChangeError. Такие изменения не рекомендуются для широко распространённых классов.
Добавление класса или интерфейса в набор разрешённых прямых подклассов или подинтерфейсов, соответственно, sealed интерфейса не нарушит совместимости с существующими двоичными файлами.
Если класс или интерфейс удаляется из набора разрешённых прямых подклассов или подинтерфейсов sealed интерфейса, то при загрузке существующего двоичного файла удалённого класса или интерфейса выбрасывается IncompatibleClassChangeError.
Изменение интерфейса, объявленного sealed, на интерфейс, объявленный non-sealed, не нарушает совместимость с существующими двоичными файлами.
non-sealed интерфейс I должен иметь sealed прямой суперинтерфейс. Удаление модификатора non-sealed помешает перекомпиляции I, так как каждый интерфейс с sealed прямым суперинтерфейсом должен быть sealed или non-sealed.
Удаление модификатора sealed из интерфейса, у которого нет sealed прямого суперинтерфейса, не нарушит совместимости с существующими двоичными файлами.
Если для запечатанного интерфейса I был sealed прямой суперинтерфейс, то удаление модификатора sealed помешает перекомпиляции I, так как каждый интерфейс с sealed прямым суперинтерфейсом должен быть sealed или non-sealed.
Изменения в иерархии интерфейсов вызывают ошибки аналогично изменениям в иерархии классов, как описано в §13.4.4. В частности, изменения, которые приводят к тому, что предыдущий суперинтерфейс класса больше не является суперинтерфейсом, могут нарушить совместимость с существующими двоичными файлами, что приведёт к VerifyError.
Добавление abstract, private или static метода в интерфейс не нарушает совместимость с существующими двоичными файлами.
Добавление поля в суперинтерфейс C может скрыть поле, унаследованное от суперкласса C. Если исходная ссылка была на поле экземпляра, произойдёт IncompatibleClassChangeError. Если исходная ссылка была присваиванием, произойдёт IllegalAccessError.
Удаление члена из интерфейса может вызвать ошибки при компоновке в существующих двоичных файлах.
Пример 13.5.4-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.
Соображения по изменению объявлений методов в интерфейсах включают в себя соображения по изменению методов в классах, как описано в §13.4.7, §13.4.14, §13.4.15, §13.4.19, §13.4.21, §13.4.22 и §13.4.23.
Добавление default метода или изменение метода с abstract на default не нарушает совместимость с существующими двоичными файлами, но может вызвать IncompatibleClassChangeError, если существующий двоичный файл попытается вызвать метод. Эта ошибка возникает, если квалифицирующий интерфейс вызова метода, K, является подинтерфейсом двух интерфейсов, I и J, где оба I и J объявляют default метод с той же сигнатурой и результатом, и ни I, ни J не являются подинтерфейсом другого.
Другими словами, добавление метода по умолчанию является изменением, совместимым с двоичными файлами, поскольку оно не вносит ошибок при компоновке, даже если вносит ошибки при компиляции или вызове. На практике риск случайных конфликтов, возникающих при добавлении метода по умолчанию, аналогичен риску, связанному с добавлением нового метода в не-final класс. В случае конфликта, добавление метода в класс маловероятно вызовет LinkageError, но случайное переопределение метода в потомке может привести к непредсказуемому поведению метода. Оба изменения могут вызвать ошибки при компиляции.
Пример 13.5.7-1. Добавление метода по умолчанию
interface Painter {
default void draw() {
System.out.println("Here's a picture...");
}
}
interface Cowboy {}
public class CowboyArtist implements Cowboy, Painter {
public static void main(String... args) {
new CowboyArtist().draw();
}
}
Эта программа выводит:
Here's a picture...
Предположим, что метод по умолчанию добавлен в Cowboy:
interface Cowboy {
default void draw() {
System.out.println("Bang!");
}
}
Если Cowboy перекомпилирована, но CowboyArtist нет, то запуск нового двоичного файла с существующим двоичным файлом для CowboyArtist скомпонуется без ошибок, но вызовет IncompatibleClassChangeError, когда main попытается вызвать draw().
Интерфейсы аннотаций ведут себя точно так же, как и любые другие интерфейсы. Добавление или удаление элемента из интерфейса аннотации аналогично добавлению или удалению метода. Существуют важные соображения, регулирующие другие изменения в интерфейсах аннотаций, такие как придание интерфейсу аннотации возможности повторения (§9.6.3), но они не влияют на компоновку двоичных файлов виртуальной машиной Java. Скорее, такие изменения влияют на поведение рефлексивных API в платформе Java SE, которые раскрывают наличие аннотаций в программе. Спецификации API описывают их поведение при различных изменениях в базовых интерфейсах аннотаций (§1.4).
Добавление или удаление аннотаций не влияет на правильную компоновку двоичных представлений программ на языке программирования Java.
© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.