Содержание | Предыдущая | Следующая


ГЛАВА 13

Двоичная совместимость

Инструментальные средства разработки Явы должны поддерживать автоматическую перекомпиляцию по мере необходимости всякий раз, когда исходный текст доступен. Определенные реализации Явы могут также сохранять файл исходного текста и двоичный файл типов в базе данных версий реализовывать ClassLoader (§20.14), который использует механизмы целостности базы данных, чтобы предотвратить ошибки редактирования, обеспечивая двоично-совместимые версии типов.

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

Документ, цитируемый выше представляется на Слушаниях OOPSLA '95 издан как ACM SIGPLAN Notices, Volume 30, Number 10, October 1995, pages 426-438. В рамках этого документа, двоичные файлы Явы - двоичные файлы (от версии к версии), двоично-совместимые при всех уместных преобразованиях, которые идентифицируют авторы. Используя их схему, представляем список некоторых важных двоично-совместимых изменений, которые поддерживает Ява:

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

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

Эта глава сначала определяет некоторые свойства, которые должен иметь любой двоичный Ява-формат (§13.1). Затем определяется двоичная совместимость, объясняющая что есть что (§13.2). В заключение перечисляется большой набор возможных изменений пакетов (§13.3), классов (§13.4) и интерфейсов (§13.5), точно определяя какие изменения гарантируют сохранение двоичной совместимости, а которые - нет.

13.1 Форма двоичного Ява-файла

В то время как много двоичных файлов языка Ява, вероятно, будут более точного формата из файла класса, определенном спецификацией виртуальной машины языка Ява, эта спецификация не обязывает использовать любой специфический формат двоичного файла. Вернее, она точно определяет свойства, которым должен подчиняться любой двоичный формат для компилируемых типов. Ряд этих свойств выбран специально для того, чтобы поддерживать преобразования исходного текста, которые сохраняют двоичную совместимость.

Требования:

 

Двоичное представление Явы для класса или интерфейса должно также содержать все из следующего:

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

Следующие разделы определяют изменения, которые могут быть сделаны относительно объявлений типов классов и интерфейсов без нарушения совместимости с уже существующими двоичными файлами. Виртуальная машина языка Ява и стандартный формат файла класса поддерживают эти изменения; другие двоичные Ява-форматы требуют поддержки этих изменений.

13.2 Что является двоичной совместимостью, а что нет

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

Как описано в §13.1, символические ссылки на методы и поля однозначно называют класс или интерфейс, в котором объявлен метод или поле. Это значит, что двоичные коды создаются чтобы взаимодействовать с доступными членами и конструкторами других классов и интерфейсов. Для сохранения двоичной совместимости, класс или интерфейс должен обработать эти доступные члены и конструкторы, их существование и поведение в зависимости с договоренностью с пользователями класса или интерфейса.

Язык Ява разработан так, чтобы предотвратить расширение соглашений и случайные коллизии имен при нарушении двоичной совместимости; характерно:

Двоичная совместимость не такая как обычная. В частности, пример в §13.4.5 показывает, что набор совместимых двоичных файлов может быть получен из исходных, которые не будут компилироваться вместе. Этот пример типичен: новое объявление добавлено, изменив значение имени в неизменяемой части исходного кода, в то время как предыдущий двоичный файл для этой неизменяемой части исходного кода сохраняет полностью квалифицированное предыдущее значение имени. Создание непротиворечивого набора исходного кода требует наличия составного имени или выражения доступа поля, соответствующего предыдущему значению.

Мы надеемся сделать некоторые усовершенствования в будущих версиях Явы для лучшего поддержания как исходно так и двоично-совместимого развития типов. В частности, мы полагаем, что такой механизм позволит классу реализовывать два интерфейса, которые имеют методы с той же самой сигнатурой, но должны быть рассмотрены по-разному или иметь различные типы возврата. Мы принимаем предложения, которые помогли бы нам делать дополнительные усовершенствования или в разрешении конфликтов имен и сигнатур или в других источниках несовместимости.

