Рейтинговые книги
Читем онлайн Философия Java3 - Брюс Эккель

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 83 84 85 86 87 88 89 90 91 ... 132

class CheckoutStand {} class Office {}

public class Store extends ArrayList<Aisle> {

private ArrayList<CheckoutStand> checkouts =

new ArrayList<CheckoutStand>(); private Office office = new OfficeO. public Store(int nAisles, int nShelves, int nProducts) { for(int i = 0; i < nAisles; i++)

add(new Aisle(nShelves. nProducts));

}

public String toStringO {

StringBuilder result = new StringBuilderO; for(Aisle a this)

for(Shelf s : a)

for(Product p • s) {

result.append(p); result.append("n").

}

return result.toStringO.

}

public static void main(String[] args) {

System out.printin(new Store(14, 5. 10)).

}

} /* Output.

258: Test, цена: $400.99 861- Test, цена- $160.99 868: Test, цена: $417.99 207- Test, цена- $268.99 551- Test. цена. $114.99 278: Test, цена: $804.99

520. Test, цена: $554.99 140: Test, цена: $530.99

*///;-

Как видно из Store.toString(), в результате мы получаем многоуровневую архитектуру контейнеров, не лишаясь преимуществ безопасности типов и управляемости. Впечатляет и то, что построение такой модели не потребует заметных умственных усилий.

Тайна стирания

Когда вы приступаете к более глубокому изучению контейнеров, некоторые обстоятельства на первых порах выглядят довольно странно. Например, запись ArrayList.class возможна, а запись ArrayList<Integer>.class — нет. Или возьмите следующий фрагмент:

//: generics/ErasedTypeEquivalence.java import java.util.*;

public class ErasedTypeEquivalence {

public static void main(String[] args) {

Class cl = new ArrayList<String>().getClassO: Class c2 = new ArrayList<Integer>().getClass(), System.out.pri ntln(cl == c2);

}

} /* Output:

true

*///.-

Было бы логично считать, что ArrayList<String> и ArrayList<Integer> — разные типы, поэтому их поведение должно различаться, и при попытке поместить Integer в ArrayList<String> результат (неудача) должен отличаться от того, который будет получен при помещении Integer в ArrayList<Integer> (успех). Однако эта программа создает впечатление, что эти типы одинаковы. Следующий пример еще сильнее запутывает ситуацию:

//. generics/Lostlnformation.java import java util *.

class Frob {} class Fnorkle {} class Quark<Q> {}

class Particle<POSITION,MOMENTUM> {}

public class Lostlnformation {

public static void main(String[] args) {

List<Frob> list = new ArrayList<Frob>(); Map<Frob,Fnorkle> map = new HashMap<Frob.Fnorkle>(); Quark<Fnorkle> quark = new Quark<Fnorkle>(); Particle<Long.Double> p = new Particle<Long.Double>(): System.out.pri ntln(Arrays.toStri ng(

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 в теле класса.

Справедливости ради нужно заметить, что в 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, если ограничение не задано.

1 ... 83 84 85 86 87 88 89 90 91 ... 132
На этой странице вы можете бесплатно читать книгу Философия Java3 - Брюс Эккель бесплатно.
Похожие на Философия Java3 - Брюс Эккель книги

Оставить комментарий