Брайан Гетц, главный консультант, Quiotix
В версии Java 5 в язык было добавлено много значительных возможностей: generic'и, перечисляемые типы, аннотации, autoboxing, улучшенный цикл for. Однако многие группы разработки все еще привязаны к JDK 1.4 или более ранним версиям и могут находиться в таком состоянии еще некоторое время. Тем не менее эти разработчики все-таки могут использовать эти полезные возможности языка, продолжая устанавливать приложения на ранние версии JVM. Возвратившийся после перерыва Брайан Гетц в этой статье серии Теория и практика Java покажет, как этого добиться.
После недавнего выхода Java 6.0 можно было бы подумать, что возможности Java 5 уже стали стандартом. Но даже сейчас, когда я спрашиваю программистов, какую версию платформы Java они используют для разработки, обычно только половина применяет Java 5, а другая половина им завидует. Многие хотели бы использовать возможности языка, добавленные в Java 5, такие как generic'и и аннотации, но в силу определенных причин не могут сделать этого.
Одна из категорий разработчиков, которые не могут воспользоваться преимуществами функциональности Java 5, - это разработчики компонентов, библиотек и каркасов для разработки приложений. Дело в том, что их заказчики, возможно, все еще используют JDK 1.4 или более ранние версии, а классы, скомпилированные под Java 5, не могут быть загружены JDK 1.4 и более старыми JVM. Поэтому использование возможностей языка Java 5 может привести к сокращению круга их заказчиков, оставив им только компании, уже перешедшие на Java 5.
Другая группа разработчиков, воздерживающаяся от использования Java 5, - это программисты, работающие с Java EE. Многие группы разработчиков не хотят использовать Java 5 c Java EE 1.4 и более ранними версиями из опасения, что Java 5 не будет поддерживаться производителем используемого ими сервера приложений. Так что пройдет некоторое время, прежде чем такие проекты начнут переходить на Java 5. Также, кроме временного разрыва между спецификациями Java EE 5 и Java SE 5, не факт, что коммерческие контейнеры Java EE 5 появятся сразу же после выпуска новой версии спецификации. Компаниям вовсе не обязательно обновлять свои сервера приложений сразу после выхода новой версии, и даже после обновления сервера приложений может потребоваться время для сертификации приложений на новую платформу.
Возможности языка, добавленные в Java 5, - generic'и, перечисляемые типы, аннотации, autoboxing и улучшенный цикл for- не требуют изменений в наборе инструкций JVM и практически полностью реализованы в статическом компиляторе javac
и библиотеках классов. Когда компилятор обнаруживает использование generic'ов, он пытается проверить, что обеспечивается безопасность типов, выдавая предупреждение "unchecked cast" (непроверенное преобразование), если не может это проверить, а затем генерирует байт код, полностью идентичный байт-коду, полученному из эквивалентного не-generic кода с преобразованиями и т.д. Точно также autoboxing и улучшенный цикл for - это просто "синтаксическая лафа" для совершенно эквивалентного, но более длинного кода, а перечисляемые типы компилируются в обычные классы.
В теории можно взять классы, сгенерированные javac
, и загрузить их в более раннюю JVM. На самом деле это и было целью создания группы JSR 14, группы Java Community Process, отвечающей за generic'и. Однако другие проблемы (такие как сохранение аннотаций), заставили изменить версию файла класса в Java 5 по сравнению с Java 1.4, что не позволяет загружать код, скомпилированный для Java 5, в более ранние версии JVM. Также некоторые из возможностей языка, добавленные в Java 5, зависят от библиотек Java 5. Если откомпилировать класс командой javac -target 1.5
и попытаться загрузить его в более раннюю JVM, возникнет ошибка UnsupportedClassVersionError
, так как параметр -target 1.5
генерирует классы с версией файла класса равной 49, а JDK 1.4 поддерживает только версии файлов классов до 48.
Улучшенный цикл for
, иногда называемый циклом for-each
, обрабатывается компилятором так, как если бы программист предоставил аналогичный цикл for
в старом стиле. Цикл for-each
позволяет осуществлять итерацию по элементам массива или коллекции. В примере 1 приведен синтаксис итерации по коллекции с помощью цикла for-each
.
Пример 1. Цикл for-each
Collection<Foo> fooCollection = ...
for (Foo f : fooCollection) {
doSomething(f);
}
|
Компилятор превратит этот код в аналогичный цикл, использующий итераторы, как показано в примере 2.
Пример 2. Эквивалент кода в примере 1 с использованием итераторов
for (Iterator<Foo> iter=f.iterator(); f.hasNext();) {
Foo f = (Foo)iter.next();
doSomething(f);
}
|
Каким образом компилятор определяет, что у предоставленного аргумента есть метод iteraror()
? Архитекторы компилятора javac
могли бы встроить в него понимание framework'a для работы с коллекциями, но этот подход мог привести к ненужным ограничениям. Вместо этого был создан новый интерфейс java.lang.Iterable
(см. пример 3), а классы коллекций были модифицированы так, чтобы реализовывать Iterable
. В результате классы-контейнеры, даже не построенные на базовых коллекциях из framework'a, также могут использовать возможности нового цикла for-each
. Но это создает зависимость от библиотеки классов Java 5, так как интерфейс Iterable
отсутствует в библиотеке JDK 1.4.
Пример 3. Интерфейс Iterable
public interface Iterable<T> {
Iterator<T> iterator();
}
|
Также как и цикл for-each
, перечисляемые типы требуют поддержки библиотеки классов. Когда компилятор встречает перечисляемый тип, он генерирует класс, наследующий библиотечному классу java.lang.Enum
. Но также как и Iterable
, класс Enum
отсутствует в библиотеке классов JDK 1.4.
Аналогично autoboxing использует методы valueOf()
, добавленные в классы-оболочки для примитивных типов, такие как Integer
. Если boxing требует преобразования из int
в Integer
, то вместо вызова new Integer(int)
компилятор генерирует вызов Integer.valueOf(int)
. Эта реализация метода valueOf()
использует шаблон flyweight для кэширования объектов Integer
для часто используемых целых значений типа integer. Например, реализация Java 6 кэширует integer значения от -128 до 127, что позволяет производительность за счет устранения избыточного создания объектов. При этом, как и в случае Iterable
и Enum
, метод valueOf()
отсутствует в библиотеке классов JDK 1.4.
Когда компилятор встречает метод, определенный со списком параметров переменной длины, он преобразует его в метод, который принимает массив компонентов соответствующего типа. Когда компилятор встречает вызов метода со списком параметров переменной длины, то он упаковывает эти элементы в массив.
При определении аннотации она может быть объявлена с аннотацией @Retention
, которая указывает, как компилятор должен поступать с классами, методами или полями, которые используют эту аннотацию. Предусмотренные политики сохранения - SOURCE
(отбрасывать данные из аннотации при компиляции), CLASS
(записывать аннотации в файл класса), RUNTIME
(записывать аннотации в файл класса и сохранять их во время исполнения, чтобы к ним можно было обращаться рефлексивным образом).
До Java 5, когда компилятор встречал попытку сложить две строки, он использовал вспомогательный класс StringBuffer
, чтобы выполнить сложение. В Java 5 и более поздних версиях вместо этого он генерирует вызовы к новому классу StringBuilder
, который не представлен в JDK 1.4 и более ранних библиотеках классов.
Из-за зависимости возможностей языка от библиотек поддержки даже если class-файлы, созданные компилятором Java 5, удастся загрузить в предыдущую версию JVM, то исполнить их все равно не получится из-за ошибок при загрузке классов. Однако подобные проблемы можно решить соответствующим преобразованием байт-кода, так как недостающие классы не содержат важной функциональности.
Во время разработки спецификации Java generic'ов и других возможностей языка Java, добавленных в Java 5, в компилятор javac
была добавлена экспериментальная поддержка, позволяющая ему использовать возможности языка Java 5 и генерировать байт-код, который можно запускать на JVM 1.4. Хотя эти возможности официально не поддерживаются и даже не задокументированы, они используются в ряде проектов Open Source, чтобы позволить разработчикам использовать языковые возможности Java 5 и создавать JAR-файлы, работающие на более ранних JVM. Теперь, когда javac
стал Open Source-проектом, эти возможности могут поддерживаться сторонними разработчиками. Чтобы активировать эти возможности, можно запустить javac
с параметрами -source 1.5
и -target jsr14
.
Режим JSR 14 заставляет компилятор javac
генерировать JDK 1.4-совместимый байт-код, обладающий возможностями Java 5.
- Generics и varargs: Преобразования, вводимые компилятором при наличии generic'ов, не зависят от библиотек классов, так что они могут так же успешно исполняться на предшествующих JVM. Точно так же код, генерируемый компилятором при наличии списка аргументов переменной длины, не зависит от библиотеки классов.
- Цикл
for-each
: При итерации по массиву компилятор генерирует индукционную переменную и стандартный код для итерации по массиву. Когда выполняется итерация по объекту Collection
, компилятор генерирует стандартный код на основе итератора. При итерации по объекту Iterable
, не принадлежащему к типу Collection
, компилятор выводит ошибку.
- Autoboxing: Вместо генерации вызова метода
valueOf()
в классе-оболочке компилятор генерирует вызов конструктора.
- Сложение строк: Режим JSR 14 заставляет компилятор
javac
генерировать вызовы к StringBuffer
вместо StringBuilder
.
- Перечисляемые типы: Перечисляемые типы. Режим JSR 14 компилятора
javac
не предоставляет специальной поддержки перечисляемых типов. Код, пытающийся использовать перечисляемые типы, вызовет сбой с ошибкой NoClassDefFoundError
при поиске базового класса java.lang.Enum
.
Использование режима JSR 14 позволяет писать код, использующий generic'и, autoboxing и цикл for-each в легких случаях, которых достаточно для большинства проектов. Это удобная, хотя и не поддерживаемая, возможность; при этом компилятор генерирует максимально совместимый байт-код за один проход.
В Java 5 есть языковые возможности, не поддерживаемые режимом JSR 14, например, такие как Iterable
и перечисляемые типы. Альтернативный подход, применяемый в таких Open Source-проектах, как Retroweaver и Retrotranslator, предлагает генерировать байт-код, используя параметр -target 1.5
, и затем механически преобразовывать байт-код в формат, совместимый с JDK 1.4
Первой появилась утилита Retroweaver, которая обрабатывает все случаи, обрабатываемые javac -target
JSR 14, а также предоставляет еще несколько возможностей.
- Цикл
for-each
: Retroweaver предлагает реализацию интерфейса Iterable
и переписывает классы, реализующие Iterable
в соответствии со своей собственной версией реализации.
- Autoboxing: Retroweaver заменяет вызовы к
valueOf()
на соответствующие конструкторы.
- Сложение строк: Retroweaver заменяет использование класса
StringBuilder
на использование класса StringBuffer
.
- Перечисляемые типы: Retroweaver предоставляет реализацию базового класса
Enum
и переписывает классы, реализующие Enum
или использующие его методы таким образом, чтобы они использовали версию Enum
из Retroweaver.
Как это часто случается в Open Source-сообществе, если проект перестает двигаться вперед, он объявляется "мертвым", и его место занимает новый проект, даже если первый проект только решил "отдохнуть". Именно такая история произошла с Retroweaver - основной разработчик проекта решил отдохнуть от него, и его место занял другой аналогичный проект - Retrotranslator. Retrotranslator предлагает такие же возможности, как Retroweaver, и еще много дополнительных возможностей, направленных на поддержку важных обновлений в библиотеке классов Java 5.
- Заменяет вызовы классов
java.util.concurrent
вызовами соответствующих классов Open Source-реализации обратной совместимости с JDK 1.4.
- Предлагает реализацию возможностей, добавленных в Collections framework в Java 5, таких как
Arrays
и Collections
. Аналогично предлагает реализации других новых методов и классов, добавленных в библиотеку классов Java 5.
- Поддерживает использование Reflections API для аннотаций во время исполнения программы.
И Retroweaver, и Retrotranslator могут выполнять трансформацию байт-кода как статически (во время компиляции), так и динамически (во время загрузки классов).
У тех невезучих разработчиков, которые не могут использовать новые возможности Java 5, - а таких разработчиков, к сожалению, еще очень много, - есть несколько путей, позволяющих им использовать новые возможности и сохранить совместимость байт-кода с JDK 1.4 и более старыми версиями. Имеется неподдерживаемая опция -target jsr14
компилятора javac
, позволяющая генерировать JDK 1.4-совместимый байт-код для некоторых возможностей Java 5, а также Open Source-проекты Retroweaver и Retrotranslator, преобразующие большинство байт-кода Java 5 в Java 1.4-совместимый байт-код. Что бы вы ни выбрали, не забудьте выполнить тестирование с особой тщательностью, чтобы убедиться в полной совместимости.