13.3 Развитие пакетов

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

Изменения в типах класса и интерфейса, которые не являются public и которые не являются суперклассом или суперинтерфейсом, типа public, воздействуют только на те типы внутри пакета, в котором они объявлены. Такие типы могут быть удаленны или изменены, даже если существует несовместимость описана здесь иначе, при условии, что рассматриваемые двоичные коды этого пакета модифицируются вместе.

13.4 Развитие классов

Этот раздел описывает эффекты изменений, соответствующих объявлениям класса, его членов и конструкторов в предыдущих двоичных кодах.

13.4.1 Абстрактные (abstract) классы

Если класс, который не был abstract изменен на abstract, то уже существующие двоичные файлы, которые пытались создавать новые экземпляры этого класса сгенерируют или InstantiationError во время компоновки, или InstantiationException во время выполнения (если используется метод newInstance (§20.3.6) класса Class); такое изменение, следовательно, не рекомендуется использовать на широко распространенных классах.

Изменение класса, который был объявлен как abstract, но более не будет таковым, не нарушает совместимости с уже существующими двоичными кодами.

13.4.2 Классы final

Если класс, который не был объявлен как final, изменен на final, то генерируется VerifyError если сгенерирован двоичный файл предыдущего подкласса этого класса, потому что final классы не могут иметь никаких подклассов; такое изменение не рекомендуется использовать на широко распространенных классах.

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

13.4.3 Классы public

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

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

13.4.4 Суперклассы и суперинтерфейсы

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

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

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

Если в результате происходят изменения непосредственного суперкласса или множества непосредственных суперинтерфейсов в любом классе или интерфейсе, который уже не является суперклассом или суперинтерфейсом, соответственно, то во время компоновки могут появится ошибки, если загружаются уже существующие двоичные файлы с двоичными файлами из измененного класса. Такие изменения не рекомендуются для широко распространяемых классов. Результирующие ошибки обнаруживаются верификатором виртуальной машины языка, когда операция предварительной компиляции нарушает тип системы. Предположим, что следующая испытательная программа:

 

class Hyper { char h = 'h'; } 
class Super extends Hyper { char s = 's'; }
class Test extends Super {
    public static void main(String[] args) {
        Hyper h = new Super();
        System.out.println(h.h);
    }
}

компилируется, выполняется и выводит:

 

h

Предположим, что затем компилируется новая версия класса Super:

 

class Super { char s = 's'; }

Эта версия класса Super - не подкласс Hyper. Если мы затем отправляем существующие двоичные файлы из Hyper и Test в новую версию Super, то во время компоновки генерируется VerifyError. Верификатор возражает этому, потому что результат new Super () не может быть назначен переменной типа Hyper, так как Super - не подкласс Hyper.

Поучительно рассмотреть то, что могло бы произойти без шага верификации: программа запускалась бы и печатала :

 

s

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

Как в следующем примере, реализуется приведение ссылочного типа в int, который мог бы быть получен выполнением в некоторых реализаций языка Ява, если они повредили выполнение процесса верификации. Представим себе реализацию, которая использует таблицу методов, с назначением смещений в ней простым последовательным способом. Затем предположим, что код языка Ява компилируется следующим образом:

 

class Hyper { int zero(Object o) { return 0; } }
class Super extends Hyper { int peek(int i) { return i; }  }

class Test extends Super {
	public static void main(String[] args) throws Throwable {
		Super as = new Super();
		System.out.println(as);
		System.out.println(Integer.toHexString(as.zero(as)));
	}
}

При использовании принятой реализации получается, что класс Super имеет два метода: первый - это метод zero, унаследованный от класса Hyper, и второй - это метод peek. Любой подкласс Super также имел бы эти самые два метода в первых двух элементах таблицы методов. (Фактически, все эти методы могли быть последователями всех методов в таблицах метода, унаследованных от класса Object, но чтобы упростить обсуждение, мы это здесь игнорируем.) Для вызова метода as.zero (as), компилятор определяет какой первый метод из таблицы метода будет активизироваться; это всегда корректно, если сохраняется безопасность типа.

