Философия Java3 - Брюс Эккель
Шрифт:
Интервал:
Закладка:
Главная причина для применения стирания заключается в том, что оно позволяет параметризованным клиентам использовать непараметризованные библиотеки, и наоборот. Эта концепция часто называется миграционной совместимостью. Наверное, в идеальном мире параметризация была бы внедрена везде и повсюду одновременно. На практике программисту, даже если он пишет только параметризованный код, приходится иметь дело с ^параметризованными библиотеками, написанными до Java SE5. Возможно, авторы этих библиотек вообще не намерены параметризовать свой код или собираются сделать это в будущем.
Из-за этого механизму параметризации Java приходится поддерживать не только обратную совместимость (существующий код и файлы классов остаются абсолютно законными и сохраняют свой прежний смысл), но и миграционную совместимость — чтобы библиотеки могли переводиться в параметризованную форму в собственном темпе, причем их параметризация не влияла бы на работу зависящего от него кода и приложений. Выбрав эту цель, проектировщики Java и различные группы, работавшие над проблемой, решили, что единственным приемлемым решением является стирание, позволяющее непарамет-ризованному коду нормально сосуществовать с параметризованным.
Проблемы стирания
Итак, главным аргументом для применения стирания является процесс перехода с непараметризованного кода на параметризованный и интеграция параметризации в язык без нарушения работы существующих библиотек. Стирание позволяет использовать существующий ^параметризованный код без изменений, пока клиент не будет готов переписать свой код с использованием параметризации.
Однако за стирание приходится расплачиваться. Параметризованные типы не могут использоваться в операциях, в которых явно задействованы типы времени выполнения — преобразования типов, instanceof и выражения new. Вся информация о типах параметров теряется, и при написании параметризованного кода вам придется постоянно напоминать себе об этом. Допустим, вы пишете фрагмент кода
class Foo<T> { Т var;
}
Может показаться, что при создании экземпляра Foo:
Foo<Cat> f = new Foo<Cat>(),
код class Foo должен знать, что он работает с Cat. Синтаксис создает впечатление, что тип Т подставляется повсюду внутри класса. Но на самом деле это не так, и при написании кода для класса вы должны постоянно напоминать себе: «Нет, это всего лишь Object».
Кроме того, стирание и миграционная совместимость означают, что контроль за использованием параметризации не настолько жесткий, как хотелось бы:
//: generics/ErasureAndlnheritance.java
class GenericBase<T> { private T element;
public void set(T arg) { arg = element; } public T get О { return element. }
}
class Derivedl<T> extends GenericBase<T> {}
class Derived2 extends GenericBase {} // Без предупреждений
// class Derived3 extends GenericBase<?> {}
// Странная ошибка.
// Обнаружен непредвиденный тип : ?
// требуется- класс или интерфейс без ограничений
public class ErasureAndlnheritance { @SuppressWarni ngs("unchecked") public static void main(String[] args) { Derived2 d2 = new Derived2(); Object obj = d2.get(); d2.set(obj); // Предупреждение!
}
} ///
Derived2 наследует от GenericBase без параметризации, и компилятор не выдает при этом никаких предупреждений. Предупреждение выводится позже, при вызове set().
Для подавления этого предупреждения в Java существует директива, приведенная в листинге (до выхода Java SE5 она не поддерживалась):
@SuppressWarnings("unchecked")
Обратите внимание: директива применяется к методу, генерирующему предупреждение, а не ко всему классу. При подавлении предупреждений желательно действовать в самых узких рамках, чтобы случайно не скрыть настоящую проблему.
Ошибка, выдаваемая в Derived3, означает, что компилятор рассчитывает увидеть «обычный» базовый класс.
Добавьте к этому дополнительные усилия на управление ограничениями, если вы не желаете интерпретировать параметр типа как простой Object, — и что мы получаем в остатке? Гораздо больше хлопот при гораздо меньше, пользе по сравнению с параметризованными типами в языках вроде С++, Ada или Eiffel. Конечно, это вовсе не означает, что эти языки в целом эффективнее Java в большинстве задач программирования, а говорит лишь о том, что их механизмы параметризации типов отличаются большей гибкостью и мощью, чем в Java.
Проблемы на границах
Пожалуй, самый странный аспект параметризации, обусловленный стиранием, заключается в возможности представления заведомо бессмысленных вещей. Пример:
//: generics/ArrayMaker.java
import java.lang.reflect.*;
import java.util.*;
public class ArrayMaker<T> { private Class<T> kind;
public ArrayMaker(Class<T> kind) { this.kind = kind; } @SuppressWarni ngs("unchecked") T[] create(int size) {
return (T[])Array.newInstance(kind, size);
}
public static void main(String[] args) { ArrayMaker<String> stringMaker =
new ArrayMaker<String>(String.class); String[] stringArray = stringMaker.create(9); System.out.pri ntin(Arrays.toStri ng(stri ngArray));
}
} /* Output;
[null, null, null. null. null. null. null. null, null]
*///:-
Несмотря на то что объект kind хранится в виде Class<T>, стирание означает, что фактически он хранится в виде Class без параметра. Следовательно, при выполнении с ним каких-либо операций (например, при создании массива) Array. newlnstance() не обладает информацией о типе, подразумеваемой kind. Метод не сможет выдать нужный результат, не требующий преобразования типа, а это приводит к выдаче предупреждения, с которым вам не удастся справиться.
Обратите внимание: для создания массивов в параметризованном коде рекомендуется использовать Array.newlnstance().
Если вместо массива создается другой контейнер, ситуация меняется:
//: generics/ListMaker.java
import java util.*;
public class ListMaker<T> {
List<T> create О { return new ArrayList<T>(); } public static void main(String[] args) {
ListMaker<String> stringMaker= new ListMaker<String>(); List<String> stringList = stringMaker.createO;
}
} ///:-
Компилятор не выдает предупреждений, хотя мы знаем, что <Т> в new ArrayList<T>() внутри create() удаляется — во время выполнения <Т> внутри класса нет, поэтому здесь его присутствие выглядит бессмысленным. Однако если вы попробуете применить эту идею на практике и преобразуете выражение в new ArrayList(), компилятор выдаст предупреждение.
Но действительно ли этот элемент не имеет смысла? Что произойдет, если мы поместим в список несколько объектов, прежде чем возватим его?
II: generics/Fi1ledListMaker java
import java.util.*;
public class FilledListMaker<T> { List<T> create(T t, int n) {
List<T> result = new ArrayList<T>(); for(int i = 0: i < n; i++)
result.add(t); return result:
}
public static void main(String[] args) {
FilledListMaker<String> stringMaker =
new Fi11edListMaker<String>(); List<String> list = stringMaker.createC'Hello", 4): System.out.pri ntln(1i st);
}
} /* Output:
[Hello, Hello. Hello. Hello]
*lll:~
Хотя компилятор ничего не может знать о Т в create(), он все равно способен проверить — на стадии компиляции — что заносимые в result объекты имеют тип Т и согласуются с ArrayList<T>. Таким образом, несмотря на то что стирание удаляет информацию о фактическом типе внутри метода или класса, компилятор все равно может проверить корректность использования типа в методе или классе.
Так как стирание удаляет информацию о типе внутри тела метода, на стадии выполнения особую роль приобретают границы — точки, в которых объект входит и выходит из метода. Именно в этих точках компилятор выполняет проверку типов и вставляет код преобразования. Рассмотрим следующий ^параметризованный пример:
// generics/SimpleHolder.java
public class SimpleHolder { private Object obj;
public void set(Object obj) { this.obj = obj; } public Object get О { return obj; } public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder().
holder set("Item"),
String s = (String)holder getO:
}
} ///-
Декомпилировав результат командой javap -с SimpleHolder, мы получим (после редактирования):
public
void set(java lang Object);
0
aload_0
1:
aload 1
2:
putfield #2; II Поле obj.Object;
5.
return
public
java lang.Object getO.
0;
aload 0
1-
getfield #2; II Поле obj-Object,
4
areturn
public
static void main(java 1ang.String[]);
0:
new #3, // Класс SimpleHolder
3-
dup
4:
invokespecial #4; // Метод "<init>".()V
7.
astore_l
8-
aload 1
9.
ldc #5; II String Item
11
invokevirtual #6; // Метод set (Object;)V
14:
aload_l
15.
invokevirtual #7, // Метод get:()Object:
18;
checkcast #8, //'Класс java/lang/String
21:
astore_2
22.
return
Методы set() и get() просто записывают и читают значение, а преобразование проверяется в точке вызова get().
Теперь включим параметризацию в приведенный фрагмент:
II: generics/GenericHolder.java
public class GenericHolder<T> { private T obj,
public void set(T obj) { this.obj = obj; } public T get О { return obj; } public static void main(String[] args) { GenericHolder<String> holder =
new GenericHolder<String>(); holder.set("Item"); String s = holder.get О;
}
Необходимость преобразования выходного значения get() отпала, но мы также знаем, что тип значения, передаваемого set(), проверяется во время компиляции. Соответствующий байт-код:
public void set(java.lang.Object);
0:
aload_0
1:
aload_l
2:
putfield #2; // Поле obj:0bject;
5:
return
public java.lang.Object getO;
0:
aload_0
1:
getfield #2; // Поле obj:0bject;
4:
areturn
public static void main(java.lang.String[]);
0.
new #3; // Класс GenericHolder
3:
dup
4:
invokespecial #4; // Метод "<init>"-()V
7:
astore_l
8:
aload_l
9:
ldc #5; // String Item
11