Философия Java3
Шрифт:
Параметризация в 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 в теле класса.
Справедливости ради нужно заметить, что в Manipulation2.java параметризация никакой реальной пользы не дает. С таким же успехом можно выполнить стирание самостоятельно, создав непараметризованный класс:
//• generics/Manipulator3.java
class Manipulators { private HasF obj,
public Manipulator3(HasF x) { obj = x; } public void manipulateO { obj f, }
} III ~
Мы приходим к важному заключению: параметризация полезна только тогда, когда вы хотите использовать параметры типов, более «общие», нежели конкретный тип (и производные от него), то есть когда код должен работать для разных классов. В результате параметры типов и их применение в параметризованном коде сложнее простой замены классов. Впрочем, это не означает, что форма <Т extends HasF> чем-то ущербна. Например, если класс содержит метод, возвращающий Т, то параметризация будет полезной, потому что метод вернет точный тип:
// generics/ReturnGenericType.java
class ReturnGenericType<T extends HasF> { private T obj,
public ReturnGenericType(T x) { obj = x; } public T get О { return obj; }
} ///:-
Просмотрите код и подумайте, достаточно ли он «сложен» для применения параметризации.
Ограничения будут более подробно рассмотрены далее в этой главе.
Миграционная совместимость
Чтобы избежать всех потенциальных недоразумений со стиранием, необходимо четко понимать, что этот механизм не является особенностью языка. Скорее это компромисс, использованный при реализации параметризации в Java, потому что параметризация не являлась частью языка в его исходном виде. Этот компромисс создает определенные неудобства, поэтому вы должны поскорее привыкнуть к нему и понять, почему он существует.
Если бы параметризация была частью Java 1.0, то для ее реализации стирание не потребовалось бы — параметры типов сохранили бы свой статус равноправных компонентов языка, и с ними можно было бы выполнять типизованные языковые и рефлексивные операции. Позднее в этой главе будет показано, что стирание снижает «обобщенность» параметризованных типов. Параметризация в Java все равно приносит пользу, но не такую, какую могла бы приносить, и причиной тому является стирание.
В реализации, основанной на стирании, параметризованные типы рассматриваются как второстепенные компоненты языка, которые не могут использоваться в некоторых важных контекстах. Параметризованные типы присутствуют только при статической проверке типов, после чего каждый параметризованный тип в программе заменяется ^параметризованным верхним ограничением. Например, обозначения типов вида List<T> стирается до List, а обычные переменные типа — до Object, если ограничение не задано.
Главная причина для применения стирания заключается в том, что оно позволяет параметризованным клиентам использовать непараметризованные библиотеки, и наоборот. Эта концепция часто называется миграционной совместимостью. Наверное, в идеальном мире параметризация была бы внедрена везде и повсюду одновременно. На практике программисту, даже если он пишет только параметризованный код, приходится иметь дело с ^параметризованными библиотеками, написанными до Java SE5. Возможно, авторы этих библиотек вообще не намерены параметризовать свой код или собираются сделать это в будущем.