На этом шаге мы рассмотрим ограничения и пределы обобщений
Представим ряд ограничений, которые следует учитывать, обращаясь с обобщениями в Java. Большинство этих ограничений являются следствием стирания типов.
Примитивный тип нельзя подставить вместо типа параметров. Это означает, что не бывает объекта типа Pair<double>, а только объекты типа Pair<Double>. Это объясняется тем, что после стирания типов в классе Pair отсутствуют поля типа Object, и поэтому их нельзя использовать для хранения значений типа double. Вместо примитивных типов можно использовать их классы-обертки.
В виртуальной машине объекты всегда имеют определенный необобщенный тип. Поэтому все запросы типов во время выполнения дают только базовый тип. Например, следующий код не скомпилируется:
if (a instanceof Pair<String>) {...}
Это же справедливо в отношении следующего кода:
if (a instanceof Pair<T>) {...}
А также в отношении приведении типов:
Pair<String> a = (Pair<String>) b;
Отметим, что функция getClass() всегда возвращает базовый тип. В следующем примере результатом сравнения оказывается логическое значение true, потому что при обоих вызовах метода getClass() возвращается объект типа Pair.class:
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() == employeePair.getClass()) {...}
Нельзя объявить массив параметризованных типов вроде следующего:
Pair<String>[] table = new Pair<String>[10];
После стирания типов таблицы становятся Pair[]. Но его можно преобразовать в тип Object[] следующим образом:
Object[] objarray = table;
В массиве запоминается тип его элементов и генерируется исключение типа ArrayStoreException, если попытаться сохранить в нем элемент неверного типа.
Но стирание делает этот механизм неэффективным для обобщенных типов. Рассмотрим пример:
objarray[0] = new Pair<Employee>();
Приведенное выше присваивание пройдет проверку на сохранение в массиве, но выдаст ошибку соблюдения типов. Именно поэтому массивы параметризованных типов не допускаются.
Следует иметь в виду, что не допускается только создание подобных массивов. Но разрешается объявить переменную, например, так:
Pair<String>[]
Рассмотрим следующий пример:
public static <T> void addAll(Collection<T> coll, T... ts) { for (T t : ts) { coll.add(t); } }
Напомним, что параметр ts на самом деле является массивом, содержащим все предоставляемые аргументы. Вызвать метод addAll можно следующим способом:
Collection<Pair<String>> table = ...; Pair<String> pair1 = ...; Pair<String> pair2 = ...; Pair<String> pair3 = ...; Pair<String> pair4 = ...; Pair<String> pair5 = ...; addAll(table, pair1, pair2, pair3, pair4, pair5);
Для вызова этого метода в виртуальной машине Java придется сформировать массив объектов типа Pair<String>, что не по правилам. Но эти правила были ослаблены, чтобы уведомлять в подобных случаях только о предупреждений.
Подавить выдачу такого предупреждения можно двумя способами:
@SafeVarargs public static <T> void addAll(Collection<T> coll, T... ts) {...}
Теперь этот метод можно вызывать с аргументами обобщенных типов. Приведенную выше аннотацию можно ввести в любые методы, выбирающие элементы из массива параметров - наиболее характерного примера употребления переменного числа аргументов.
Переменные типа нельзя использовать в выражениях вроде new T {...}, new T[...] или T.class. Стирание типов может изменить обобщенный тип T на Object, а вызывать конструктор new Object(), конечно, не стоит.
На переменные типа нельзя ссылаться в статических полях или методах. Рассмотрим пример:
public class Singleton<T> { private static T singleInstance; public static T getSingleInstance() { if (singleInstance == null) { /*конструирование нового объекта*/ } return singleInstance; } }
Если бы такое было бы возможно, то программа должна была бы сконструировать экземпляр типа Singleton<Random> для общего генератора случайных чисел и экземпляр типа Singleton<String>. Но такое не возможно, так как после стирания типов остается только один класс Singleton и только одно поле singleInstance. Именно по этой причине статические поля и методы с переменными типа просто не допустимы.
Генерировать или перехватывать объекты обобщенного класса в виде исключений не допускается. Обобщенный класс не может расширять класс Throwable. Например, в следующем примере код не скомпилируется:
public class Problem<T> extends Throwable {....}
Кроме того, переменную типа нельзя использовать в блоке catch. Например, следующий метод не будет скомпилирован:
public static <T extends Throwable> void doWork(Classt) { try { /*код*/ } catch (T e) { /*обработчик исключения*/ } }
Но в то же время переменные типа можно использовать в определениях исключений. Например, следующий код вполне допустим:
public static <T extends Throwable> void doWork(T t) throws T { try { /*код*/ } catch (Throwable e) { t.initCause(e); throw t; } }
Не допускается создавать условия, приводящие к конфликтам после стирания обобщенных типов. Допустим, в обобщенный класс Pair вводится метод equals() следующим образом:
public class Pair<T> { public boolean equals(T value) { return first.equals(value) && second.equals(value); } ... }
Рассмотрим теперь класс Pair<String>. На первый взгляд кажется, что в нем два метода equals():
boolean equals(Object); boolean equals(String);
Но это заблуждение. В результате стирания типов метод boolean equals(T) становится методом boolean equals(Object), который вступает в конфликт с методом Object.equals(). Понятно, что в таком случае для разрешения конфликта следует переименовать соответствующий метод.
На следующем шаге мы рассмотрим правила наследования обобщенных типов