Глава 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Поля и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.1.
- 13.5. Эволюция интерфейсов
Инструменты разработки для языка программирования Java должны поддерживать автоматическую перекомпиляцию при необходимости, когда доступен исходный код. Некоторые реализации могут также хранить исходный код и бинарные файлы типов в базе данных с версиями и реализовывать механизм, использующий средства целостности базы данных для предотвращения ошибок связывания, предоставляя бинарно-совместимые версии типов клиентам.
Разработчики пакетов и классов, которые предполагается распространять широко, сталкиваются с другими проблемами. В Интернете, который является нашим любимым примером широко распределенной системы, часто бывает непрактично или невозможно автоматически перекомпилировать существующие бинарные файлы, которые напрямую или косвенно зависят от типа, который должен быть изменен. Вместо этого данное спецификация определяет набор изменений, которые разработчики могут внести в пакет или класс или интерфейс, сохраняя (не нарушая) совместимость с существующими бинарными файлами.
В рамках Бинарной совместимости при выпуске новых версий в SOM (Форман, Коннер, Данфорт и Рейпер, Материалы конференции 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 11.
END_OF_DOCUMENT_MARKERПрограммы должны быть скомпилированы либо в формате файла class, указанном в спецификации Виртуальной машины Java, издание Java SE 11, либо в представление, которое может быть преобразовано в этот формат загрузчиком классов, написанным на языке программирования 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, то пусть T будет C. В противном случае, пусть T — самый внутренний лексически окружающий тип объявления, членом которого являетсяf. В любом случае, T — квалифицирующий тип ссылки. -
Если ссылка имеет вид ИмяТипа
.f, где ИмяТипа обозначает класс или интерфейс, то класс или интерфейс, обозначенный ИмяТипа, является квалифицирующим типом ссылки. -
Если выражение имеет вид ИмяВыражения
.fили Основное.f, то:-
Если тип во время компиляции ИмяВыражения или Основное — пересечение типов V1
&...&Vn (§4.9), то квалифицирующим типом ссылки является V1. -
В противном случае, тип во время компиляции ИмяВыражения или Основное является квалифицирующим типом ссылки.
-
-
Если выражение имеет вид
super.f, то суперкласс C — квалифицирующий тип ссылки. -
Если выражение имеет вид ИмяТипа
.super.f, то суперкласс класса, обозначенного ИмяТипа, является квалифицирующим типом ссылки.
Ссылка на
fдолжна быть скомпилирована в символическую ссылку на стирание (§4.6) квалифицирующего типа ссылки, плюс простое имя поля,f. Ссылка также должна включать символическую ссылку на стирание объявленного типа поля, чтобы верификатор мог проверить, что тип соответствует ожиданиям. -
-
В выражении вызова метода или выражении ссылки на метод в классе или интерфейсе C, ссылающемся на метод с именем
m, объявленный (или неявно объявленный (§9.2)) в (возможно, отличном) классе или интерфейсе D, квалифицирующий тип вызова метода определяется следующим образом:-
Если D является
Object, то квалифицирующий тип выражения являетсяObject. -
В противном случае:
-
Если метод ссылается с помощью простого имени, то если
mявляется членом текущего класса или интерфейса C, пусть T будет C; в противном случае, пусть T будет самым внутренним лексически окружающим типом объявления, членом которого являетсяm. В любом случае, T является квалифицирующим типом вызова метода. -
Если выражение имеет вид ИмяТипа
.mили ТипСсылка::m, то тип, обозначаемый ИмяТипа или ТипСсылка, является квалифицирующим типом вызова метода. -
Если выражение имеет вид ИмяВыражения
.mили Первичное.mили ИмяВыражения::mили Первичное::m, то:-
Если тип во время компиляции ИмяВыражения или Первичное является типом пересечения V1
&...&Vn (§4.9), то квалифицирующий тип вызова метода — это V1. -
В противном случае, тип во время компиляции ИмяВыражения или Первичное является квалифицирующим типом вызова метода.
-
-
Если выражение имеет вид
super.mилиsuper::m, то суперкласс C является квалифицирующим типом вызова метода. -
Если выражение имеет вид ИмяТипа
.super.mили ИмяТипа.super::m, то если ИмяТипа обозначает класс X, то суперкласс X является квалифицирующим типом вызова метода; если ИмяТипа обозначает интерфейс X, то X является квалифицирующим типом вызова метода.
-
Ссылка на метод должна быть разрешена во время компиляции в символическую ссылку на стирание (§4.6) квалифицирующего типа вызова, плюс стирание подписи (§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.
Ссылка на конструктор должна быть разрешена во время компиляции в символическую ссылку на стирание (§4.6) квалифицирующего типа вызова, плюс подпись конструктора (§8.8.2). Подпись конструктора должна содержать следующее:
-
Количество параметров конструктора
-
Символическую ссылку на тип каждого формального параметра
-
Двоичное представление класса или интерфейса также должно содержать следующее:
-
Если это класс и он не
Object, то символическая ссылка на стирание непосредственного суперкласса этого класса. -
Символическая ссылка на стирание каждого непосредственного суперинтерфейса, если таковые имеются.
-
Описание каждого поля, объявленного в классе или интерфейсе, заданное как простое имя поля и символическая ссылка на стирание типа поля.
-
Если это класс, то стёртый подпись каждого конструктора, как описано выше.
-
Для каждого метода, объявленного в классе или интерфейсе (исключая, для интерфейса, его неявно объявленные методы (§9.2)), его стёртый подпись и тип возвращаемого значения, как описано выше.
-
Код, необходимый для реализации класса или интерфейса:
-
Каждый тип должен содержать достаточную информацию для восстановления его канонического имени (§6.7).
-
Каждый член-тип должен иметь достаточную информацию для восстановления его модификатора доступа на уровне исходного кода.
-
Каждый вложенный класс и вложенный интерфейс должны иметь символическую ссылку на свой непосредственно окружающий тип (§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).
Для справки, следующие конструкции объявлены неявно в исходном коде, но не помечены как обязательные, поскольку только формальные параметры могут быть помечены как таковые в файле class (JVMS §4.7.24):
Файл 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, не нарушает совместимость с существующими двоичными файлами.
Если класс, который не был объявлен 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.
Вывод заключается в том, что реализация без верификатора или без его использования не будет сохранять безопасность типов и, следовательно, не является корректной реализацией.
Требование, чтобы альтернативы в предложении многократного 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.
Удаление поля из класса нарушит совместимость с любыми существующими бинарными файлами, которые ссылаются на это поле, и произойдёт ошибка 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.21). (Можно предположить, что новое значение не отображается, если используется в выражении константы (§15.28), но отображается в противном случае. Это не так; существующие двоичные файлы вообще не видят нового значения.)
Лучший способ избежать проблем с "непостоянными константами" в широко распространённом коде — использовать 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, возникнет, если поле используется существующим бинарным файлом, который ожидал поле другого типа. Такие изменения не рекомендуются в коде, который широко распространён.
Добавление объявления метода или конструктора в класс не нарушит совместимость с существующими бинарными файлами, даже в случае, когда тип больше не может быть повторно скомпилирован, потому что вызов ранее ссылался на метод или конструктор суперкласса с несовместимым типом. Ранее скомпилированный класс с такой ссылкой продолжит ссылаться на метод или конструктор, объявленный в суперклассе.
Предположим ссылку на метод m с квалифицирующим типом T. Предположим далее, что m фактически является методом экземпляра (соответственно static) объявленным в суперклассе T, S.
Если новый метод типа X с той же сигнатурой и возвращаемым типом, что и m, добавляется в подкласс S, который является суперклассом T или самим T, то может произойти ошибка связи. Такая ошибка связи произойдёт только если, помимо вышесказанного, выполняется одно из следующих условий:
-
Новый метод имеет меньший доступ, чем старый.
-
Новый метод является методом класса (соответственно методом экземпляра).
Удаление метода или конструктора из класса может нарушить совместимость с любыми существующими бинарными файлами, которые ссылались на этот метод или конструктор; может быть брошено исключение NoSuchMethodError при связывании такой ссылки из существующего бинарного файла. Такая ошибка произойдёт только если в суперклассе не объявлен метод с такой же сигнатурой и возвращаемым типом.
Если исходный код для класса, не являющегося вложенным, не содержит объявленных конструкторов, то неявно объявляется конструктор по умолчанию без параметров (§8.8.9). Добавление одного или нескольких объявлений конструкторов в исходный код такого класса предотвратит явное объявление этого конструктора по умолчанию, фактически удалив конструктор, если только один из новых конструкторов также не имеет параметров, таким образом заменив конструктор по умолчанию. Конструктору по умолчанию без параметров предоставляется тот же модификатор доступа, что и классу его объявления, поэтому любая замена должна иметь такой же или больший доступ, чтобы сохранить совместимость с существующими бинарными файлами.
Добавление или удаление параметра типа метода или конструктора само по себе не имеет последствий для двоичной совместимости.
Если такой параметр типа используется в типе метода или конструктора, это может иметь обычные последствия изменения вышеупомянутого типа.
Переименование параметра типа метода или конструктора не влияет на существующие бинарные файлы.
Изменение первого ограничения параметра типа метода или конструктора может изменить стирание (§4.6) любого члена, использующего этот параметр типа в собственном типе, и это может повлиять на двоичную совместимость. Конкретно:
-
Если параметр типа используется как тип поля, эффект такой, как если бы поле было удалено, и было добавлено поле с тем же именем, тип которого — новое стирание типа переменной.
-
Если параметр типа используется как тип любого формального параметра метода, но не как возвращаемый тип, эффект такой, как если бы этот метод был удалён и заменён новым методом, который идентичен, за исключением типов вышеупомянутых формальных параметров, которые теперь имеют новое стирание параметра типа в качестве своего типа.
-
Если параметр типа используется как возвращаемый тип метода, но не как тип какого-либо формального параметра метода, эффект такой, как если бы этот метод был удален и заменён новым методом, который идентичен, за исключением возвращаемого типа, который теперь является новым стиранием параметра типа.
-
Если параметр типа используется как возвращаемый тип метода и как тип одного или нескольких формальных параметров метода, эффект такой, как если бы этот метод был удалён и заменён новым методом, который идентичен, за исключением возвращаемого типа, который теперь является новым стиранием параметра типа, и за исключением типов вышеупомянутых формальных параметров, которые теперь имеют новое стирание параметра типа в качестве своего типа.
Изменение любого другого ограничения не влияет на двоичную совместимость.
Изменение имени формального параметра метода или конструктора не влияет на существующие бинарные файлы.
Изменение имени метода или типа формального параметра метода или конструктора, или добавление параметра к объявлению метода или конструктора, или удаление параметра создаёт метод или конструктор с новой сигнатурой и имеет совокупный эффект удаления метода или конструктора со старой сигнатурой и добавления метода или конструктора с новой сигнатурой (§13.4.12).
Изменение типа последнего формального параметра метода с T[] на параметр с переменным числом аргументов типа T (т.е. на T...), и наоборот, не влияет на существующие бинарные файлы.
Для целей двоичной совместимости, добавление или удаление метода или конструктора m, чья сигнатура включает переменные типов (§4.4) или параметризованные типы (§4.5), эквивалентно добавлению (соответственно удалению) аналогичного метода, чья сигнатура является стиранием (§4.6) сигнатуры m.
Изменение возвращаемого типа метода или замена возвращаемого типа на void или замена void на возвращаемый тип, имеет совокупный эффект удаления старого метода и добавления нового метода с новым возвращаемым типом или новым void возвращаемым типом (см. §13.4.12).
Для целей двоичной совместимости, добавление или удаление метода или конструктора m, чьё возвращаемое значение включает переменные типов (§4.4) или параметризованные типы (§4.5), эквивалентно добавлению (соответственно удалению) аналогичного метода, чьё возвращаемое значение является стиранием (§4.6) возвращаемого значения m.
Изменение метода, объявленного как абстрактный, на метод, не объявленный как абстрактный, не нарушает совместимость с существующими бинарными файлами.
Изменение метода, не объявленного как абстрактный, на метод, объявленный как абстрактный, нарушит совместимость с существующими бинарными файлами, которые ранее вызывали этот метод, вызывая ошибку 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-компилятор не может встроить метод в строке на этапе компиляции. В общем случае, мы рекомендуем реализациям использовать отложенное (время выполнения) генерирование кода и оптимизацию.
Добавление новых методов или конструкторов, перегружающих существующие методы или конструкторы, не нарушает совместимость с существующими бинарными файлами. Подпись, которая должна использоваться для каждого вызова, определялась во время компиляции этих существующих бинарных файлов; поэтому новые добавленные методы или конструкторы не будут использоваться, даже если их подписи применимы и более специфичны, чем первоначально выбранная подпись.
Хотя добавление нового перегруженного метода или конструктора может вызвать ошибку компиляции при следующем компилировании класса или интерфейса, так как нет метода или конструктора, который является наиболее специфичным (§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.
Добавление abstract, private или static метода в интерфейс не нарушает совместимости с существующими двоичными файлами.
Добавление поля в суперинтерфейс 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.
Влияние изменений параметров типа интерфейса аналогично влиянию аналогичных изменений параметров типа класса.
Рассмотрение изменений объявлений полей в интерфейсах аналогично рассмотрению изменений объявлений полей в классах, как описано в §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, если существующий двоичный файл пытается вызвать метод. Эта ошибка возникает, если квалифицирующий тип T является подтипом двух интерфейсов I и J, где оба I и J объявляют default метод с одинаковой сигнатурой и результатом, и ни I, ни J не являются подинтерфейсами друг друга.
Другими словами, добавление метода по умолчанию — это изменение, совместимое с двоичными файлами, поскольку оно не приводит к ошибкам на этапе связывания, даже если оно приводит к ошибкам на этапе компиляции или вызова. На практике риск случайных столкновений, возникающих при введении метода по умолчанию, аналогичен риску, связанному с добавлением нового метода в не-final класс. В случае столкновения добавление метода в класс вряд ли вызовет LinkageError, но случайная переопределение метода в дочернем классе может привести к непредсказуемому поведению метода. Оба изменения могут вызвать ошибки на этапе компиляции.
Пример 13.5.6-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, которые манипулируют аннотациями. Документация этих API указывает их поведение при различных изменениях в базовых типах аннотаций.
Добавление или удаление аннотаций не влияет на правильное связывание двоичных представлений программ на языке программирования Java.
© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.