Generics. Шаблоны в Java

При создании коллекции можно указать тип объектов, которые будут храниться в ней.

Пример:

Напечатает “str1”.

Строка Vector < String> vect; говорит о том, что вектор будет содержать только строки. Вот почему в строке String str = vect.get(0); можно обойтись без приведения типов. При создании объекта вектора указываем параметр String: vect = new Vector < String>();.

Главная цель шаблонов – type-safety, т.е. правильное соотношение типов.

Формальные параметры

В предыдущем примере однозначно был указан параметр вектора. Есть случаи, где удобнее использовать формальный параметр, подобно формальным аргументам функций. Пример:

Создан параметризованный класс FormalVector < E>. E – формальный параметр. При создании экземпляра класса вместо E указываем конкретный тип. Простой пример:

Напечатает “str1”.

В классе StringValues создаётся экземпляр класса FormalVector < String>. Вместо формального параметра E указываем String. Теперь класс FormalVector будет хранить и работать с типом String. Можно создать экземпляр класса FormalVector с другим параметром, например, Integer: FormalVector < Integer> vect = new FormalVector < Integer>(). Это значит, что FormalVector < Integer> будет работать с типом Integer.

Все экземпляры параметризованного класса представляют один тип, независимо от фактического значения параметров.

Шаблоны и подтипы

Важное правило: если класс A есть потомок класса B, то это не означает, что aClass < A> есть потомок aClass < B>.

Методы с шаблонами

Шаблоны могут применяться и в методах. Типы аргументов можно делать параметрическими. Пример:

void aMethod(Vector < E> vect);

или так:

void aMethod(Vector < ?> vect);

Рекомендуется использовать неизвестный параметр < ?> в случаях, когда от него не зависят другие аргументы и возвращаемое значение. Если такая зависимость имеется, то следует использовать формальный параметр < E>.

Возможно использовать вместе неизвестный параметр < ?> и формальный параметр < E> :

pablic < E> void aMethod(Vector < E> vect, List < ? extends E> list);

здесь аргумент list имеет ограниченный неизвестный параметр < ? extends E>, но имеется связь между двумя аргументами – это формальный параметр < E>.

Неизвестный параметр

Вместо формального параметра можно указать знак вопроса, т.е. неизвестный параметр. Изменим класс StringValues(см. выше):

Напечатает “str1”.

Новым для нас является аргумент метода

private void printValues(FormalVector < ?> vect)

Знак вопроса вместо какого-то типа говорит о том, что здесь может оказаться любой тип, т.е. FormalVector < ?> — это супертип для любого конкретного типа, например, для FormalVector < String>. Обратим внимание на первую строку метода:

Object str = vect.getVal(0);

здесь str имеет тип Object. Это позволяет безопасно получать элементы коллекции методом getVal, ведь любые объекты представляют тип Object.

Неизвестный параметр может примениться и при объявлении полей:

private Vector < ?> aVector;

Ограниченный неизвестный параметр

Ограниченный неизвестный параметр указывает, что вместо него можно подставить не любой тип, а только тип, наследуемый от определённого класса. Пример:

Vector < ? extends aClass>

здесь на место неизвестного параметра ? можно поставить aClass или его наследников, пусть даже непрямых. aClass в этом случае называется верхней границей неизвестного параметра. Другой пример:

Vector < ? super aClass>

Параметр вида < ? super aClass> называется нижняя граница. Фактический параметр должен быть родительским по отношению к aClass, или должен быть самим aClass.

Рассмотрим формальный пример. Создадим три пустых класса: ClassA

public class ClassA {}

его наследника ClassB

public class ClassB extends ClassA {}

ClassC, как наследника от ClassB:

public class ClassC extends ClassB {}

Почему классы пустые? В этом примере важно увидеть отношения наследования между классами, а не содержимое этих классов.

Теперь создадим класс MainClass, а в нём метод

static void addObject(Vector < ? extends ClassA> vect) {}

В методе addObject имеем ограниченный неизвестный параметр < ? extends ClassA>, который говорит нам, что аргументом может быть любой вектор, содержащий объекты класса ClassA или его наследников. А таковыми и являются наши три пустых класса: ClassA, ClassB, ClassC.

В коде метода main создадим объект класса ClassA

ClassA ca = new ClassA();

вектор, содержащий объекты класса ClassA

Vector < ClassA> vectA = new Vector < ClassA>();

добавим ca к vectA

vectA.add(ca);

Такой вектор соответсвует определению аргумента метода addObject, к которому мы и обращаемся:

addObject(vectA);

Для классов ClassB, ClassC действия аналогичные. Вот полный код класса MainClass:

Если метод addObject объявить так:

static void addObject(Vector < ClassA> vect) {}

т.е. без ограниченного неизвестного параметра, то получим сообщения об ошибках в строках addObject(vectB); и addObject(vectC);(несоответствие типов аргументов). Почему? Смотрите пункт “Шаблоны и подтипы”. Vector < ClassB> и Vector < ClassC> есть подтипы не Vector < ClassA>, а Vector < ? extends ClassA>.

Что такое raw type?

Объявляя класс-коллекцию, мы указываем параметр, т.е. тип объектов, хранящихся в нём. Если не указать параметр, то можем получить предупреждение “raw type”. Пример:

Можно объявлять коллекции без указания параметра, это сделано для работы с прежним кодом, когда ещё не было шаблонов. Ещё пример:

В последней строке получаем предупреждение, которое приведёт к ошибке при попытке извлечь строку из vect:

System.out.print((String) vect.get(0));

Сообщение об ошибке: java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.String. Исправить положение можно так: указать параметр для vect, Double привести к строке

Что такое unchecked conversion?

Это значит, что компилятор не гарантирует соответствие типов. Пример:

Мы не знаем, что будет храниться в aVect, там может оказаться и не строка, отсюда предупреждение. Исправляем положение, указав параметр для aVect:

Соглашение по именам параметров

Параметрические типы принято обзначать T, если имеется несколько параметров, то для следующих используют буквы, близкие к T, т.е. S, U, V и т.д. Элементы коллекций обозначают буквой E. Ключи – K, значения — V, числа – N. Во всех случаях параметры обозначают большими буквами.