Если код компилируется и затем выполняется, то печатается что-нибудь вроде:

 

Super@ee300858

что является корректным выводом. Но если компилируется новая версия Super, который является некоторым исключением для операторов extends :

 

class Super { int peek(int i) { return i; } }

то первый метод в таблице методов для Super будет теперь peek, а не zero. Использование нового двоичного кода для Super со старым двоичным кодом для Hyper и Test будет причиной для вызова метода as.zero (as) отправить метод peek в Super скорее чем метод zero в Hyper. Это, конечно же, является нарушением типа; аргумент имеет тип Super, а параметр имеет тип int. С несколькими вероятными предположениями относительно внутренних представлений данных и последствий нарушения типа, выполнение этой некорректной программы могло бы выводить:

 

Super@ee300848

Ee300848

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

Реализация языка Ява, которой не достает верификатора или терпит неудачу при использовании не поддерживаемых безопасных типов, является недопустимой.

13.4.5 Тело класса и объявления членов класса

Несовместимость с уже существующими двоичными файлами приводит к добавлению члена класса, который имеет то же самое имя (для полей) или ту же самую сигнатуры и типа возврата (для методов), как член суперкласса или подкласса. Ссылки к первоначальному полю или методу были разрешены во время компиляции в символическую ссылку, содержащую имя класса, в котором они объявлены. Это заставляет защитить скомпилированный Явой код от изменений, которые могли бы быть. Ошибки не происходит, даже если множество связываемых классов могло бы компилироваться с ошибкой. Например, программа:

 

class Hyper { String h = "Hyper"; }
class Super extends Hyper { }
class Test extends Super {
	public static void main(String[] args) {
		String s = new Test().h;
		System.out.println(s);
	}
}

компилируется, выполняется и выводит:

Hyper

Предположим, что затем компилируется новая версия класса Super:

 

class Super extends Hyper { char h = 'h'; }

Если результирующий двоичный файл используется с существующими двоичными файлами Hyper и Test, то все еще выводится:

 

Hyper

даже компиляция исходного текста для этих двоичных файлов:

 

class Hyper { String h = "Hyper"; }
class Super extends Hyper { char h = 'h'; }
class Test extends Super {
	public static void main(String[] args) {
		String s = new Test().h;
		System.out.println(s);
	}
}

была бы результатом в ошибке времени компиляции, потому что h в исходном коде для main рассматривался как ссылающийся на поле char, объявленное в Super, и значение char не может быть обозначено как String .

Удаление члена класса или конструктора, который не объявлен private, может допускать ошибку компоновки, если член класса или конструктор используется уже существующим двоичным файлом, даже если член класса был экземпляром метода, который был непосредственным суперклассом метода. Это потому, что в процессе разрешения, компоновщик смотрит только на класс, который был идентифицирован во время компиляции. Таким образом, если программа:

 

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 и Hyper компилируются повторно, а Test нет, тогда результатом во время компоновки будет NoSuchMethodError, потому что метод hello больше не объявлен в классе Super.

Чтобы сохранять двоичную совместимость, методы не должны удалятся; а должна использоваться “переадресация методов”. В нашем примере, заменяем описание Super на:

 

class Super extends Hyper {

void hello() { super.hello(); }

}

тогда при повторной компиляции Super и Hyper, и при выполнении новых двоичных файлов с исходным двоичным файлом Test, программа выводит:

 

hello from Hyper

что могло бы наивно ожидаться от предыдущего примера.

Ключевое слово super может использоваться в качестве доступа к методу, объявленному в суперклассе, обходя любые методы, объявленные в текущем классе. Выражение:

 

super.Identifier

