Чтение онлайн

ЖАНРЫ

Философия Java3

Эккель Брюс

Шрифт:

Подход С++

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

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

Проблемы стирания

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

Однако за стирание приходится расплачиваться. Параметризованные типы не могут использоваться в операциях, в которых явно задействованы типы времени выполнения — преобразования типов, instanceof и выражения new. Вся информация о типах параметров теряется, и при написании параметризованного кода вам придется постоянно напоминать себе об этом. Допустим, вы пишете фрагмент кода

Поделиться с друзьями: