Эккель Брюс
Шрифт:
list.getClass.getTypeParameters)); System out println(Arrays.toString(
map. getClassO .getTypeParametersO)). Л
продолжение &
System out pri ntinCArrays.toStri ng(
qua rk.getClass.getTypePa rameters));
System.out.pri ntinCArrays.toStri ng(
p.getClass.getTypePa rameters));
}
} /* Output: [E]
[K. V] [Q]
[POSITION. MOMENTUM] *///:-
Согласно документации JDK, Class.getTypeParameters «возвращает массив объектов TypeVariable, представляющих переменные типов, указанные в параметризованном объявлении...» Казалось бы, по ним можно определить параметры типов — но, как видно из результатов, вы всего лишь узнаете, какие идентификаторы использовались в качестве заполнителей, а эта информация не представляет особого интереса.
Мы приходим к холодной, бездушной истине:
Информация о параметрах типов недоступна внутри параметризованного кода.
Таким образом, вы можете узнать идентификатор параметра типа и ограничение параметризованного типа, но фактические параметры типов, использованные для создания конкретного экземпляра, остаются неизвестными. Этот факт, особенно раздражающий программистов с опытом работы на С++, является основной проблемой, которую приходится решать при использовании параметризации в Java.
Параметризация в Java реализуется с применением стирания (erasure). Это означает, что при использовании параметризации вся конкретная информация о типе утрачивается. Внутри параметризованного кода вы знаете только то, что используется некий объект. Таким образом, List<String> и List<Integer> действительно являются одним типом во время выполнения; обе формы «стираются» до своего низкоуровневого типа List. Именно стирание и создаваемые им проблемы становятся главной преградой при изучении параметризации в Java; этой теме и будет посвящен настоящий раздел.
Подход С++
В следующем примере, написанном на С++, используются шаблоны. Синтаксис параметризованных типов выглядит знакомо, потому что многие идеи С++ были взяты за основу при разработке Java:
//: generics/Templates.cpp #include <iostream> using namespace std:
tempiate<class T> class Manipulator {
T obj: public:
Manipulatory x) { obj = x; } void manipulateO { obj.fO; }
}:
class HasF { public:
void f { cout « "HasF::f" « endl; }
}:
int mainO { HasF hf.
Manipulator<HasF> manipulator(hf): manipulator manipulateO. } /* Output HasF-:f
III ~
Класс Manipulator хранит объект типа Т. Нас здесь интересует метод manipulateO, который вызывает метод f для obj. Как он узнает, что у параметра типа Т существует метод f? Компилятор С++ выполняет проверку при создании экземпляра шаблона, поэтому в точке создания Manipulator<HasF> он узнает о том, что HasF содержит метод f. В противном случае компилятор выдает ошибку, а безопасность типов сохраняется.
Написать такой код на С++ несложно, потому что при создании экземпляра шаблона код шаблона знает тип своих параметров. С параметризацией Java дело обстоит иначе. Вот как выглядит версия HasF, переписанная на Java:
II. generics/HasF java
public class HasF {
public void f { System.out.printlnC'HasF.f"); } } ///:-
Если мы возьмем остальной код примера и перепишем его на Java, он не будет компилироваться:
//: generics/Manipulation.java // {CompileTimeError} (He компилируется)
class Manipulator<T> { private T obj:
public Manipulator^ x) { obj = x; }
// Ошибка: не удается найти символическое имя: метод f:
public void manipulateO { obj.fO; }
}
public class Manipulation {
public static void main(String[] args) { HasF hf = new HasFO; Mampulator<HasF> manipulator =
new Manipulator<HasF>(hf); manipulator.manipulateO:
}
} ///:-
Из-за стирания компилятор Java не может сопоставить требование о возможности вызова f для obj из manipulateO с тем фактом, что HasF содержит метод f. Чтобы вызвать f, мы должны «помочь» параметризованному классу, и передать ему ограничение; компилятор принимает только те типы, которые соответствуют указанному ограничению. Для задания ограничения используется ключевое слово extends. При заданном ограничении следующий фрагмент компилируется нормально:
//: generics/Manipulator2 java
class Manipulator2<T extends HasF> { private T obj;
public Manipulator2(T x) { obj = x; } public void manipulateO { obj.fO; }
} ///.-
Ограничение <T extends HasF> указывает на то, что параметр Т должен относиться к типу HasF или производному от него. Если это условие выполняется, то вызов f для obj безопасен.
Можно сказать, что параметр типа стирается до первого ограничения (как будет показано позже, ограничений может быть несколько). Мы также рассмотрим понятие стирания параметра типа. Компилятор фактически заменяет параметр типа его «стертой» версией, так что в предыдущем случае Т стирается до HasF, а результат получается таким, как при замене Т на HasF в теле класса.