разрешается методом M, объявленном в индивидуальном суперклассе S во время компиляции. Метод M должен все еще объявляться в этом классе во время выполнения, или обнаружится ошибка компоновки. Если метод M - экземпляр метода, тогда метод MR, вызванный во время управления, метод с той же самой сигнатурой как и у M, который является членом непосредственного суперкласса класса, содержащего выражение, которое по предположению super. Таким образом, если программа:

 

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

как Вы и ожидали. (Недоработка некоторых ранних версий языка Ява приводит к некорректной печати):

 

hello from Hyper

13.4.6 Доступ к членам класса и конструкторам

Изменение объявленного доступа члена класса или конструктора так, чтобы разрешить меньшее количество доступа, может нарушать совместимость с уже существующими двоичными файлами, приводя к генерации ошибки компоновки, когда эти двоичные файлы разрешены. Меньший доступ разрешается, если модификатор доступа по умолчанию заменяется на доступ private; protected доступ заменяется на доступ по умолчанию или private доступ, public доступ - на protected доступ, доступ по умолчанию, или private доступ. Изменение члена класса или конструктора на менее доступный не рекомендуется для широко распространяемых классов.

Оказалось возможно то, что в языке Ява изменение члена класса или конструктора на более доступный не приводит к ошибке компоновки, когда подкласс (уже) определяет метод, имеющий меньший доступа. Так, например, если пакет points, определяет класс Point:

 

package points;

public class Point {
	public int x, y;
	protected void print() {
		System.out.println("(" + x + "," + y + ")");
	}
}

использующийся программой Test:

 

class Test extends points.Point {
	protected void print() { System.out.println("Test"); }
	public static void main(String[] args) {
		Test t = new Test();
		t.print();
	}
}

тогда эти классы компилируются, программа Test выполняется и выводит:

Test

Если метод print в классе Point изменяется на public, и тогда только класс Point компилируется повторно, и затем выполняется с уже существующим двоичным файлом из Test, то не произойдет никакой ошибки компоновки, даже если это не правильно, во время компиляции метод public может игнорироваться методом protected (на примере показано, что класс Test может не компилироваться повторно, используя этот новый класс Point, если печать не была изменена в public.)

Разрешение в суперклассах изменений методов protected на public без нарушения двоичных файлов уже существующих подклассов помогает сделать двоичные файлы языка Ява долговечнее. Вариант, где изменение привело бы к ошибке компоновки, создал бы дополнительную двоичную несовместимость без очевидной выгоды.

13.4.7 Объявления поля

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

 

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 является выходным с исходным двоичным файлом класса из main независимо от того какой тип поля h объявлен в Super. Сначала это может показаться удивительным, но это служит для уменьшения числа несовместимостей, которые происходят во время выполнения. (В идеальном мире, все исходные файлы, которым необходима повторная компиляция, будут повторно компилироваться всякий раз, когда один из них изменялся, устраняя возможные несоответствия. Но такая массовая перекомпиляция часто непрактична или невозможна, особенно в Интернет. И, как было отмечено ранее, такая перекомпиляция иногда требовала бы дальнейших изменений в исходном коде.)

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

13.4.8 Поля и константы final

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

class Super { static char s; }

class Test extends Super {
	public static void main(String[] args) {
		s = 'a';
		System.out.println(s);
	}
}

компилируется и выполняется, она выводит:

a

Предположим, что затем выводится новая версия класса Super:

class Super { static char s; }

Здесь, вероятно, ошибка в оригинале. Можно поправить следующим образом:

class Super { static final char s; }

Если Super компилируется повторно, а Test нет, то последующее выполнение двоичного файла с существующим двоичным файлом Test приведет к ошибке IncompatibleClassChangeError. (В некоторых ранних реализациях языка Ява этот пример проходил бы без ошибки из-за недостатков в реализации.)

Мы называем поле примитивной константой, которое является static, final, и инициализировано константным выражением во время компиляции. Заметьте, что все поля в интерфейсах неявно static и final, и они часто, но не всегда, константы.

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

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

 

class Flags { final static boolean debug = true; }

class Test {
	public static void main(String[] args) {
		if (Flags.debug)
			System.out.println("debug is true");
	}
}

