Цикл статей «Учебник Java 8».
Следующая статья — «Java 8 исключения».
Предыдущая статья — «Java 8 ещё раз о перегрузке методов».
Обобщения (Generics) позволяют указать ограничения, накладываемое на поведение класса или методов, в терминах неизвестных типов.
Содержание
Простой класс Lair
Обобщённая версия класса Lair
Соглашение об именовании переменных типа
Создание экземпляра обобщённого типа и обращение к нему
Бриллиантовая операция (Diamond operator)
Несколько параметров типа
Параметризованный тип
Сырой тип (Raw type)
Сообщения об ошибках “unchecked”
Обобщённые методы
Ограниченные параметры типа
Обобщения, наследование и дочерние типы
Выведение типов
— Выведение типов и обобщённый методы
— Выведение типов и создание экземпляра обобщённого класса
— Выведение типа и обобщённые конструкторы обобщённых и необобщённых классов
— Целевые типы
Подстановочный символ (wildcard)
— Подстановочный символ, ограниченный сверху (Upper bounded wildcard)
— Неограниченный подстановочный символ (Unbounded wildcard)
— Ограниченный снизу подстановочный символ (Lower bound Wildcard)
— Подстановочные символы и дочерние типы
— Захват символа подстановки (Wildcard Capture) и вспомогательные методы
— Руководство по использованию подстановочного символа
Стирание типа (Type Erasure)
— Стирание типа в обобщённых типах
— Стирание типа в обобщённых методах
— Влияние стирания типа и методы-мосты (bridge methods)
— —Методы-мосты (Bridge Methods)
— Загрязнение кучи (Heap pollution)
— Потенциальные уязвимости методов с произвольным числом параметров с нематериализуемыми формальными параметрами
— Подавление предупреждений для методов с произвольным количеством параметров с нематериализуемыми формальными параметрами
Ограничения обобщений
— Нельзя создавать экземпляры обобщённых типов с примитивными типами в качестве аргументов типа.
— Нельзя создавать экземпляры параметров типа
— Нельзя объявлять статические поля с типом параметра типа
— Нельзя использовать приведения типа или instanceof с параметризованными типами
— Невозможно создавать массивы параметризованных типов
— Нельзя создавать, ловить (catch) или бросать (throw) объекты параметризованных типов
— Нельзя перегружать метод так, чтобы формальные параметры типа стирались в один и тот же сырой тип
Простой класс Lair
Посмотрите следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// Логово class Lair { // Житель Object inhabitant; public void setInhabitant(Object inhabitant) { this.inhabitant = inhabitant; } public Object getInhabitant() { return this.inhabitant; } } class Goblin { } class Main { public static void main(String[] args) { Lair lair = new Lair(); // указываем жителя. lair.setInhabitant(new Goblin()); // Нужно явное приведение типа! Goblin goblin = (Goblin) lair.getInhabitant(); } } |
Так как класс работает с Object , вы можете свободно поместить в жилище Lair абсолютно любой класс. Нет никакого способа проверки использования класса во время компиляции. Одна часть кода может поселить в жилище гоблина Goblin , а другая попытаться вытащить джинна Genie , что приведёт к ошибке во время выполнения.
Обобщённая версия класса Lair
Обобщённый класс объявляется вот так:
1 |
class name<T1, T2, ..., Tn> { /* ... */ } |
Секция параметра типа, заключённая в угловые скобки <> , следует за именем класса. Она определяет параметры типа (parameter types, также называются переменными типа type variables) T1, T2 … Tn.
Чтобы класс Lair использовал обобщение, нужно сделать объявление обобщённого класса путём замены class Lair на class Lair<T> , что создаст переменную типа T , которую можно использовать в любом месте класса Lair.
С этими изменениями код класса Lair станет таким:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Логово class Lair<T> { // Житель T inhabitant; public void setInhabitant(T inhabitant) { this.inhabitant = inhabitant; } public T getInhabitant() { return this.inhabitant; } } |
Как вы видите все вхождения Object заменены на T . Переменная типа может быть любым типом, кроме примитивных типов: любой класс, интерфейс или другая переменная типа.
Соглашение об именовании переменных типа
По соглашению переменные типа именуются одной буквой в верхнем регистре. Это сильно отличается от соглашения об именовании переменных, классов и интерфейсов. Без такого отличия было бы трудно отличить переменную типа от класса и интерфейса.
Наиболее часто используемые имена для параметров типа:
- E — элемент (Element, обширно используется Java Collections Framework)
- K — Ключ
- N — Число
- T — Тип
- V — Значение
- S, U, V и т. п. — 2-й, 3-й, 4-й типы
Создание экземпляра обобщённого типа и обращение к нему
При обращении к обобщённому типу нужно заменить параметры типа на конкретные классы или интерфейсы, например Goblin :
1 |
Lair<Goblin> goblinLair; |
Вы можете думать, что обращение к параметризованному типу похоже на обычный вызов метода, но вместо передачи аргумента в метод вы передаёте аргумент типа (type argument), например Goblin в класс Lair в данном случае.
Параметр типа и аргумент типа — это два разных понятия. Когда вы объявляете обобщённый тип Lair<T> , то здесь T является параметром типа. Когда вы обращаетесь к обобщённому типу, вы передаёте аргумент типа, например Goblin . Это довольно похоже на различие формальных параметрах и аргументов методов.
Как и любое другое объявление переменной Lair<Goblin> goblinLair НЕ создаёт экземпляр класса Lair . Такой код просто объявляет переменную goblinLair как Lair Goblin -ов.
Обращение к обобщённому типу обычно называется параметризованным типом (parameterized type).
Чтобы создать экземпляр класса, используйте ключевое слово new , как обычно, и в дополнение укажите <Goblin> между именем класса и скобками с параметрами конструктора:
1 |
Lair<Goblin> goblinLair = new Lair<Goblin>(); |
После создания экземпляра можно обращаться к методам:
1 2 3 4 |
// указываем жителя. goblinLair.setInhabitant(new Goblin()); // Приведение типа уже не нужно. Goblin goblin = goblinLair.getInhabitant(); |
Бриллиантовая операция (Diamond operator)
Начиная с Java 7 существует также бриллиантовая операция (diamond operator), которая позволяет указывать пустые аргументы типа <> там, где компилятор может вывести тип из контекста:
1 |
Lair<Goblin> goblinLair = new Lair<>(); |
Несколько параметров типа
Обобщённый тип может иметь несколько параметров типа:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class PairLair<T, S> { T inhabitant1; S inhabitant2; public void setInhabitant1(T inhabitant1) { this.inhabitant1 = inhabitant1; } public T getInhabitant1() { return this.inhabitant1; } public void setInhabitant2(S inhabitant2) { this.inhabitant2 = inhabitant2; } public S getInhabitant2() { return this.inhabitant2; } } class Goblin { } class Genie { } class Main { public static void main(String[] args) { PairLair<Goblin, Genie> goblinGenieLair = new PairLair<>(); goblinGenieLair.setInhabitant1(new Goblin()); goblinGenieLair.setInhabitant2(new Genie()); Goblin goblin = goblinGenieLair.getInhabitant1(); Genie genie = goblinGenieLair.getInhabitant2(); } } |
Параметризованный тип
Вы можете также заменить параметр типа на параметризованный тип:
1 |
PairLair<Goblin, Lair<Genie>> hierarchicalLair = new PairLair<>(); |
Обобщённые интерфейсы объявляются схожим с обобщёнными классами способом.
Сырой тип (Raw type)
Сырой тип (raw type) — это имя обобщённого класса или интерфейса без аргументов типа (type arguments).
Например, параметризованный тип создаётся так:
1 |
Lair<Goblin> goblinLair = new Lair<>(); |
Если убрать аргументы типа, то будет создан сырой тип:
1 |
Lair lair = new Lair(); |
Поэтому Lair — это сырой тип обобщённого типа Lair<T> . Однако необобщённый класс или интерфейс НЕ являются сырыми типами.
Вы можете часто увидеть использование сырых типов в старом коде, поскольку многие классы, например коллекции) до Java 5 были необобщёнными. Когда вы используете сырые типы, вы по сути получаете то же самое поведение, которое было до введения обобщений в Java.
Для совместимости со старым кодом допустимо присваивать параметризованный тип своему собственному сырому типу:
1 2 |
Lair<Goblin> goblinLair = new Lair<>(); Lair lair = goblinLair; // OK |
Но если вы попытаетесь присвоить параметризованному типу сырой тип, то будет предупреждение (warning):
1 2 |
Lair lair = new Lair(); Lair<Goblin> goblinLair = lair; // warning |
Вы также получите предупреждение (warning), если попытаетесь вызвать обобщённый метод в сыром типе:
1 2 3 |
Lair<Goblin> goblinLair = new Lair<>(); Lair lair = goblinLair; lair.setInhabitant(new Goblin()); // warning |
Предупреждение показывает, что сырой тип обходит проверку обобщённого типа, что откладывает обнаружение ошибки на выполнение программы.
Сообщения об ошибках “unchecked”
Как упоминалось выше, при использовании сырого типа вы можете столкнуться с предупреждениями вида:
1 2 |
Note: Main.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. |
Термин “unchecked” означает непроверенные, то есть компилятор не имеет достаточного количества информации для обеспечения безопасности типов. По умолчанию этот вид предупреждений выключен, поэтому компилятор даёт подсказку. Чтобы видеть все “unchecked” предупреждения нужно перекомпилировать код с опцией -Xlint:unchecked :
1 |
javac -Xlint:unchecked Main.java |
Предупреждения будут такого вида:
1 2 3 4 5 6 |
Main.java:48 warning: [unchecked] unchecked call to setInhabitant(T) as member of the raw type Lair lair.setInhabitant(new Goblin()); //warning ^ where T is type-variable: T extends Object declared in class Lair 1 warning |
Чтобы полностью отключить подобные предупреждения нужно перекомпилировать с опцией -Xlint:-unchecked . Можно также использовать аннотацию @SuppressWarnings("unchecked") , чтобы отключить эти предупреждения для поля, метода, параметра, конструктора или локальной переменной.
Обобщённые методы
Обобщённые методы похожи на обобщённые классы, но параметры типа относятся к методу, а не к классу. Допустимо делать обобщёнными статические и нестатические методы, а также конструкторы.
Синтаксис обобщённого метода включает параметры типа внутри угловых скобок, которые указываются перед возвращаемым типом.
1 2 3 4 5 6 7 8 |
class Utils { // Обобщённый метод. static <T> void setIfNull(Lair<T> lair, T t) { if (lair.getInhabitant() == null) { lair.setInhabitant(t); } } } |
Пример вызова обобщённого метода:
1 2 |
Lair<Goblin> goblinsLair = new Lair<>(); Utils.<Goblin>setIfNull(goblinsLair, new Goblin()); |
Здесь тип Goblin указан явно, но обычно он может быть опущен, и компилятор выведет тип из контекста:
1 2 |
Lair<Goblin> goblinsLair = new Lair<>(); Utils.setIfNull(goblinsLair, new Goblin()); |
Более подробно выведение типа будет описано ниже.
Ограниченные параметры типа
В некоторых случаях имеет смысл ограничить типы, которые можно использовать в качестве аргументов в параметризованных типах. Например, в жилище Lair могут жить только наследники класса Monster . Подобное ограничение можно сделать с помощью ограниченного параметра типа (bounded type parameters).
Чтобы объявить ограниченный параметр типа нужно после имени параметра указать ключевое слово extends, а затем указать верхнюю границу (upper bound), которой в данном примере является класс Monster. В этом контексте extends означает как extends , так и implements.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// У параметра типа указываем верхнюю границу Monster. class Lair<T extends Monster> { T inhabitant; public void setInhabitant(T inhabitant) { this.inhabitant = inhabitant; } public T getInhabitant() { return this.inhabitant; } public void tick() { if (inhabitant != null) { // Можно вызывать методы // интерфейса или класса, // указанного в качестве верхней // границы параметра типа. inhabitant.doSomething(); } } } class Monster { public void doSomething() { System.out.println("Doing something."); } } class Goblin extends Monster { } class Main { public static void main(String[] args) { Lair<Goblin> goblinLair = new Lair<>(); goblinLair.setInhabitant(new Goblin()); goblinLair.tick(); } } |
В этом примере мы ограничили возможные типы, которые можно использовать в параметризованных классах Lair, наследниками класса Monster. Если попытаться указать, например, Lair<Integer> , то возникнет ошибка компиляции. Плюс в дополнение мы получили возможность вызывать в обобщённом классе методы класса Monster( inhabitant.doSomething(); ).
Можно указать несколько верхних границ, перечисляя их через символ «&», но при этом только один класс может быть указан в списке верхних границ, и он должен стоять первым:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Monster {} interface Enemy {} interface Dreadful {} // Указываем несколько верхних границ. // Если в списке верхних границ есть класс, то // он обязательно должен идти первым. class Lair<T extends Monster & Enemy & Dreadful> { } |
Аналогичным образом можно создавать обобщённые методы с ограничением:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class Monster { boolean isSelect() { return true; // some selection logic } } interface Enemy {} interface Dreadful {} class Utils { public static<T extends Monster & Enemy & Dreadful> T selectMonster(T[] monsters) { T result = null; for (T obj : monsters) { result = obj.isSelect() ? obj : null; // some selection logic } return result; } } class Goblin extends Monster implements Enemy, Dreadful { } class Main { public static void main(String[] args) { Goblin[] goblins = new Goblin[10]; for (int n = 0; n < goblins.length; n++) { goblins[n] = new Goblin(); } Goblin selectedMonster = Utils.selectMonster(goblins); } } |
Обобщения, наследование и дочерние типы
Как вы уже знаете, можно присвоить объекту одного типа объект другого типа, если эти типы совместимы. Например, вы можете присвоить объект типа Integer переменной типа Object , так как Object является одним из супертипов Integer -а:
1 2 3 |
Object someObject = new Object(); Integer someInteger = new Integer(10); someObject = someInteger; // OK |
В объектно-ориентированной терминологии это называется связью «является» (“is a”). Так как Integer является Object -ом, то такое присвоение разрешено. Но Integer также является и Number , поэтому следующий код тоже корректен:
1 2 3 4 |
public void someMethod(Number n) { /* ... */ } someMethod(new Integer(10)); // OK someMethod(new Double(10.1)); // OK |
Это также верно для обобщений. Вы можете осуществить вызов обобщённого типа, передав Number в качестве аргумента типа, и любой дальнейший вызов будет разрешён, если аргумент совместим с Number :
1 2 3 |
Box<Number> box = new Box<Number>(); box.add(new Integer(10)); // OK box.add(new Double(10.1)); // OK |
Теперь рассмотрим метод:
1 |
public void boxTest(Box<Number> n) { /* ... */ } |
Какой тип аргумента он будет принимать? Если посмотрите на сигнатуру, то вы можете увидеть, что он принимает один аргумент с типом Box<Number>. Но что это означает? Можете ли вы передать Box<Integer> или Box<Double> , как вы могли бы ожидать? Нет, не можете, так как Box<Integer> и Box<Double> не являются потомками Box<Number>.
Это частое недопонимание принципов работы обобщений, и это важно знать.
Запомните: Для двух типов A и B (например, Number и Integer ), MyClass<A> не имеет никакой связи или родства с MyClass<B> , независимо от того, как A и B связаны между собой. Общий родитель MyClass<A> и MyClass<B> — это Object.
Информация о способах создания связи подобной потомок-родитель между двумя обобщёнными типами, когда параметры типа связаны, смотрите раздел «Подстановочные символы и дочерние типы».
Вы можете указать обобщённый класс или интерфейс в качестве родительского для своего класса или интерфейса. Связь между параметрами типа одного класса или интерфейса и другого определяются ключевыми словами extends и implements .
Для классов коллекций, например, ArrayList<E> implements List<E> , и List<E> extends Collection<E> . Также ArrayList<String> является дочерним типом для List<String> , который является дочерним типом Collection<String> . Наследование между типами сохраняется, пока вы не меняете аргумент типа.
Давайте представим, что мы хотим определить свой собственный интерфейс списка PayloadList , который связывает необязательное значение P с каждым элементом. Объявление может выглядеть так:
1 2 3 4 |
interface PayloadList<E,P> extends List<E> { void setPayload(int index, P val); ... } |
Следующие параметризованные типы являются дочерними типами для List<String> , но не связаны между собой:
- PayloadList<String,String>
- PayloadList<String,Integer>
- PayloadList<String,Exception>
Выведение типов
Выведение типов — это возможность компилятора Java автоматически определять аргументы типа на основе контекста, чтобы вызов получился возможным. Алгоритм выведения типов определяет типы аргументов и, если есть, тип, в который присваивается результат или в котором возвращается результат. Далее алгоритм пытается найти наиболее конкретный тип, который работает со всеми аргументами.
В приведённом ниже примере выведение типов определяет, что второй аргумент, передаваемый в метод pick имеет тип Serializable :
1 2 |
static <T> T pick(T a1, T a2) { return a2; } Serializable s = pick("d", new ArrayList<String>()); |
Выведение типов и обобщённый методы
В описании обобщённых методов уже рассказывалось о выведении типов, которое делает возможным вызов обобщённого метода так, будто это обычный метод, без указания типа в угловых скобках <>. Рассмотрим этот пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
class Lair<T> { private T inhabitant; public void setInhabitant(T t) { this.inhabitant = t; } public T getInhabitant() { return this.inhabitant; } } class Goblin { private String name; public Goblin(String name) { this.name = name; } @Override public String toString() { return this.name; } } class LairDemo { public static <U> void addLair(U u, java.util.List<Lair<U>> lairs) { Lair<U> lair = new Lair<>(); lair.setInhabitant(u); lairs.add(lair); } public static <U> void outputLairs(java.util.List<Lair<U>> lairs) { int counter = 0; for (Lair<U> lair : lairs) { U lairInhabitant = lair.getInhabitant(); System.out.println("Lair #" + counter + " contains [" + lairInhabitant.toString() + "]" ); counter++; } } public static void main(String[] args) { java.util.ArrayList<Lair<Goblin>> listOfGoblinsLairs = new java.util.ArrayList<>(); LairDemo.<Goblin>addLair(new Goblin("Michael"), listOfGoblinsLairs); LairDemo.addLair(new Goblin("Rafael"), listOfGoblinsLairs); LairDemo.addLair(new Goblin("Pushkin"), listOfGoblinsLairs); LairDemo.outputLairs(listOfGoblinsLairs); } } |
Этот код выведет в консоль следующее:
1 2 3 |
Lair #0 contains [Michael] Lair #1 contains [Rafael] Lair #2 contains [Pushkin] |
Обобщённый метод addLair объявляет один параметр типа U . В большинстве случаев компилятор Java может вывести параметры типа вызова обобщённого метода, в результате вам чаще всего вовсе не обязательно их указывать. Например, чтобы вызвать обобщённый метод addBox , вы можете указать параметры типа так:
1 |
LairDemo.<Goblin>addLair(new Goblin("Michael"), listOfGoblinsLairs); |
Либо вы можете опустить их, и тогда компилятор Java автоматически выведет тип Goblin из аргументов метода:
1 |
LairDemo.addLair(new Goblin("Rafael"), listOfGoblinsLairs); |
Выведение типов и создание экземпляра обобщённого класса
Вы можете заменить аргументы типа, необходимые для вызова конструктора обобщённого класса пустым множеством параметров типа ( <> ), так как компилятор может вывести аргументы типа из контекста. Эта пара угловых скобок называется бриллиантовой операцией (diamond operator).
Рассмотрим следующее объявление переменной:
1 |
Map<String, List<String>> myMap = new HashMap<String, List<String>>(); |
Вы можете заменить параметризованный тип конструктора пустыми угловыми скобками ( <> ):
1 |
Map<String, List<String>> myMap = new HashMap<>(); |
Обратите внимание, что для того чтобы воспользоваться выведением типов при создании экземпляра обобщённого класса, вы должны использовать бриллиантовую операцию (diamond operator). В примере ниже компилятор сгенерирует предупреждение unchecked conversion warning, так как конструктор HashMap() обращается к сырому типу HashMap, а не к Map<String, List<String>> :
1 |
Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning |
Выведение типа и обобщённые конструкторы обобщённых и необобщённых классов
Конструкторы могут быть обобщёнными как в обобщённых, так и в необобщённых классах. Рассмотрим пример:
1 2 3 4 5 |
class MyClass<X> { <T> MyClass(T t) { // ... } } |
Рассмотрим создание экземпляра класса MyClass :
1 |
new MyClass<Integer>("") |
Эта инструкция создаёт экземпляр параметризованного типа MyClass<Integer> . Инструкция явно указывает Integer в качестве формального параметра типа X обобщённого класса MyClass<X> . Обратите внимание, что конструктор этого обобщённого класса содержит параметр типа T . Компилятор выводит тип String для этого формального параметра T , так как фактически переданный аргумент является экземпляром класса String.
Компилятор Java 7 и более поздней версии может вывести аргументы типа создаваемого экземпляра обобщённого класса с помощью бриллиантовой операции (diamong operator). Пример:
1 |
MyClass<Integer> myObject = new MyClass<>(""); |
В этом примере компилятор выводит Integer для параметра типа X обобщённого класса MyClass<X> . Он выводит тип String для параметра T обобщённого конструктора обобщённого класса.
Важно запомнить, что алгоритм выведения типа использует только аргументы вызова, целевые типы и возможно очевидный ожидаемый возвращаемый тип для выведения типов. Алгоритм выведения не использует последующий код программы.
Целевые типы
Компилятор Java пользуется целевыми типами для вывода параметров типа вызова обобщённого метода. Целевой тип выражения — это тип данных, который компилятор Java ожидает в зависимости от того, в каком месте находится выражение. Рассмотрим метод Collections.emptyList() , который объявлен так:
1 |
static <T> List<T> emptyList(); |
Рассмотрим следующую инструкцию присвоения:
1 |
List<String> listOne = Collections.emptyList(); |
Эта инструкция ожидает экземпляр List<String> . Этот тип данных является целевым типом. Поскольку метод emptyList возвращает значение типа List<T> , компилятор выводит, что аргумент типа T будет типом String . Это работает как в Java 7, так и в Java 8. Вы также можете указывать аргумент типа напрямую:
1 |
List<String> listOne = Collections.<String>emptyList(); |
Но в данном случае в этом нет необходимости. Это может быть необходимо в других случаях. Рассмотрим метод:
1 2 3 |
void processStringList(List<String> stringList) { // process stringList } |
Представьте, что вы хотите вызвать метод processStringList с пустым списком. В Java 7 следующий код не будет работать:
1 |
processStringList(Collections.emptyList()); |
Компилятор Java 7 сгенерирует примерно такую ошибку:
1 |
List<Object> cannot be converted to List<String> |
Компилятору необходимо значение аргумента типа для T , и он начинает с Object . В результате вызов Collections.emptyList возвращает тип List<Object> , который несовместим с методом processStringList . Таким образом в Java 7 вы должны указать аргумент типа так:
1 |
processStringList(Collections.<String>emptyList()); |
В Java 8 в этом больше нет необходимости. Термин целевой тип расширен и включает аргументы методов. В этом случае processStringList -у необходим аргумент типа List<String> Метод Collections.emptyList возвращает значение типа List<T> , компилятор выводит аргумент типа T как String , используя целевой тип List<String> . Таким образом в Java 8 следующая инструкция успешно скомпилируется:
1 |
processStringList(Collections.emptyList()); |
Подстановочный символ (wildcard)
В обобщённом коде знак вопроса (?), называемый подстановочным символом, означает неизвестный тип. Подстановочный символ может использоваться в разных ситуациях: как параметр типа, поля, локальной переменной, иногда в качестве возвращаемого типа. Подстановочный символ никогда не используется в качестве аргумента типа для вызова обобщённого метода, создания экземпляра обобщённого класса или супертипа.
Подстановочный символ, ограниченный сверху (Upper bounded wildcard)
Вы може использовать подстановочный символ, ограниченный сверху, чтобы ослабить ограничения переменной. Например, если вы хотите написать метод, который работает с List<Integer> , List<Double> и List<Number> , вы можете достичь этого с помощью ограниченного сверху подстановочного символа.
Чтобы объявить ограниченный сверху подстановочный символ, используйте символ вопроса «?», с последующим клчевым словом extends , с последующим ограничением сверху. Запомните, что в этом контексте extends означает как расширение класса, так и реализацию интерфейса.
Чтобы написать метод, который работает со списками Number и дочерними типами от Number , например Integer , Double и Float , вы можете указать List<? extends Number> . List<Number> вводит более жёсткое ограничение, чем List<? extends Number> , потому что оно соответствует только списку типа Number , а List<? extends Number> соответствует списку типа Number и спискам всех его подклассов.
Рассмотрим следующий метод process :
1 |
public static void process(List<? extends Monster> list) { /* ... */ } |
Ограниченный сверху подстановочный символ <? extends Monster> , где Monster — любой тип, соответствует Monster и любому подтипу Monster . Метод process может обращаться к элементу списка как к типу Monster :
1 2 3 4 5 |
public static void process(List<? extends Monster> list) { for (Monster elem : list) { // ... } } |
Любой метод класса или интерфейса Monster может быть использован у elem .
Метод sumOfList возвращает сумму чисел в списке:
1 2 3 4 5 6 |
public static double sumOfList(List<? extends Number> list) { double s = 0.0; for (Number n : list) s += n.doubleValue(); return s; } |
Следующий код использует список Integer -ов и выведет: «sum = 6.0»:
1 2 |
List<Integer> li = Arrays.asList(1, 2, 3); System.out.println("sum = " + sumOfList(li)); |
Список Double -ов может использовать тот же самый метод. Следующий код выведет: «sum = 7.0»:
1 2 |
List<Double> ld = Arrays.asList(1.2, 2.3, 3.5); System.out.println("sum = " + sumOfList(ld)); |
Неограниченный подстановочный символ (Unbounded wildcard)
Если просто использовать подстановочный символ, то получится подстановочный символ без ограничений. List<?> Означает список неизвестного типа.
Неограниченный подстановочный символ полезен в двух случаях:
- Если вы пишете метод, который может быть реализован с помощью функциональности класса Object.
- Когда код использует методы обобщённого класса, которые не зависят от параметра типа. Например, List.size() или List.clear() . В реальности Class<?> используется так часто, потому что большинство методов Class<T> не зависят от T.
Рассмотрите следующий метод printList :
1 2 3 4 5 |
public static void printList(List<Object> list) { for (Object elem : list) System.out.println(elem + " "); System.out.println(); } |
Цель метода printList — вывод в консоль списка любого типа, но сейчас он её не выполняет, так как он может вывести в консоль только список объектов типа Object . Он не может принимать в качестве параметра List<Integer> , List<String> , List<Double> и какие-либо ещё, так как они не являются дочерними типами для List<Object> . Чтобы сделать этот метод более общим, нужно использовать List<?> :
1 2 3 4 5 |
public static void printList(List<?> list) { for (Object elem: list) System.out.print(elem + " "); System.out.println(); } |
List<A> является дочерним типом для List<?> для любого конкретного типа A , поэтому вы можете использовать printList для вывода в консоль списков любого типа:
1 2 3 4 |
List<Integer> li = Arrays.asList(1, 2, 3); List<String> ls = Arrays.asList("one", "two", "three"); printList(li); printList(ls); |
Заметка: Метод Arrays.asList конвертирует массив и возвращает список фиксированного размера.
Важно запомнить, что List<Object> и List<?> — это НЕ одно и то же. Вы можете вставить Object или любой дочерний тип от Object в List<Object> . Но вы можете вставить только null в List<?> . Пункт «Руководство по использованию подстановочного символа» содержит информацию о выборе типа подстановочного символа, который следует использовать в конкретной ситуации.
Ограниченный снизу подстановочный символ (Lower bound Wildcard)
Ограниченный снизу подстановочный символ ограничивает неизвестный тип тип так, чтобы он был либо указанным типом, либо одним из его предков.
Вы можете указать либо только верхнюю границу для подстановочного символа, либо только нижнюю, но вы не можете указать оба ограничения сразу.
Допустим, что вы хотите написать метод, который добавляет объекты Integer в список. Чтобы максимизировать гибкость, вы можете захотеть, чтобы метод работал с List<Integer> , List<Number> и List<Object> — всё, что может хранить экземпляры класса Integer .
Чтобы написать метод, который работает со списком Integer-ов и супертипами Integer-а (такими как Integer , Number и Object ), вы можете указать List<? super Integer>. Вариант List<Integer> более ограничен, чем List<? super Integer> , потому что он позволяет использовать только список объектов типа Integer , тогда как List<? super Integer> соответствует спискам любого родительского класса от Integer и списку Integer-ов.
Следующий код добавляет числа от 1 до 10 в конец списка:
1 2 3 4 5 |
public static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 10; i++) { list.add(i); } } |
Пункт «Руководство по использованию подстановочного символа» содержит советы, когда использовать ограниченный сверху подстановочный символ, а когда использовать ограниченный снизу подстановочный символ.
Подстановочные символы и дочерние типы
Как было описано в пункте «Обобщения, наследование и дочерние типы», обобщённые классы или интерфейсы связаны не только из-за связи между их типами. Однако вы можете использовать подстановочные символы (wildcards) для создания связи между обобщёнными классами и интерфейсами.
С данными обычными (необобщёнными) классами:
1 2 |
class Monster { /* ... */ } class Goblin extends Monster { /* ... */ } |
Имеет смысл написать вот такой код:
1 2 |
Goblin goblin = new Goblin(); Monster monster = goblin; |
Этот пример показывает, что наследование следует правилу подчинённых типов: класс Goblin является подклассом класса Monster, если он расширяет его. Это правило НЕ работает для обобщённых типов:
1 2 |
List<Goblin> listGoblins = new ArrayList<>(); List<Monster> listMonsters = listGoblins; // compile-time error |
Если Integer является дочерним типом для Number , то какая связь между List<Integer> и List<Number> ?
Не смотря на то что Integer является подтипом Number , List<Integer> не является подтипом List<Number>. Это разные типы. Общим предком для List<Number> и List<Integer> является List<?>.
Для того чтобы создать такую связь между этими классами, чтобы код мог иметь доступ к методам Number через элементы List<Integer> , используйте подстановочный символ:
1 2 |
List<? extends Integer> intList = new ArrayList<>(); List<? extends Number> numList = intList; // OK. List<? extends Integer> дочерний тип от List<? extends Number> |
Так как Integer является дочерним типом от Number , и numList является списком объектов типа Number , теперь существует связь между intList (список объектов типа Integer ) и numList . Следующая диаграмма показывает связь между несколькими классами List , объявленными с ограниченными сверху подстановочными символами и ограниченными снизу подстановочными символами.
Пункт «Руководство по использованию подстановочного символа» содержит больше информации об использовании ограниченных сверху подстановочны символов и ограниченных снизу подстановочных символов.
Захват символа подстановки (Wildcard Capture) и вспомогательные методы
В некоторых случаях компилятор может вывести тип подстановочного символа. Список может быть определён как List<?>, но при вычислении выражения компилятор выведет конкретный тип из кода. Этот сценарий называется захватом подстановочного символа.
В большинстве случаев вам нет нужды беспокоиться о захвате подстановочного символа, кроме случаев, когда вы видите фразу “capture of” в сообщении об ошибке.
Следующий код выводит сообщение об ошибке, связанное с захватом подстановочного символа, при компиляции:
1 2 3 4 5 6 7 8 |
import java.util.List; public class WildcardError { void foo(List<?> i) { i.set(0, i.get(0)); } } |
В этом примере компилятор обрабатываем параметр i как тип Object . Когда метод foo вызывает List.set(int, E) , компилятор не может подтвердить тип объекта, который будет вставляться в список, и генерирует ошибку. Когда возникает этот тип ошибки, это обычно означает, что компилятор верит, что вы присваиваете неправильный тип переменной. Обобщения были добавлены в Java именно для этого — чтобы усилить безопасность типов во время компиляции.
При компиляции в JDK 8 кода, приведённого выше, компилятор выдаст следующее сообщение об ошибке:
1 2 3 4 5 6 7 |
WildcardError.java:6: error: incompatible types: Object cannot be converted to CAP#1 i.set(0, i.get(0)); ^ where CAP#1 is a fresh type-variable: CAP#1 extends Object from capture of ? Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output 1 error |
В нашем же примере код пытается выполнить безопасную операцию, тогда как мы можем обойти ошибку компиляции? Вы можете исправить её написав приватный вспомогательный метод (private helper method), который захватывает подстановочный символ. В этом случае вы можете обойти проблему с помощь создания приватного вспомогательного метода fooHelper() :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class WildcardFixed { void foo(List<?> i) { fooHelper(i); } // Вспомогательный метод создан так, чтобы // подстановочный символ мог быть захвачен. // через выведение типа. private <T> void fooHelper(List<T> l) { l.set(0, l.get(0)); } } |
Благодаря вспомогательному методу компилятор использует выведение типа для определения, что T является CAP#1 ( захваченная переменная в вызове). Пример теперь успешно компилируется.
По соглашению вспомогательные методы обычно называются как originalMethodNameHelper .
Теперь рассмотрите более сложный пример, WildcardErrorBad :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import java.util.List; public class WildcardErrorBad { void swapFirst(List<? extends Number> l1, List<? extends Number> l2) { Number temp = l1.get(0); l1.set(0, l2.get(0)); // ожидается CAP#1 extends Number, // получаем CAP#2 extends Number; // одинаковые ограничения, но разные типы l2.set(0, temp); // ожидается CAP#1 extends Number, // получаем Number } } |
В этом примере код пытается выполнить небезопасную операцию. Например, рассмотрите следующий вызов метода swapFirst :
1 2 3 |
List<Integer> li = Arrays.asList(1, 2, 3); List<Double> ld = Arrays.asList(10.10, 20.20, 30.30); swapFirst(li, ld); |
Не смотря на то что List<Integer> и List<Double> оба удовлетворяют критерию List<? extends Number> , это совершенно неверно брать элемент из списка объектов типа Integer и пытаться добавить его в список объектов типа Double .
При компиляции в Oracle JDK 8 компилятор выведет следующее сообщение об ошибке:
1 2 3 4 5 6 7 8 9 10 11 12 |
WildcardErrorBad.java:7: error: incompatible types: Number cannot be converted to CAP#1 l1.set(0, l2.get(0)); // expected a CAP#1 extends Number, ^ where CAP#1 is a fresh type-variable: CAP#1 extends Number from capture of ? extends Number WildcardErrorBad.java:10: error: incompatible types: Number cannot be converted to CAP#1 l2.set(0, temp); // expected a CAP#1 extends Number, ^ where CAP#1 is a fresh type-variable: CAP#1 extends Number from capture of ? extends Number Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output 2 errors |
В этом случае обойти проблему уже не удастся, так как код в принципе неверен.
Руководство по использованию подстановочного символа
Когда использовать ограниченный сверху подстановочный символ (wildcard), и когда использовать ограниченный снизу подстановочный символ, определить зачастую бывает довольно сложно. Здесь собраны советы по выбору необходимого ограничения для подстановочного символа.
В этом обсуждении будет полезно думать о переменных, будто они представляют две функции:
- Входная переменная. Предоставляет данные для кода. Для метода copy(src, dst) параметр src предоставляет данные для копирования, поэтому он считается входной переменной.
- Выходная переменная. Содержит данные для использования в другом месте. В примере с copy(src, dst) параметр dst принимает данные и является выходной переменной.
Некоторые переменную могут быть одновременно входными и выходными, такой случай тоже здесь рассматривается.
Руководство:
- Входная переменная определяется с ограниченным сверху подстановочным символом, используя ключевое слово extends.
- Выходная переменная определяется с ограниченным снизу подстановочным символом, используя ключевое слово super.
- Если к входной переменной можно обращаться только используя методы класса Object, используйте неограниченный подстановочный символ.
- Если переменная должна использоваться как входная и как выходная одновременно, то НЕ используйте подстановочный символ.
Это руководство не охватывает использование подстановочных символов в возвращаемых из методов типах. Не используйте подстановочные символы в возвращаемых типах, потому что это будет принуждать других программистов разбираться с подстановочными символами.
Список, объявленный как List<? extends ...> может неформально считаться как только для чтения, но это не строгое ограничение. Предположим, что вы имеете следующие два класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class NaturalNumber { private int i; public NaturalNumber(int i) { this.i = i; } // ... } class EvenNumber extends NaturalNumber { public EvenNumber(int i) { super(i); } // ... } |
Рассмотрите следующий код:
1 2 3 |
List<EvenNumber> le = new ArrayList<>(); List<? extends NaturalNumber> ln = le; ln.add(new NaturalNumber(35)); // compile-time error |
Так как List<EvenNumber> является дочерним типом от List<? extends NaturalNumber> , то вы можете присвоить le переменной ln . Но вы не можете использовать ln для добавления NaturalNumber в список объектов EvenNumber . Над списком возможны следующие операции:
- Вы можете добавить в список null .
- Вы можете вызвать clear.
- Вы можете получить iterator и вызвать remove.
- Вы можете захватить подстановочный символ и записывать элементы, которые вы прочитали из списка.
Вы можете увидеть, что список List<? extends NaturalNumber> НЕ является списком только для чтения, но вы можете считать его таким, потому что вы не можете добавить туда новый элемент или заменить существующий элемент в списке.
Стирание типа (Type Erasure)
Обобщения были введены в язык программирования Java для обеспечения более жёсткого контроля типов во время компиляции и для поддержки обобщённого программирования. Для реализации обобщения компилятор Java применяет стирание типа (type erasure) к:
- Заменяет все параметры типа в обобщённых типах их границами или Object-ами, если параметры типа не ограничены. Сгенерированный байткод содержит только обычные классы, интерфейсы и методы.
- Вставляет приведение типов где необходимо, чтобы сохранить безопасность типа.
- Генерирует связующие методы, чтобы сохранить полиморфизм в расширенных (extended, наследующиеся от других) обобщённых типах.
Стирание типа обеспечивает, что никакие новые классы не создаются для параметризованных типов, следовательно обобщения не приводят к накладным расходам во время выполнения.
Стирание типа в обобщённых типах
Во время процесса стирания типов компилятор Java стирает все параметры типа и заменяет каждый его ограничением, если параметр типа ограничен, либо Object -ом, если параметр типа неограничен.
Рассмотрим следующий обобщённый класс, который представляет узел односвязного списка:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Node<T> { private T data; private Node<T> next; public Node(T data, Node<T> next) } this.data = data; this.next = next; } public T getData() { return data; } // ... } |
Так как параметр T неограничен, то компилятор заменяет его Object -ом:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Node { private Object data; private Node next; public Node(Object data, Node next) { this.data = data; this.next = next; } public Object getData() { return data; } // ... } |
В следующем примере обобщённый класс Node использует ограниченный параметр:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Node<T extends Comparable<T>> { private T data; private Node<T> next; public Node(T data, Node<T> next) { this.data = data; this.next = next; } public T getData() { return data; } // ... } |
Компилятор Java заменяет ограниченный параметр T первой границей Comparable :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Node { private Comparable data; private Node next; public Node(Comparable data, Node next) { this.data = data; this.next = next; } public Comparable getData() { return data; } // ... } |
Стирание типа в обобщённых методах
Компилятор Java также стирает параметры типа обобщённых методов. Рассмотрите следующий обобщённый метод:
1 2 3 4 5 6 7 8 9 |
// Подсчитывает количество вхождений элемента в массив. // public static <T> int count(T[] anArray, T elem) { int cnt = 0; for (T e : anArray) if (e.equals(elem)) ++cnt; return cnt; } |
Так как T неограничен, то компилятор Java заменяет его на Object :
1 2 3 4 5 6 7 |
public static int count(Object[] anArray, Object elem) { int cnt = 0; for (Object e : anArray) if (e.equals(elem)) ++cnt; return cnt; } |
Предположим, что объявлены следующие классы:
1 2 3 |
class Monster { /* ... */ } class Goblin extends Monster { /* ... */ } class Viy extends Monster { /* ... */ } |
Вы можете написать метод, рисующий разных монстров:
1 |
public static <T extends Monster> void draw(T monster) { /* ... */ } |
омпилятор Java заменит T на Monster :
1 |
public static void draw(Monster monster) { /* ... */ } |
Влияние стирания типа и методы-мосты (bridge methods)
Иногда стирание типа приводит к ситуации, которую вы не ожидали. Следующие примеры показывают, как это может произойти.
Пусть есть два класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Node<T> { public T data; public Node(T data) { this.data = data; } public void setData(T data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node<Integer> { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } } |
Рассмотрим следующий код:
1 2 3 4 5 |
MyNode mn = new MyNode(5); Node n = mn; // Сырой тип - компилятор генерирует // предупреждение unchecked warning n.setData("Hello"); Integer x = mn.data; // Исключение ClassCastException. |
После стирания типа код станет таким:
1 2 3 4 5 |
MyNode mn = new MyNode(5); Node n = (MyNode)mn; // Сырой тип - компилятор генерирует // предупреждение unchecked warning n.setData("Hello"); Integer x = (String)mn.data; // Исключение ClassCastException. |
Что происходит при выполнении кода:
- n.setData("Hello"); приводит к вызову метода setData(Object) у объекта класса MyNode . (Класс MyNode унаследовал setData(Object) от Node ).
- В теле метода setData(Object) , в поле data объекта присваивается String.
- К полю data того же самого объекта можно обратиться через переменную mn , и будет ожидаться, что оно содержит Integer , так как mn ссылается на экземпляр класса MyNode , который является Node<Integer> .
- Попытка присвоить String к Integer-у приводит к исключению ClassCastException в операции приведения типа, вставляемой компилятором Java.
Методы-мосты (Bridge Methods)
При компиляции класса или интерфейса, который расширяет параметризованный класс или реализует параметризованный интерфейс, компилятор требует создания синтетического метода, называемого методом-мостом (bridge method). Обычно вам не придётся беспокоиться об этих методах, но вы можете быть озадачены, если он появится в трассировке стека:
После стирания типа классы Node и MyNode станут такими:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Node { public Object data; public Node(Object data) { this.data = data; } public void setData(Object data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } } |
После стирания типа сигнатуры методов не совпадают. Метод из класса Node становится setData(Object) , а метод из класса MyNode становится setData(Integer) , поэтому метод setData из класса MyNode не переопределяет метод setData из класса Node .
Чтобы исправить проблему и сохранить полиморфизм обобщённых типов после стирания типа, компилятор Java генерирует методы-мосты, для того чтобы расширение работало как ожидается. Для класса MyNode компилятор генерирует следующий метод-мост setData:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class MyNode extends Node { // Метод-мост, сгенерированный компилятором // public void setData(Object data) { setData((Integer) data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } / |
Как вы можете видеть, метод-мост, которые имеет ту же сигнатуру, что и метод
setData у класса
Node делегирует действие к оригинальному методу
setData .
c
Я так и не смог нормально перевести слово “Non-Reifiable”, но пусть будет переводиться как «нематериализуемые».
В разделе стирания типов обсуждается процесс, где компилятор удаляет информацию, связанную с параметрами типа и аргументами типа. Стирание типа имеет последствия, связанные с произвольным количеством параметров (varargs).
Материализуемые типы (reifiable types) — это типы, информация о которых полностью доступа во время выполнения: примитивы, необобщённые типы, сырые типы, обращения к неограниченным подстановочным символам.
Нематериализуемые типы (Non-reifiable types) — это типы, информация о которых удаляется во время компиляции стиранием типов: обращения к обобщённым типам, которые не объявлены с помощью неограниченных подстановочных символов. Во время выполнения о нематериализуемых типах (Non-reifiable types) нет всей информации. Примеры нематериализуемых типов: List<String> и List<Number> . Виртуальная машина Java не может узнать разницу между ними во время выполнения. В некоторых ситуациях нематериализуемые типы не могут использоваться, например в выражениях instanceof или в качестве элементов массива.
Загрязнение кучи (Heap pollution)
Загрязнение кучи (heap pollution) возникает, когда переменная параметризованного типа ссылается на объект, который не является параметризованным типом. Такая ситуация возникает, если программа выполнила некоторую операцию, которая генерирует предупреждение unchecked warning во время компиляции. Предупреждение unchecked warning генерируется, если правильность операции, в которую вовлечён параметризованный тип (например приведение типа или вызов метода) не может быть проверена. Например, загрязнение кучи возникает при смешивании сырых типов и параметризованных типов, или при осуществлении непроверяемых преобразований типа.
В обычных ситуациях, когда код компилируется в одно и то же время, компилятор генерирует unchecked warning, чтобы привлечь ваше внимание к загрязнению кучи. Если вы компилируете различные части вашего кода отдельно, то становится трудно определить потенциальную угрозу загрязнения кучи. Если вы обеспечите компиляцию кода без предупреждений, то загрязнение кучи (heap pollution) не сможет произойти.
Потенциальные уязвимости методов с произвольным числом параметров с нематериализуемыми формальными параметрами
Обобщённые методы, которые включают произвольное число параметров могут привести к загрязнению кучи.
Рассмотрите следующий класс ArrayBuilder :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class ArrayBuilder { public static <T> void addToList (List<T> listArg, T... elements) { for (T x : elements) { listArg.add(x); } } public static void faultyMethod(List<String>... l) { Object[] objectArray = l; // Valid objectArray[0] = Arrays.asList(42); String s = l[0].get(0); // Здесь генерируется ClassCastException } } |
Следующий пример использует класс ArrayBuilder :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class HeapPollutionExample { public static void main(String[] args) { List<String> stringListA = new ArrayList<String>(); List<String> stringListB = new ArrayList<String>(); ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine"); ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve"); List<List<String>> listOfStringLists = new ArrayList<List<String>>(); ArrayBuilder.addToList(listOfStringLists, stringListA, stringListB); ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!")); } } |
При компиляции возникает следующее предупреждение на определении метода ArrayBuilder.addToList :
1 |
warning: [varargs] Possible heap pollution from parameterized vararg type T |
Когда компилятор сталкивается с методом с произвольным числом параметров (varargs-метод), он преобразует varargs-параметр в массив. Однако Java не запрещает создание массивов с параметризованными типами. В методе ArrayBuilder.addToList компилятор преобразует формальный параметр T... elements в формальный параметр T[] elements . Но из-за стирания типа компилятор конвертирует формальный параметр в Object[] elements , поэтому возникает загрязнение кучи.
Следующая инструкция присваивает varargs-параметр l в массив объектов objectArgs :
1 |
Object[] objectArray = l; |
Эта инструкция потенциально приводит к загрязнению кучи. Значение, которое не соответствует параметризованному типу varargs-параметра l может быть присвоено переменной в objectArray и таким образом может быть присвоено в l . Но компилятор не генерирует unchecked warning в этой инструкции, так как он уже сгенерировал предупреждение, когда преобразовывал varargs-параметр List<String>... l в формальный параметр List[] l . Инструкция корректна, переменная l имеет тип List[] , который является дочерним типом для Object[] .
В результате компилятор не сгенерирует никаких ошибок или предупреждений, если вы присвоите объект List объектов любого типа к элементу массива ObjectArray , как показано здесь:
1 |
objectArray[0] = Arrays.asList(42); |
Эта инструкция присваивает первому элементу массива objectArray список List , содержащий объекты типа Integer .
Предположим, что вы вызываете метод ArrayBuilder.faultyMethod следующим образом:
1 |
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!")); |
Во время выполнения виртуальная машина Java генерирует ClassCastException на следующей инструкции:
1 2 |
// ClassCastException thrown here String s = l[0].get(0); |
Объект, сохранённый в первом элементе массива переменной l , имеет тип List<Integer> , но инструкция ожидает объект типа List<String> .
Подавление предупреждений для методов с произвольным количеством параметров с нематериализуемыми формальными параметрами
Если вы объявили метод с произвольным числом параметров параметризованного типа и обеспечили то, что тело метода не бросает исключение ClassCastException или другое похожее исключение, связанное с неправильной обработкой varargs-параметра, то вы можете отключить предупреждение, которое компилятор генерирует для этих методов при помощи добавления аннотации к статическому методу и методу неконструктору:
1 |
@SafeVarargs |
Аннотация @SafeVarargs относится к документируемой части объявления метода. Эта аннотация говорит, что эта реализация метода корректно обрабатывает varargs-параметр.
Также возможно, но менее желательно, использовать следующую аннотацию для подавления этих предупреждений:
1 |
@SuppressWarnings({"unchecked", "varargs"}) |
Но этот способ не подавляет предупреждения со стороны вызова метода. Смотрите «Java 8 аннотации».
Ограничения обобщений
Нельзя создавать экземпляры обобщённых типов с примитивными типами в качестве аргументов типа.
Рассмотрите следующий класс:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } // ... } |
При создании объекта Pair вы не можете заменять примитивным типом формальные параметры K и V :
1 |
Pair<int, char> p = new Pair<>(8, 'a'); // compile-time error |
Вы можете заменить их только непримитивными типами:
1 |
Pair<Integer, Character> p = new Pair<>(8, 'a'); |
Компилятор использует автоупаковку Integer.valueOf(8) и Character('a') :
Нельзя создавать экземпляры параметров типа
Вы не можете создать экземпляр параметра типа. Например, следующий код приведёт к ошибке компиляции:
1 2 3 4 |
public static <E> void append(List<E> list) { E elem = new E(); // compile-time error list.add(elem); } |
В качестве обходного пути вы можете создать объект параметра типа с помощью отражения (reflection):
1 2 3 4 |
public static <E> void append(List<E> list, Class<E> cls) throws Exception { E elem = cls.newInstance(); // OK list.add(elem); } |
Вы можете вызвать метод append вот так:
1 2 |
List<String> ls = new ArrayList<>(); append(ls, String.class); |
Нельзя объявлять статические поля с типом параметра типа
Статические поля класса являются общими для всех объектов этого класса, поэтому статические поля с типом параметра типа запрещены. Рассмотрите следующий класс:
1 2 3 4 5 |
public class MobileDevice<T> { private static T os; // ... } |
Если бы статические поля с типом параметра типа были бы разрешены, то следующий код сбивал бы с толку:
1 2 3 |
MobileDevice<Smartphone> phone = new MobileDevice<>(); MobileDevice<Pager> pager = new MobileDevice<>(); MobileDevice<TabletPC> pc = new MobileDevice<>(); |
Так как статическое поле os является общим для phone , pager и pc , то какого типа os? Оно не может быть Smartphone , Pager и TabletPC в одно и то же время, поэтому вы не можете создавать статические поля с типом параметра типа.
Нельзя использовать приведения типа или instanceof с параметризованными типами
Так как компилятор Java стирает все параметры типа из обобщённого кода, то вы не можете проверить во время выполнения, какой параметризованный тип используется для обобщённого типа:
1 2 3 4 5 |
public static <E> void rtti(List<E> list) { if (list instanceof ArrayList<Integer>) { // compile-time error // ... } } |
Множество параметризованных типов, которые можно передать в метод rtti :
1 |
S = {ArrayList<Integer>, ArrayList<String>, LinkedList<Character>, ...} |
Во время выполнения нет параметров типа, поэтому мы не можем различить ArrayList<Integer> и ArrayList<String>. Наибольшее, что вы можете сделать — это использовать подстановочный символ для проверки, что список является типом ArrayList :
1 2 3 4 5 |
public static void rtti(List<?> list) { if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type // ... } } |
Обычно вы не можете использовать приведение типа к параметризованному типу, если он не использует неограниченный подстановочный символ. Например:
1 2 |
List<Integer> li = new ArrayList<>(); List<Number> ln = (List<Number>) li; // ошибка компиляции |
Однако в некоторых случаях компилятор знает, что параметр типа всегда верный и позволяет использовать приведение типа. Например:
1 2 |
List<String> l1 = ...; ArrayList<String> l2 = (ArrayList<String>)l1; // OK |
Невозможно создавать массивы параметризованных типов
Вы не можете создавать массивы параметризованных типов. Например, следующий код не будет компилироваться:
1 |
List<Integer>[] arrayOfLists = new List<Integer>[2]; // ошибка компиляции |
Следующий код показывает, что случится при вставке различных типов в массив:
1 2 3 |
Object[] strings = new String[2]; strings[0] = "hi"; // OK strings[1] = 100; // исклчение ArrayStoreException |
Если вы попробуете то же самое с обобщённым списком, то будет такая проблема:
1 2 3 4 5 6 |
Object[] stringLists = new List<String>[]; // ошибка компиляции, но допустим, что это возможно stringLists[0] = new ArrayList<String>(); // OK stringLists[1] = new ArrayList<Integer>(); // должно быть // исключение // ArrayStoreException // но среда выполнения не может его заметить. |
Если бы массивы с параметризованными типами были бы разрешены, то предыдущий код не смог бы бросить исключение ArrayStoreException.
Нельзя создавать, ловить (catch) или бросать (throw) объекты параметризованных типов
Обобщённый класс не может расширять класс Throwable напрямую или ненапрямую. Например. следующие классы не компилируются:
1 2 3 4 5 |
// Расширяет Throwable ненапрямую class MathException<T> extends Exception { /* ... */ } // ошибка компиляции // Расширяет Throwable напрямую class QueueFullException<T> extends Throwable { /* ... */ // ошибка компиляции |
Метод не может ловить (catch) экземпляр параметра типа:
1 2 3 4 5 6 7 8 |
public static <T extends Exception, J> void execute(List<J> jobs) { try { for (J job : jobs) // ... } catch (T e) { // ошибка компиляции // ... } } |
Однако вы можете использовать параметр типа в клаузе throws :
1 2 3 4 5 |
class Parser<T extends Exception> { public void parse(File file) throws T { // OK // ... } } |
Нельзя перегружать метод так, чтобы формальные параметры типа стирались в один и тот же сырой тип
Класс не может иметь два перегруженных метода, которые будут иметь одинаковую сигнатуру после стирания типов:
1 2 3 4 |
public class Example { public void print(Set<String> strSet) { } public void print(Set<Integer> intSet) { } } |
Этот код не будет компилироваться.
Цикл статей «Учебник Java 8».
Следующая статья — «Java 8 исключения».
Предыдущая статья — «Java 8 ещё раз о перегрузке методов».
Я может ошибаюсь, но в теме «Подстановочные символы и дочерние типы» содержится ошибка.
В диаграмме нарисовано, что как будто бы класс Number дочерний от класса Integer:
| List |
^
|
| List |
Так. Часть моего сообщения съело)
Но смысл я понял. В диаграмме действительно была ошибка. В пункте «Подстановочные символы и дочерние типы».
Связь есть между List<? extends Integer> и List<? extends Number>.
А также есть связь между List<? super Number> и List<? super Integer>.
Мы можем присвоить переменной типа List<? extends Number> значение типа List<? extends Integer>, но не наоборот.
И мы можем присвоить переменной типа List<? super Integer> значение List<? super Number>, но не наоборот.
Здесь я имею в виду присваивание без явного приведения типа, разумеется. С явным приведением типа можно присвоить и туда и сюда, и это даже будет работать.
Сейчас диаграмму исправил, спасибо.
Также содержится ошибка в блоках слева (нижнем и среднем) 🙂
Да, со схемой я намудрил что-то. Тут тема такая, воображение нужно сильное. Сейчас исправил. Теперь, надеюсь, правильно:)
В «Обобщённые методы», в примере, в третьей строке
static void setIfNull(Lair lair, T t) {
«void» — лишнее слово, опечатка?
— это и есть возвращаемый тип, так ведь?
Или я что-то не правильно понял?
В том примере void — это что нет результата метода, а то, что перед этим — это для указания, что метод обобщённый.