Map (Ключевое слово extends и вопросительные знаки будут описаны позднее в этой главе.) Казалось бы, эта конструкция избыточна, а компилятор мог бы вычислить один из списков аргументов по-другому. В действительности сделать это он не может, но вычисление аргументов типов все же позволяет немного упростить код. Например, мы можем создать вспомогательную библиотеку с различными статическими методами, содержащими самые распространенные реализации различных контейнеров:
//: net/mindview/uti1/New.java
// Utilities to simplify generic container creation
// by using type argument inference.
package net mindview util; import java util *.
public class New {
public static
}
public static
}
public static
}
public static
}
public static
}
// Примеры:
public static void main(String[] args) {
Map
}
} ///-
Примеры использования представлены в main() — вычисление аргументов типов устраняет необходимость в повторении списков параметров. Этот прием можно использовать в holding/MapOfList.java:
public class SimplerPets {
public static void main(String[] args) {
Map }
} ///.-
Пример интересный, однако трудно сказать, насколько он эффективен в действительности. Человеку, читающему код, придется просмотреть дополнительную библиотеку и разобраться в ее коде. Возможно, вместо этого стоит оставить исходное (пусть и избыточное) определение — как ни парадоксально, этот вариант проще. Хотя, если в стандартную библиотеку Java будет добавлено некое подобие New.java, им можно будет пользоваться.
Вычисление типов не работает ни в каких других ситуациях, кроме присваивания. Если передать результат вызова метода (скажем, New.mapO) в аргументе другого метода, компилятор
//: generics/LimitsOfInference.java import typeinfo.pets.*; import java.util.*;
public class LimitsOflnference { static void
f(Map // f(New.mapO); II He компилируется
}
} ///:-
Явное указание типа
При вызове параметризованного метода также можно явно задать тип, хотя на практике этот синтаксис используется редко. Тип указывается в угловых скобках за точкой, непосредственно перед именем метода. При вызове метода в пределах класса необходимо ставить this перед точкой, а при вызове статических методов перед точкой указывается имя класса. Проблема, продемонстрированная в LimitsOflnference.java, решается при помощи следующего синтаксиса:
//: generics/ExplicitTypeSpecification.java import typeinfo.pets.*: import java.util.*; i mport net.mi ndvi ew.uti1.*:
public class ExplicitTypeSpecification {
static void f(Map }
} ///:-
Конечно, при этом теряются преимущества от использования класса New для уменьшения объема кода, но дополнительный синтаксис необходим только за пределами команд присваивания.
Параметризованные методы и переменные списки аргументов
Параметризованные методы нормально сосуществуют с переменными списками аргументов:
II: generics/GenericVarargs.java import java.util.*;
public class GenericVarargs {
public static
result.add(item);-return result;
public static void main(String[] args) { List
Is = makeListC"ABCDEFFHIJKLMNOPQRSTUVWXYZ".split("")); System.out.printi n( Is);
}
} /* Output: [A]
[А, В. C]
[, А, В. C. D, E. F, F, H, I, J. K. L, M, N, 0. P. Q, R. S. T. U. V. W. X, Y, Z] *///•-
Метод makeList() предоставляет ту же функциональность, что и метод java. util.Arrays.asList() из стандартной библиотеки.
Использование параметризованных методов с Generator
Генераторы хорошо подходят для заполнения Collection, и для выполнения этой операции было бы удобно создать параметризованный метод:
//: generics/Genetcitors.java // Обобщенный метод заполнения коллекции import generics.coffee.*; import java.util.*; import net.mindview.util.*;
public class Generators {
public static
fill(Collection
coll .add(gen.nextO); return coll;
}
public static void main(String[] args) { Collection
new ArrayList
System.out.printin(c); Collection
new ArrayList
System.out.printO + \ ");
}
} /* Output: Americano 0 Latte 1 Americano 2 Mocha 3
1. 1. 2. 3. 5. 8. 13. 21. 34. 55. 89. 144. *///:-
Обратите внимание на то, как параметризованный метод fill() применяется к контейнерам и генераторам как для типа Coffee, так и для Integer.
Обобщенный генератор
Следующий класс создает генератор для любого класса, обладающего конструктором по умолчанию. Для уменьшения объема кода в него также включен параметризованный метод для получения BasicGenerator:
//: net/mindview/uti1/BasicGenerator java // Автоматическое создание Generator для класса // с конструктором по умолчанию (без аргументов) package net.mindview util;
public class BasicGenerator
public BasicGenerator(Class
// Предполагается, что type является public-классом, return type.newlnstance(); } catch(Exception e) {
throw new RuntimeException(e);
}
}
// Получение генератора по умолчанию для заданного type: public static
}
} ///:-
Класс предоставляет базовую реализацию, создающую объекты класса, который (1) является открытым (так как BasicGenerator определяется в отдельном пакете, соответствующий класс должен иметь уровень доступа public, не ограничиваясь пакетным доступом), и (2) обладает конструктором по умолчанию (то есть конструктором без аргументов). Чтобы создать один из таких объектов BasicGenerator, следует вызвать метод create() и передать ему обозначение генерируемого типа, параметризованный метод create() позволяет использовать запись BasicGenerator.create(MyType.class) вместо более громоздкой конструкции new BasicGenerator
Для примера рассмотрим простой класс с конструктором по умолчанию:
//: generics/CountedObject.java
public class CountedObject {
private static long counter = 0; private final long id = counter++; public long id() { return id; }
public String toStringO { return "CountedObject " + id,} } ///:-
Класс CountedObject отслеживает количество созданных экземпляров и включает его в выходные данные toString().
При помощи BasicGenerator можно легко создать Generator для CountedObject:
//: generics/BasicGeneratorDemo java i mport net.mi ndvi ew.uti1.*.
public class BasicGeneratorDemo {
public static void main(String[] args) { Generator
BasicGenerator create(CountedObject.class), for(int i = 0; i < 5; i++)
System, out pri ntl n(gen. nextO);
}
} /* Output CountedObject 0 CountedObject 1 CountedObject 2 CountedObject 3 CountedObject 4 */// ~
Как видите, применение параметризованного метода снижает объем кода, необходимого для получения объекта Generator. Раз уж механизм параметризации Java все равно заставляет вас передавать объект Class, его можно заодно использовать для вычисления типа в методе create().
Упрощение работы с кортежами
Используя вычисление аргументов типов в сочетании со static-импортом, можно оформить приведенную ранее реализацию кортежей в более универсальную библиотеку. В следующем примере кортежи создаются перегруженным статическим методом:
//• net/mi ndvi ew/uti1/Tuple.java // Библиотека для работы с кортежами // с использованием вычисления аргументов типов package net mindview.util,
public class Tuple {
public static TwoTuple tuple(A а. В b) { return new TwoTuple(a, b).
}
public static ThreeTuple tuple(A а. В b. С с) {
return new ThreeTuple(a, b, c):
}
public static FourTuple tuple(A а. В b. С с. D d) {
return new FourTuple(a. b. c. d).
}
FiveTuple tuple(A а. В b, С с, D d. E e) {
return new FiveTuple(a, b. c. d. e);
}
} ///:-
А вот как выглядит обновленная версия TupleTest.java для тестирования Tuple.java:
//: generics/TupleTest2.java import net.mindview.util.*;
import static net.mindview.util.Tuple.*;
public class TupleTest2 {
static TwoTuple
}
static TwoTuple f2() { return tupleC'hi", 47); } static ThreeTuple
}
static
FourTuple
return tuple(new VehicleO, new AmphibianO, "hi", 47);
}
static
FiveTuple
}
public static void main(String[] args) {
TwoTuple
}
} /* Output (hi, 47) (hi, 47)
(Amphibian@7d772e, hi, 47) (Vehicle@757aef, Amphibian@d9f9c3, hi. 47) (Vehicle@la46e30, Amphibian@3e25a5. hi. 47, 11.1) *///:-
Обратите внимание: f() возвращает параметризованный объект TwoTuple, a f2() — ^параметризованный объект TwoTuple. Компилятор в данном случае не выдает предупреждения о f2(), потому что возвращаемое значение не используется в «параметризованном» стиле: в каком-то смысле проводится «восходящее преобразование» его до непараметризованного TwoTuple. Но, если попытаться сохранить результат f2() в параметризованном объекте TwoTuple, компилятор выдаст предупреждение.
Вспомогательный класс Set
Рассмотрим еще один пример использования параметризованных методов: математические операции между множествами. Эти операции удобно определить в виде параметризованных методов, используемых с различными типами:
//: net/mi ndvi ew/uti1/Sets.java package net.mindview.util; import java.util.*;
public class Sets {
public static
result.addAl1(b). return result;
}
public static
Set
}
// Вычитание подмножества из надмножества-public static
Set
}
// Дополнение -- все. что не входит в пересечение public static
return difference(union(a. b). intersection^, b));
}
} ///:-
Первые три метода дублируют первый аргумент, копируя его ссылки в новый объект HashSet, поэтому аргументы Set не изменяются напрямую. Таким образом, возвращаемое значение представляет собой новый объект Set.
Четыре метода представляют математические операции с множествами: union() возвращает объект Set, полученный объединением множеств-аргументов, intersection() возвращает объект Set с общими элементами аргументов, difference() вычисляет разность множеств, a complement) — объект Set со всеми элементами, не входящими в пересечение. Чтобы создать простой пример использования этих методов, мы воспользуемся перечислением, содержащим разные названия акварельных красок:
//: generics/watercolors/Watercolors java package generics.watercolors;
public enum Watercolors {
ZINC. LEM0N_YELL0W. MEDIUM_YELLOW, DEEP_YELLOW, ORANGE. BRILLIANT_RED. CRIMSON. MAGENTA. ROSE_MADDER. VIOLET. CERULEAN_BLUE_HUE. PHTHALO_BLUE, ULTRAMARINE. COBALT_BLUE_HUE, PERMANENT_GREEN. VIRIDIAN_HUE. SAP_GREEN. YELL0W_0CHRE. BURNT_SIENNA. RAWJJMBER, BURNTJJMBER. PAYNES_GRAY. IVORY_BLACK } ///:-
Для удобства (чтобы избежать уточнения всех имен) в следующем примере это перечисление импортируется статически. Мы используем EnumSet — новый инструмент Java SE5 для простого создания Set на базе перечисления. Статическому методу EnumSet.range() передаются первый и последний элементы диапазона, по которому строится множество:
II: generics/WatercolorSets.java import generics.watercolors.*; import java.util.*;
import static net.mindview.util.Print.*;
import static net.mindview util.Sets *; import static generics.watercolors.Watercolors.*;
public class WatercolorSets {
public static void main(String[] args) { Set
EnumSet.range(BRILLIANT_RED, VIRIDIAN_HUE); Set
EnumSet range(CERULEAN_BLUE_HUE, BURNT_UMBER), printCsetl. " + setl); print("set2- " + set2);
print("union(setl. set2)- " + union(setl. set2)); Set
difference(setl. subset)). print("difference(set2. subset). " +
difference(set2. subset)); print("complement(setl. set2) " + complement(setl. set2));
}
} /* Output.
setl. [BRILLIANT_RED. CRIMSON. MAGENTA. ROSE_MADDER, VIOLET. CERULEAN_BLUE_HUE. PHTHALO_BLUE. ULTRAMARINE. COBALT_BLUE_HUE. PERMANENT_GREEN. VIRIDIAN_HUE] set2- [CERULEAN_BLUE_HUE, PHTHALO_BLUE. ULTRAMARINE. COBALT_BLUE_HUE. PERMANENT_GREEN. VIRIDIAN_HUE. SAP_GREEN. YELL0W_0CHRE. BURNT_SIENNA. RAWJJMBER. BURNT_UMBER] union(setl. set2) [SAP_GREEN. ROSE_MADDER, YELL0W_0CHRE. PERMANENT_GREEN. BURNTJJMBER, COBALT_BLUE_HUE, VIOLET. BRILLIANT_RED. RAWJJMBER. ULTRAMARINE. BURNT_SIENNA. CRIMSON. CERULEAN_BLUE_HUE. PHTHALO_BLUE. MAGENTA. VIRIDIAN_HUE]
intersection(setl. set2): [ULTRAMARINE. PERMANENT_GREEN. COBALT_BLUE_HUE. PHTHALO_BLUE. CERULEAN_BLUE_HUE. VIRIDIAN_HUE]
difference(setl, subset): [ROSE_MADDER, CRIMSON. VIOLET. MAGENTA. BRILLIANT_RED] difference(set2. subset): [RAWJJMBER, SAP_GREEN. YELLOW J3CHRE, BURNT_SIENNA, BURNTJJMBER]
complement(setl, set2): [SAP_GREEN. ROSE_MADDER. YELL0W_0CHRE. BURNTJJMBER, VIOLET. BRILLIANT_RED, RAW_UMBER. BURNT_SIENNA. CRIMSON. MAGENTA] *///:-
В выходных данных показаны результаты выполнения каждой операции. В следующем примере представлены варианты вызова Sets.difference() для разных классов Collection и Map из java.util:
//: net/mi ndvi ew/uti1/Contai nerMethodDi fferences.java package net.mindview.util; import java lang reflect *; import java.util.*,
public class ContainerMethodDifferences {
static Set
Set
}
static void interfaces(Class > type) {
System.out.print("Interfaces in " +
type getSimpleNameO + ": "); List
for(Class > с : type.getlnterfacesO) result.add(c.getSimpleName()); System.out.println(result);
}
static Set
differencesass > superset, Class > subset) {
System.out.pri nt(superset.getSimpleName() +
" extends " + subset.getSimpleNameO + ", adds: "); Set
methodSet(superset). methodSet(subset)); сотр.removeAll(object); // He показывать методы 'Object' System.out.println(comp); interfaces(superset),
}
public static void main(String[] args) {
System.out.printlnC'Collection: " +
methodSet(Collection.class)), interfaces(Collection.class); difference(Set.class. Collection.class); difference(HashSet.class. Set.class); difference(LinkedHashSet.class, HashSet.class); difference(TreeSet.class. Set.class); differences st. class. Col 1 ecti on.cl ass); difference(ArrayList.class, List.class); differences nkedLi st. class. List.class); difference(Queue.class. Collection.class); di fference(Pri ori tyQueue.class. Queue.class); System.out.println("Map: " + methodSet(Map.class)); difference(HashMap.class. Map.class); difference(LinkedHashMap.class. HashMap.class); difference(SortedMap.class. Map.class); difference(TreeMap.class, Map class);
}
} ///:-
Анонимные внутренние классы
Параметризация также может применяться к внутренним классам и анонимным внутренним классам. Пример реализации интерфейса Generator с использованием анонимных внутренних классов:
//: generics/BankTeller.java
II Очень простая имитация банковского обслуживания.
import java.util.*;
import net.mindview.util.*;
class Customer {
private static long counter = 1; private final long id = counter++; private Customer() {}
public String toStringO { return "Customer " + id; } // Метод для получения объектов Generator: public static Generator
public Customer nextO { return new CustomerO; }
class Teller {
private static long counter = 1; private final long id = counter++; private TellerO {}
public String toStringO { return "Teller " + id; } // Синглетный объект Generator: public static Generator
public Teller next О { return new TellerO; }
}:
}
public class BankTeller {
public static void serve(Teller t, Customer c) {
System.out.printin(t + " обслуживает " + с);
}
public static void main(String[] args) { Random rand = new Random(47); Queue
serve(tellers.get(rand.nextlnt(tellers.size())), c);
}
} /* Output:
Teller
3
обслуживает
Customer
1
Teller
2
обслуживает
Customer
2
Teller
3
обслуживает
Customer
3
Teller
1
обслуживает
Customer
4
Teller
1
обслуживает
Customer
5
Teller
3
обслуживает
Customer
6
Teller
1
обслуживает
Customer
7
Teller
2
обслуживает
Customer
8
Teller
3
обслуживает
Customer
9
Teller
3
обслуживает
Customer
10
Teller
2
обслуживает
Customer
11
Teller
4
обслуживает
Customer
12
Teller
2
обслуживает
Customer
13
Teller
1
обслуживает
Customer
14
Teller
1
обслуживает
Customer
15
*///•-
И Customer, и Teller содержат приватные конструкторы, поэтому для создания их объектов пользователь вынужден использовать объекты Generator. Customer содержит метод generator(), который при каждом вызове создает новый объект Generator
Поскольку метод generator() в Customer и объект Generator в Teller являются статическими, они не могут быть частью интерфейса, поэтому «обобщить» эту"
конкретную идиому не удастся. Несмотря на это обстоятельство, она достаточно хорошо работает в методе fill().
Построение сложных моделей
К числу важных преимуществ параметризации относится простота и надежность создания сложных моделей. Например, можно легко создать список (List) с элементами-кортежами:
//: generics/TupleList.java
// Построение сложных параметризованных типов путем объединения
import java.util.*;
import net.mindview util.*;
public class TupleList extends ArrayList public static void main(String[] args) {
TupleList
new TupleList
for(FourTuple
}
} /* Output:
(Vehicle@llb86e7. Amphibian@35ce36, hi. 47) (Vehicle@757aef, Amphibian@d9f9c3. hi. 47) *///.-
Запись получается довольно громоздкой (особенно при создании итератора), однако вы получаете довольно сложную структуру данных без излишков программного кода.
А вот другой пример, который показывает, как легко строить сложные модели на основе параметризованных типов. Хотя каждый класс представляет собой автономный «строительный блок», их совокупность имеет сложную структуру. В данном случае моделируется магазин с товарами, полками и стеллажами:
//. generics/Store.java
// Построение сложной модели на базе параметризованных контейнеров, import java util *. import net.mindview.util.*.
class Product {
private final int id. private String description; private double price;
public Product(int IDnumber, String descr. double price){ id = IDnumber; description = descr, this.price = price; System.out.pri ntln(toString());
}
public String toStringO {
return id + " + description + ", цена: $" + price;
public void priceChange(double change) { price += change,
}
public static Generator
private Random rand = new Random(47), public Product next О {
return new Product(rand nextlnt(lOOO), "Test",
Math round(rand nextDoubleO * 1000 0) + 0 99).
class Shelf extends ArrayList
Generators fill(this. Product generator. nProducts),
class Aisle extends ArrayList
public AisleCint nShelves, int nProducts) { for(int i = 0; i < nShelves; i++) add(new Shelf(nProducts)),
class CheckoutStand {} class Office {}
public class Store extends ArrayList
private ArrayList
new ArrayList
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
//: generics/ErasedTypeEquivalence.java import java.util.*;
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class cl = new ArrayList
}
} /* Output:
true
*///.-
Было бы логично считать, что ArrayList
//. generics/Lostlnformation.java import java util *.
class Frob {} class Fnorkle {} class Quark {}
class Particle
public class Lostlnformation {
public static void main(String[] args) {
List
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 реализуется с применением
Подход С++
В следующем примере, написанном на С++, используются
//: generics/Templates.cpp #include
tempiate
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
Класс Manipulator хранит объект типа Т. Нас здесь интересует метод manipulateO, который вызывает метод f() для obj. Как он узнает, что у параметра типа Т существует метод f()? Компилятор С++ выполняет проверку при создании экземпляра шаблона, поэтому в точке создания Manipulator
Написать такой код на С++ несложно, потому что при создании экземпляра шаблона код шаблона знает тип своих параметров. С параметризацией 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
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
new Manipulator
}
} ///:-
Из-за стирания компилятор Java не может сопоставить требование о возможности вызова f() для obj из manipulateO с тем фактом, что HasF содержит метод f(). Чтобы вызвать f(), мы должны «помочь» параметризованному классу, и передать ему
//: generics/Manipulator2 java
class Manipulator2
public Manipulator2(T x) { obj = x; } public void manipulateO { obj.fO; }
} ///.-
Ограничение
Можно сказать, что параметр типа
Справедливости ради нужно заметить, что в Manipulation2.java параметризация никакой реальной пользы не дает. С таким же успехом можно выполнить стирание самостоятельно, создав непараметризованный класс:
//• generics/Manipulator3.java
class Manipulators { private HasF obj,
public Manipulator3(HasF x) { obj = x; } public void manipulateO { obj f(), }
Мы приходим к важному заключению: параметризация полезна только тогда, когда вы хотите использовать параметры типов, более «общие», нежели конкретный тип (и производные от него), то есть когда код должен работать для разных классов. В результате параметры типов и их применение в параметризованном коде сложнее простой замены классов. Впрочем, это не означает, что форма <Т extends HasF> чем-то ущербна. Например, если класс содержит метод, возвращающий Т, то параметризация будет полезной, потому что метод вернет точный тип:
// generics/ReturnGenericType.java
class ReturnGenericType
public ReturnGenericType(T x) { obj = x; } public T get О { return obj; }
} ///:-
Просмотрите код и подумайте, достаточно ли он «сложен» для применения параметризации.
Ограничения будут более подробно рассмотрены далее в этой главе.
Миграционная совместимость
Чтобы избежать всех потенциальных недоразумений со стиранием, необходимо четко понимать, что этот механизм
Если бы параметризация была частью Java 1.0, то для ее реализации стирание не потребовалось бы — параметры типов сохранили бы свой статус равноправных компонентов языка, и с ними можно было бы выполнять типизованные языковые и рефлексивные операции. Позднее в этой главе будет показано, что стирание снижает «обобщенность» параметризованных типов. Параметризация в Java все равно приносит пользу, но не такую, какую могла бы приносить, и причиной тому является стирание.
В реализации, основанной на стирании, параметризованные типы рассматриваются как второстепенные компоненты языка, которые не могут использоваться в некоторых важных контекстах. Параметризованные типы присутствуют только при статической проверке типов, после чего каждый параметризованный тип в программе заменяется ^параметризованным верхним ограничением. Например, обозначения типов вида List
Главная причина для применения стирания заключается в том, что оно позволяет параметризованным клиентам использовать непараметризованные библиотеки, и наоборот. Эта концепция часто называется
Из-за этого механизму параметризации Java приходится поддерживать не только
Проблемы стирания
Итак, главным аргументом для применения стирания является процесс перехода с непараметризованного кода на параметризованный и интеграция параметризации в язык без нарушения работы существующих библиотек. Стирание позволяет использовать существующий ^параметризованный код без изменений, пока клиент не будет готов переписать свой код с использованием параметризации.
Однако за стирание приходится расплачиваться. Параметризованные типы не могут использоваться в операциях, в которых явно задействованы типы времени выполнения — преобразования типов, instanceof и выражения new. Вся информация о типах параметров теряется, и при написании параметризованного кода вам придется постоянно напоминать себе об этом. Допустим, вы пишете фрагмент кода
class Foo
}
Может показаться, что при создании экземпляра Foo:
Foo
код class Foo должен знать, что он работает с Cat. Синтаксис создает впечатление, что тип Т подставляется повсюду внутри класса. Но на самом деле это не так, и при написании кода для класса вы должны постоянно напоминать себе: «Нет, это всего лишь Object».
Кроме того, стирание и миграционная совместимость означают, что контроль за использованием параметризации не настолько жесткий, как хотелось бы:
//: generics/ErasureAndlnheritance.java
class GenericBase
public void set(T arg) { arg = element; } public T get О { return element. }
}
class Derivedl
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 существует
@SuppressWarnings("unchecked")
Обратите внимание: директива применяется к методу, генерирующему предупреждение, а не ко всему классу. При подавлении предупреждений желательно действовать в самых узких рамках, чтобы случайно не скрыть настоящую проблему.
Ошибка, выдаваемая в Derived3, означает, что компилятор рассчитывает увидеть «обычный» базовый класс.
Добавьте к этому дополнительные усилия на управление ограничениями, если вы не желаете интерпретировать параметр типа как простой Object, — и что мы получаем в остатке? Гораздо больше хлопот при гораздо меньше, пользе по сравнению с параметризованными типами в языках вроде С++, Ada или Eiffel. Конечно, это вовсе не означает, что эти языки в целом эффективнее Java в большинстве задач программирования, а говорит лишь о том, что их механизмы параметризации типов отличаются большей гибкостью и мощью, чем в Java.
Проблемы на границах
Пожалуй, самый странный аспект параметризации, обусловленный стиранием, заключается в возможности представления заведомо бессмысленных вещей. Пример:
//: generics/ArrayMaker.java
import java.lang.reflect.*;
import java.util.*;
public class ArrayMaker
public ArrayMaker(Class
return (T[])Array.newInstance(kind, size);
}
public static void main(String[] args) { ArrayMaker
new ArrayMaker
}
} /* Output;
[null, null, null. null. null. null. null. null, null]
*///:-
Несмотря на то что объект kind хранится в виде Class
Обратите внимание: для создания массивов в параметризованном коде рекомендуется использовать Array.newlnstance().
Если вместо массива создается другой контейнер, ситуация меняется:
//: generics/ListMaker.java
import java util.*;
public class ListMaker
List
ListMaker
}
} ///:-
Компилятор не выдает предупреждений, хотя мы знаем, что <Т> в new ArrayList
Но действительно ли этот элемент не имеет смысла? Что произойдет, если мы поместим в список несколько объектов, прежде чем возватим его?
II: generics/Fi1ledListMaker java
import java.util.*;
public class FilledListMaker
List
result.add(t); return result:
}
public static void main(String[] args) {
FilledListMaker
new Fi11edListMaker
}
} /* Output:
[Hello, Hello. Hello. Hello]
Хотя компилятор ничего не может знать о Т в create(), он все равно способен проверить — на стадии компиляции — что заносимые в result объекты имеют тип Т и согласуются с ArrayList
Так как стирание удаляет информацию о типе внутри тела метода, на стадии выполнения особую роль приобретают
// 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; // Метод "
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
public void set(T obj) { this.obj = obj; } public T get О { return obj; } public static void main(String[] args) { GenericHolder
new GenericHolder
}
Необходимость преобразования выходного значения 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; // Метод "
7:
astore_l
8:
aload_l
9:
ldc #5; // String Item
11
invokevirtual #6; II Метод set:(Object;)V
14
aload_l
15
invokevirtual #7; // Метод get:OObject:
18
checkcast #8; // Класс java/lang/String
21
astore_2
22
return
Как видите, байт-код идентичен. Дополнительная работа по проверке входного типа set() выполняется компилятором «бесплатно». Преобразование выходного значения get() по-прежнему сохранилось, но, по крайней мере, вам не приходится выполнять его самостоятельно — оно автоматически вставляется компилятором.
Компенсация за стирание
Как мы видели, в результате стирания становится невозможным выполнение некоторых операций в параметризованном коде. Все, для чего необходима точная информация о типе во время выполнения, работать не будет:
//: generics/Erased.java // {CompileTimeError} (He компилируется)
public class Erased
private final int SIZE = 100: public static void f(Object arg) { if(arg instanceof T) {} T var = new TO; T[] array = new T[SIZE]; T[] array = (T)new Object[SIZE]
}
}