компилируется, выполняется и выводит:

 

debug is true

Предположим, что затем выводится новая версия класса Flags:

 

class Flags { final static boolean debug = false; }

Если Flags компилируется повторно, а Test нет, то, управляя новым двоичным файлом с существующим двоичным файлом Test, выводится:

 

debug is true

потому что значение debug было во время компиляции примитивной константой, и могло быть использовано при компиляции Test без создания ссылки на класс Flags.

Этот результат является побочным эффектом решения, поддерживающего условную компиляцию, что обсуждается в конце § 14.19.

Такое поведение не изменилось бы, если бы интерфейс Flags был изменен так, как показано в следующем примере:

 

interface Flags { boolean debug = true; }
class Test {
	public static void main(String[] args) {
		if (Flags.debug)
			System.out.println("debug is true");
	}
}

(Одна из причин требования встраивания примитивных констант - это то, что операторы switch языка Ява требуют наличия констант в каждом case, причем никакие два значения константы не должны совпадать. В языке Ява дублирование значений константы в операторе switch проверяется во время компиляции; формат файла класса не предусматривает символическую компоновку значений case.)

Лучший способ избегать проблем с "непостоянными константами" в широко распространяемом коде - это объявлять как примитивные константы только те значения, которые верны и должны изменятся. Многие примитивные константы в интерфейсах имеют небольшие целые значения, заменяющие перечисляемые типы, которые язык Ява не поддерживает; эти небольшие значения могут быть выбраны произвольно, и не нуждаются в изменении. Другой способ, для основных математических констант, который мы рекомендуем, состоит в том, что код языка Ява очень экономно использует переменные класса, которые объявлены static и final. Если требуется элемент final только для чтения, то лучше будет объявить переменную private static и подходящий вспомогательный метод для получения этих значений. Таким образом, мы рекомендуем использовать:

 

private static int N;

public static int getN() { return N; }

а не:

 

public static final int N = ...;

Нет никакой проблемы с:

 

public static int N = ...;

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

 

interface Flags {

boolean debug = new Boolean(true).booleanValue();

}

гарантируя, что это значение не является константой. Подобные идиомы существуют для других примитивных типов.

Заметим еще одно обстоятельство, которое состоит в том, что поля static final, которые имеют значения константами (во всяком случае примитивные константы или константы типа String) должны иметь по умолчанию начальные значения для своих типов (§ 4.5.4). Это означает что все такие поля инициализируются первыми при инициализации класса (§ 8.3.2.1, § 9.3.1, § 12.4.2).

13.4.9 Поля static

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

13.4.10 Поля transient

Добавление или удаление модификатора transient у поля не нарушает совместимость с уже существующими двоичными файлами.

13.4.11 Поля volatile

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

13.4.12 Описания метода и конструктора

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

Удаление метода или конструктора из класса будет нарушать совместимость с любым уже существующим двоичным файлом, который ссылался на этот метод или конструктор; когда такая ссылка на уже существующий двоичный файл компонуется, генерируется NoSuchMethodError. Только методы private или конструкторы могут благополучно удаляться из широко распространяемого класса.

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

13.4.13 Параметры метода и конструктора

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

13.4.14 Тип результата метода

Изменение типа результата метода, заменяя типа результата с void, или заменяя void с типом результата, имеет комбинированный эффект удаления старого метода или конструктора и добавления нового метода или конструктора с новым типом результата или недавним void результатом (см. § 13.4.12).

13.4.15 Методы abstract

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

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

 

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, и, поэтому, он является (или должен быть) абстрактным. (Ранняя версия языка Ява неправильно выводила:

 

Way

неправильно разрешая подготовку класса Test, перед возникновением AbstractMethodError при призыве метода out, даже если он имеет метод abstract и не объявлен abstract.)

13.4.16 Методы final

Изменение экземпляра метода, который не является final, на 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, не нарушает совместимость с существующими двоичными файлами, потому что класс фактического метода будет разрешатся во время компиляции.

Удаление модификатора final у метода не нарушает совместимость с уже существующими двоичными файлами.

13.4.17 Методы native

Добавление или удаление модификатора native у метода не нарушает совместимость с уже существующими двоичными файлами.

Влияние изменений типов языка Ява на уже существующие методы native, которые повторно не компилируются, лежит вне возможности этой спецификации и должно предоставляться в описании реализации языка Ява. Поддерживаются такие реализации, которые не являются требуемыми, методов native в пути, которые ограничивают это влияние.

13.4.18 Методы static

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

13.4.19 Методы synchronized

Добавление или удаление модификатора synchronized у метода не нарушает совместимость с существующими двоичными файлами.

13.4.20 Throws метода и конструктора

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

Мы рассматриваем, должна ли будущая версия языка Ява требовать более строгой проверки предложений throws, если классы верифицируются.

13.4.21 Тело метода и конструктора

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

Мы замечаем, что компилятор не может встраивать код метода непосредственно в место вызова метода во время компиляции за исключением, например, когда:

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

Вообще мы предлагаем то, что реализация языка Ява использует позднее связывание (время выполнения) кода генерации и оптимизации.

13.4.22 Перегрузка метода и конструктора

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

Добавление нового перегруженного метода или конструктора может в следующий раз привести к ошибке во время компиляции, когда класс или интерфейс компилируется, потому что не будет метода или конструктора, который является наиболее подходящим (§ 15.11.2.2), такой ошибки не происходит, когда выполняется программа на языке Ява, потому что связывание при перегрузке происходит не во время выполнения.

Например программа:

 

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

как могло бы наивно ожидаться от предыдущего примера.

13.4.23 Замещение метода

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

 

class Hyper {
	void hello() { System.out.print("Hello, "); }
	static void world() { System.out.println("world!"); }
}
class Super extends Hyper { }

class Test {
	public static void main(String[] args) {
		Super s = new Super();
		s.hello();
		s.world();
	}
}

компилируется, выполняется и выводит:

 

Hello, world!

Предположим, что затем выводится новая версия класса Super:

 

class Super extends Hyper {
	void hello() { System.out.print("Goodbye, cruel "); }
	static void world() { System.out.println("earth!"); }
}

Если Super повторно компилируется, а Hyper или Test нет, то, управляя новым двоичным файлом с существующими двоичными файлами Hyper и Test, выводит:

 

Goodbye, cruel world!

Этот пример демонстрирует что вызов в:

 

s.world ();

в методе main связывается во время компиляции символической ссылки на класс, содержащий метод класса world, как если бы было написано:

 

Hyper.world ();

Это потому, что world метода Hyper лучше вызывается в этом примере чем Super. Конечно, при перекомпилировании всех классов производятся новые двоичные файлы и выводится:

 

Goodbye, cruel earth!

13.4.24 Статические инициализаторы

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

13.5 Развитие интерфейсов

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

13.5.1 Интерфейсы public

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

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

13.5.2 Суперинтерфейсы

Изменения иерархии интерфейсов приводит к ошибкам таким же образом, как и изменение иерархии классов, что описано в § 13.4.4. В частности, изменения, в результате которых суперинтерфейс класса перестает быть таковым, могут нарушать совместимость с уже существующими двоичными файлами, и приведут к ошибке VerifyError.

13.5.3 Члены интерфейса

Добавление члена к интерфейсу не нарушает совместимость с уже существующими двоичными файлами.

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

 

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

13.5.4 Описания полей

Изменение описаний полей в интерфейсах подчиняется тем же правилам, как и для static final полей в классах, что описано в § 13.4.7 и § 13.4.8.

13.5.5 Описания абстрактных методов

Изменение описаний абстрактных методов в интерфейсах подчиняется тем же правилам, как и для абстрактных методов в классах, что описано в § 13.4.13, § 13.4.14, § 13.4.20 и § 13.4.22.


Содержание | Предыдущая | Следующая