0 added
0 removed
Original
2026-01-01
Modified
2026-02-21
1
<p><a>#статьи</a></p>
1
<p><a>#статьи</a></p>
2
<ul><li>17 ноя 2021</li>
2
<ul><li>17 ноя 2021</li>
3
<li>0</li>
3
<li>0</li>
4
</ul><p>Рассказываем, как в любой непонятной ситуации правильно сочетать дженерик-типы.</p>
4
</ul><p>Рассказываем, как в любой непонятной ситуации правильно сочетать дженерик-типы.</p>
5
<p>Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.</p>
5
<p>Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.</p>
6
<p>В предыдущей статье "<a>Дженерики для самых маленьких</a>" мы рассказали о том, что такое дженерики (generics), зачем они нужны и как создавать дженерик-типы и методы. Там же говорили про ограничения (boundings) и wildcards. Без этих основ вам будет сложно разобраться с тем, что написано дальше. Поэтому освежите знания, если это необходимо.</p>
6
<p>В предыдущей статье "<a>Дженерики для самых маленьких</a>" мы рассказали о том, что такое дженерики (generics), зачем они нужны и как создавать дженерик-типы и методы. Там же говорили про ограничения (boundings) и wildcards. Без этих основ вам будет сложно разобраться с тем, что написано дальше. Поэтому освежите знания, если это необходимо.</p>
7
<p>Из этой статьи вы узнаете:</p>
7
<p>Из этой статьи вы узнаете:</p>
8
<ul><li><a>почему ни один дженерик не доживает до выполнения программы</a>;</li>
8
<ul><li><a>почему ни один дженерик не доживает до выполнения программы</a>;</li>
9
<li><a>как создать наследника дженерик-класса</a>;</li>
9
<li><a>как создать наследника дженерик-класса</a>;</li>
10
<li><a>что не так с дженерик-типами классов-наследников</a>;</li>
10
<li><a>что не так с дженерик-типами классов-наследников</a>;</li>
11
<li><a>как переопределить метод с дженерик-типами</a>;</li>
11
<li><a>как переопределить метод с дженерик-типами</a>;</li>
12
<li><a>как wildcards с ограничениями "портят" коллекции и зачем нужен принцип PECS</a>.</li>
12
<li><a>как wildcards с ограничениями "портят" коллекции и зачем нужен принцип PECS</a>.</li>
13
</ul><p>Воспользуемся примером из первой части рассказа о дженериках: там был класс Box<T> - коробка для сбора мусора: можно было положить в неё или извлечь из неё только объект определённого типа:</p>
13
</ul><p>Воспользуемся примером из первой части рассказа о дженериках: там был класс Box<T> - коробка для сбора мусора: можно было положить в неё или извлечь из неё только объект определённого типа:</p>
14
class Box<T> { // обозначение типа - T // переменная с типом T private T item; public void putItem(T item) { // параметр метода типа T this.item = item; } public T getItem() { // возвращает объект типа T return item; } }<p>Теперь создадим экземпляр такого класса и подставим вместо T конкретный тип: например, Paper - для коробки, в которую будем собирать бумагу:</p>
14
class Box<T> { // обозначение типа - T // переменная с типом T private T item; public void putItem(T item) { // параметр метода типа T this.item = item; } public T getItem() { // возвращает объект типа T return item; } }<p>Теперь создадим экземпляр такого класса и подставим вместо T конкретный тип: например, Paper - для коробки, в которую будем собирать бумагу:</p>
15
class Paper {} Box<Paper> boxForPaper = new Box<Paper>();<p>Можно предположить, что теперь мы имеем дело с таким классом:</p>
15
class Paper {} Box<Paper> boxForPaper = new Box<Paper>();<p>Можно предположить, что теперь мы имеем дело с таким классом:</p>
16
class Box<Paper> { private Paper item; public void putItem(Paper item) { this.item = item; } public Paper getItem() { return item; } }<p>Эта запись помогает понять, как класс будет работать, но она не имеет ничего общего с тем, во что<em>превращается</em>дженерик-класс или интерфейс в результате компиляции.</p>
16
class Box<Paper> { private Paper item; public void putItem(Paper item) { this.item = item; } public Paper getItem() { return item; } }<p>Эта запись помогает понять, как класс будет работать, но она не имеет ничего общего с тем, во что<em>превращается</em>дженерик-класс или интерфейс в результате компиляции.</p>
17
<p>Компилятор не генерирует class-файл для<em>каждого</em>параметризованного типа. Он создаёт<em>один</em>class-файл для дженерик-типа.</p>
17
<p>Компилятор не генерирует class-файл для<em>каждого</em>параметризованного типа. Он создаёт<em>один</em>class-файл для дженерик-типа.</p>
18
<p>Компилятор стирает информацию о типе, заменяя все параметры без ограничений (<strong>unbounded</strong>) типом Object, а параметры с границами (<strong>bounded</strong>) - на эти границы. Это называется<strong>type erasure</strong>.</p>
18
<p>Компилятор стирает информацию о типе, заменяя все параметры без ограничений (<strong>unbounded</strong>) типом Object, а параметры с границами (<strong>bounded</strong>) - на эти границы. Это называется<strong>type erasure</strong>.</p>
19
<p>Кроме стирания (иногда говорят "затирания") типов, компилятор может добавлять приведение (cast) к нужному типу и создавать переходные bridge-методы, чтобы сохранить полиморфизм в классах-наследниках.</p>
19
<p>Кроме стирания (иногда говорят "затирания") типов, компилятор может добавлять приведение (cast) к нужному типу и создавать переходные bridge-методы, чтобы сохранить полиморфизм в классах-наследниках.</p>
20
<p><strong>Пример 1. Стирание типа для дженерика без границ</strong></p>
20
<p><strong>Пример 1. Стирание типа для дженерика без границ</strong></p>
21
<p>Все параметры типов заменяются на Object. Вот что получится для нашего класса-коробки:</p>
21
<p>Все параметры типов заменяются на Object. Вот что получится для нашего класса-коробки:</p>
22
class Box { private Object item; public void putItem(Object item) { this.item = item; } public Object getItem() { return item; } }<p><strong>Пример 2. Стирание типа для дженерика с границами</strong></p>
22
class Box { private Object item; public void putItem(Object item) { this.item = item; } public Object getItem() { return item; } }<p><strong>Пример 2. Стирание типа для дженерика с границами</strong></p>
23
<p>Объявим дженерик-интерфейс c ограничением сверху (<strong>upper bounding</strong>):</p>
23
<p>Объявим дженерик-интерфейс c ограничением сверху (<strong>upper bounding</strong>):</p>
24
interface BoxMap<K extends Box, V>{ void put(K key, V value); V get(K key); }<p>Вот что от этого останется после компиляции:</p>
24
interface BoxMap<K extends Box, V>{ void put(K key, V value); V get(K key); }<p>Вот что от этого останется после компиляции:</p>
25
interface BoxMap{ void put(Box key, Object value); Object get(Box key); }<p><strong>Пример 3. Bridge-метод</strong></p>
25
interface BoxMap{ void put(Box key, Object value); Object get(Box key); }<p><strong>Пример 3. Bridge-метод</strong></p>
26
<p>Создадим класс-наследник коробки для бумаги и переопределим в нём метод putItem:</p>
26
<p>Создадим класс-наследник коробки для бумаги и переопределим в нём метод putItem:</p>
27
class CoolPaperBox extends Box<Paper>{ public void putItem(Paper item) { super.putItem(item); } }<p>Этому классу не всё равно, какого типа объекты приходят к нему в putItem, - нужно, чтобы они были типа Paper. Поэтому компилятору придётся немного докрутить класс - добавить в него bridge-метод с приведением типа:</p>
27
class CoolPaperBox extends Box<Paper>{ public void putItem(Paper item) { super.putItem(item); } }<p>Этому классу не всё равно, какого типа объекты приходят к нему в putItem, - нужно, чтобы они были типа Paper. Поэтому компилятору придётся немного докрутить класс - добавить в него bridge-метод с приведением типа:</p>
28
class CoolPaperBox extends Box<Paper>{ public void putItem(Paper item) { super.putItem(item); } // это и есть bridge-метод public void putItem(Object item) { putItem((Paper)item); } }<p>А вот ещё несколько примеров дженерик-типов и того, что от них останется после компиляции:</p>
28
class CoolPaperBox extends Box<Paper>{ public void putItem(Paper item) { super.putItem(item); } // это и есть bridge-метод public void putItem(Object item) { putItem((Paper)item); } }<p>А вот ещё несколько примеров дженерик-типов и того, что от них останется после компиляции:</p>
29
<strong>До компиляции</strong><strong>После компиляции</strong><T extends Box<T>>Box<? super Box>BoxList<Box>[]List[]<p>Из-за стирания типов при выполнении программы точно не известно, какой<em>конкретно</em>тип будет иметь экземпляр дженерик-класса. Единственное исключение - дженерик с wildcard без ограничений, например List<?>. Такой список будет считаться List<Object>.</p>
29
<strong>До компиляции</strong><strong>После компиляции</strong><T extends Box<T>>Box<? super Box>BoxList<Box>[]List[]<p>Из-за стирания типов при выполнении программы точно не известно, какой<em>конкретно</em>тип будет иметь экземпляр дженерик-класса. Единственное исключение - дженерик с wildcard без ограничений, например List<?>. Такой список будет считаться List<Object>.</p>
30
<p>Теперь, когда вы знаете про<strong>type erasure</strong>и его последствия, наверняка сможете ответить на вопрос, почему нельзя создать дженерик-Exception:</p>
30
<p>Теперь, когда вы знаете про<strong>type erasure</strong>и его последствия, наверняка сможете ответить на вопрос, почему нельзя создать дженерик-Exception:</p>
31
class GenericException<T> extends Exception { // не скомпилируется }<p><strong>Ответ:</strong></p>
31
class GenericException<T> extends Exception { // не скомпилируется }<p><strong>Ответ:</strong></p>
32
<p>В каждом блоке<strong>try</strong><strong>catch</strong>проверяется тип исключения, так как разные типы исключений могут обрабатываться по-разному. Для дженерик-исключения определить конкретный тип было бы невозможно, а потому компилятор даже не даст его создать. Это правило относится к классу Throwable и его наследникам.</p>
32
<p>В каждом блоке<strong>try</strong><strong>catch</strong>проверяется тип исключения, так как разные типы исключений могут обрабатываться по-разному. Для дженерик-исключения определить конкретный тип было бы невозможно, а потому компилятор даже не даст его создать. Это правило относится к классу Throwable и его наследникам.</p>
33
<p>Наследник дженерик-класса может быть дженериком или обычным классом. Это зависит от того, как обращаться с параметрами типа родителя. Разберём три примера со знакомым нам Box<T>.</p>
33
<p>Наследник дженерик-класса может быть дженериком или обычным классом. Это зависит от того, как обращаться с параметрами типа родителя. Разберём три примера со знакомым нам Box<T>.</p>
34
<p><strong>Пример 1.</strong>Класс-наследник - не дженерик.</p>
34
<p><strong>Пример 1.</strong>Класс-наследник - не дженерик.</p>
35
public class SuperNonGenericBox extends Box<Paper> { }<p>Чтобы получить обычный, не дженерик-класс, мы должны вместо параметра T передать какой-то конкретный тип, что мы и сделали - передали Paper.</p>
35
public class SuperNonGenericBox extends Box<Paper> { }<p>Чтобы получить обычный, не дженерик-класс, мы должны вместо параметра T передать какой-то конкретный тип, что мы и сделали - передали Paper.</p>
36
<p><strong>Пример 2.</strong>Класс-наследник и сам дженерик с тем же числом параметров.</p>
36
<p><strong>Пример 2.</strong>Класс-наследник и сам дженерик с тем же числом параметров.</p>
37
public class SuperGenericBox<T> extends Box<T> { }<p>Параметры у Box и SuperGenericBox не обязаны обозначаться буквой T (от type) - можно брать любую. В этом примере важно, чтобы буквы были одинаковые, иначе компилятор не разберётся.</p>
37
public class SuperGenericBox<T> extends Box<T> { }<p>Параметры у Box и SuperGenericBox не обязаны обозначаться буквой T (от type) - можно брать любую. В этом примере важно, чтобы буквы были одинаковые, иначе компилятор не разберётся.</p>
38
<p><strong>Пример 3.</strong>Класс-наследник - дженерик с другим числом параметров.</p>
38
<p><strong>Пример 3.</strong>Класс-наследник - дженерик с другим числом параметров.</p>
39
public class SuperDoubleGenericBox<T, V> extends Box<T> { public void newMethod(V param) { // здесь что-то происходит } }<p>Здесь уже не один, а два параметра. Один передадим родителю, а второй используем как-нибудь ещё - например, напишем метод newMethod с параметром этого нового типа.</p>
39
public class SuperDoubleGenericBox<T, V> extends Box<T> { public void newMethod(V param) { // здесь что-то происходит } }<p>Здесь уже не один, а два параметра. Один передадим родителю, а второй используем как-нибудь ещё - например, напишем метод newMethod с параметром этого нового типа.</p>
40
<p>У наследника класса-дженерика может быть сколько угодно параметров, включая ноль (когда это вовсе не дженерик). Главное - помнить про все параметры типа родительского класса и передать для каждого параметра конкретный тип или какой-то из параметров класса-наследника.</p>
40
<p>У наследника класса-дженерика может быть сколько угодно параметров, включая ноль (когда это вовсе не дженерик). Главное - помнить про все параметры типа родительского класса и передать для каждого параметра конкретный тип или какой-то из параметров класса-наследника.</p>
41
<p>В Java можно присвоить объект одного типа объекту другого типа, если типы совместимы: реализуют один и тот же интерфейс или лежат в одной цепочке наследования.</p>
41
<p>В Java можно присвоить объект одного типа объекту другого типа, если типы совместимы: реализуют один и тот же интерфейс или лежат в одной цепочке наследования.</p>
42
<p><strong>Например</strong>, PaperBox - наследник Box, и пример ниже успешно компилируется:</p>
42
<p><strong>Например</strong>, PaperBox - наследник Box, и пример ниже успешно компилируется:</p>
43
class Box{} class PaperBox extends Box{} class Test{ Box box = new PaperBox(); }<p>В терминах объектно-ориентированного программирования это называют отношением<strong>is a</strong>(является): бумажная коробка - это коробка (является коробкой). Или говорят, что PaperBox - это<strong>подтип</strong>(subtype) Box. При этом Box -<strong>супертип</strong>PaperBox.</p>
43
class Box{} class PaperBox extends Box{} class Test{ Box box = new PaperBox(); }<p>В терминах объектно-ориентированного программирования это называют отношением<strong>is a</strong>(является): бумажная коробка - это коробка (является коробкой). Или говорят, что PaperBox - это<strong>подтип</strong>(subtype) Box. При этом Box -<strong>супертип</strong>PaperBox.</p>
44
<p>Теперь возьмём не простую коробку, а её дженерик-вариант (Box<T>), в которую будем класть разные типы мусора: Paper, Glass и тому подобные типы - наследники Garbage:</p>
44
<p>Теперь возьмём не простую коробку, а её дженерик-вариант (Box<T>), в которую будем класть разные типы мусора: Paper, Glass и тому подобные типы - наследники Garbage:</p>
45
class Garbage{} class Paper extends Garbage{} class Glass extends Garbage{} class Box<T extends Garbage> { // методы класса }<p>В этом случае в качестве аргумента типа можно выбрать как Garbage, так и его подтип:</p>
45
class Garbage{} class Paper extends Garbage{} class Glass extends Garbage{} class Box<T extends Garbage> { // методы класса }<p>В этом случае в качестве аргумента типа можно выбрать как Garbage, так и его подтип:</p>
46
Box<Garbage> box = new Box<>(); box.putItem(new Garbage()); // успешно компилируется box.putItem(new Paper()); // успешно компилируется<p>Но что, если Box<Garbage> станет типом параметра метода? Сможем ли мы в этом случае передать другой дженерик-тип? Напишем простой пример:</p>
46
Box<Garbage> box = new Box<>(); box.putItem(new Garbage()); // успешно компилируется box.putItem(new Paper()); // успешно компилируется<p>Но что, если Box<Garbage> станет типом параметра метода? Сможем ли мы в этом случае передать другой дженерик-тип? Напишем простой пример:</p>
47
public void handle(Box<Garbage> box) { // что-то делаем с коробкой } public void test() { handle(new Box<Paper>()); // не скомпилируется }<p>И убедимся, что замена тут не пройдёт. Несмотря на то что Paper - подтип Garbage, Box<Paper> -<strong>не</strong>подтип Box<Garbage>.</p>
47
public void handle(Box<Garbage> box) { // что-то делаем с коробкой } public void test() { handle(new Box<Paper>()); // не скомпилируется }<p>И убедимся, что замена тут не пройдёт. Несмотря на то что Paper - подтип Garbage, Box<Paper> -<strong>не</strong>подтип Box<Garbage>.</p>
48
<p>Дженерики инвариантны. Это означает, что, даже если A - подтип B, дженерик от A не является подтипом дженерика от B.</p>
48
<p>Дженерики инвариантны. Это означает, что, даже если A - подтип B, дженерик от A не является подтипом дженерика от B.</p>
49
<p>Для сравнения, массивы в Java ковариантны: если A - подтип B, A[] - подтип B[].</p>
49
<p>Для сравнения, массивы в Java ковариантны: если A - подтип B, A[] - подтип B[].</p>
50
Несмотря на то что Paper - наследник Garbage, Box<Paper> - не наследник Box<Garbage>. Они оба наследники Object. Инфографика: Екатерина Степанова / Skillbox Media<p>Потренируемся сначала на простых типах и вспомним, что при переопределении методов необязательно полностью повторять сигнатуру родительского метода. Например, у таких методов могут различаться типы возвращаемых значений.</p>
50
Несмотря на то что Paper - наследник Garbage, Box<Paper> - не наследник Box<Garbage>. Они оба наследники Object. Инфографика: Екатерина Степанова / Skillbox Media<p>Потренируемся сначала на простых типах и вспомним, что при переопределении методов необязательно полностью повторять сигнатуру родительского метода. Например, у таких методов могут различаться типы возвращаемых значений.</p>
51
<p>Переопределение будет правильным, если тип переопределённого метода - это подтип исходного метода. Например, так:</p>
51
<p>Переопределение будет правильным, если тип переопределённого метода - это подтип исходного метода. Например, так:</p>
52
class Box { public Garbage doSomething() { return new Garbage(); } } class PaperBox extends Box { // корректное переопределение, т. к. Paper - подтип Garbage @Override public Paper doSomething() { return (Paper) super.doSomething(); } }<p>Добавим немного дженериков и применим то же правило:</p>
52
class Box { public Garbage doSomething() { return new Garbage(); } } class PaperBox extends Box { // корректное переопределение, т. к. Paper - подтип Garbage @Override public Paper doSomething() { return (Paper) super.doSomething(); } }<p>Добавим немного дженериков и применим то же правило:</p>
53
class Box { public List<Garbage> doSomething() { return Collections.emptyList(); } } class PaperBox extends Box { // корректное переопределение, // т. к. ArrayList<Garbage> - подтип List<Garbage> @Override public ArrayList<Garbage> doSomething() { return (ArrayList<Garbage>) super.doSomething(); } }<p>Дженерики добавляют ещё пару возможностей для корректного переопределения. Оно будет верным, если:</p>
53
class Box { public List<Garbage> doSomething() { return Collections.emptyList(); } } class PaperBox extends Box { // корректное переопределение, // т. к. ArrayList<Garbage> - подтип List<Garbage> @Override public ArrayList<Garbage> doSomething() { return (ArrayList<Garbage>) super.doSomething(); } }<p>Дженерики добавляют ещё пару возможностей для корректного переопределения. Оно будет верным, если:</p>
54
<ul><li>переопределённый метод возвращает значение сырого (raw) типа от дженерик-типа, возвращаемого исходным методом;</li>
54
<ul><li>переопределённый метод возвращает значение сырого (raw) типа от дженерик-типа, возвращаемого исходным методом;</li>
55
<li>переопределённый метод возвращает значение сырого (raw) типа от <em>наследника</em>дженерик-типа, возвращаемого исходным методом.</li>
55
<li>переопределённый метод возвращает значение сырого (raw) типа от <em>наследника</em>дженерик-типа, возвращаемого исходным методом.</li>
56
</ul><p>Звучит сложно, так что лучше взглянем на код:</p>
56
</ul><p>Звучит сложно, так что лучше взглянем на код:</p>
57
class Box { public List<Garbage> doSomething() { return Collections.emptyList(); } } class PaperBox extends Box { // корректное переопределение, // т. к. мы взяли raw-type от List<Garbage> @Override public List doSomething() { return super.doSomething(); } } class GlassBox extends Box { // корректное переопределение, т. к. мы взяли // raw-type от ArrayList<Garbage> - наследника List<Garbage> @Override public ArrayList doSomething() { return (ArrayList) super.doSomething(); } }<p>Правда, в обоих случаях компилятор покажет предупреждение о небезопасном использовании типов (<strong>unchecked warning</strong>):</p>
57
class Box { public List<Garbage> doSomething() { return Collections.emptyList(); } } class PaperBox extends Box { // корректное переопределение, // т. к. мы взяли raw-type от List<Garbage> @Override public List doSomething() { return super.doSomething(); } } class GlassBox extends Box { // корректное переопределение, т. к. мы взяли // raw-type от ArrayList<Garbage> - наследника List<Garbage> @Override public ArrayList doSomething() { return (ArrayList) super.doSomething(); } }<p>Правда, в обоих случаях компилятор покажет предупреждение о небезопасном использовании типов (<strong>unchecked warning</strong>):</p>
58
<p>Note: GlassBox.java uses unchecked or unsafe operations.</p>
58
<p>Note: GlassBox.java uses unchecked or unsafe operations.</p>
59
<p>Его можно понять: исходный метод требует, чтобы возвращался список объектов типа Garbage, а переопределённые хотят просто какой-то список. Там могут быть объекты типа Garbage, а могут и любые другие - вот компилятору и тревожно.</p>
59
<p>Его можно понять: исходный метод требует, чтобы возвращался список объектов типа Garbage, а переопределённые хотят просто какой-то список. Там могут быть объекты типа Garbage, а могут и любые другие - вот компилятору и тревожно.</p>
60
<p>Зато если в исходном методе возвращаемый тип - с wildcard без ограничений, то при аналогичном переопределении предупреждений не будет:</p>
60
<p>Зато если в исходном методе возвращаемый тип - с wildcard без ограничений, то при аналогичном переопределении предупреждений не будет:</p>
61
class Box { public List<?> doSomething() { return Collections.emptyList(); } } class PaperBox extends Box { // корректное переопределение, // и никаких unchecked warnings @Override public List doSomething() { return super.doSomething(); } }<p>При переопределении дженерик-методов с одинаковым числом параметров типа можно произвольно менять обозначения этих параметров:</p>
61
class Box { public List<?> doSomething() { return Collections.emptyList(); } } class PaperBox extends Box { // корректное переопределение, // и никаких unchecked warnings @Override public List doSomething() { return super.doSomething(); } }<p>При переопределении дженерик-методов с одинаковым числом параметров типа можно произвольно менять обозначения этих параметров:</p>
62
class Box { public <T> void doSomething(T item) { // здесь что-то происходит } } class SuperBox extends Box { @Override public <S> void doSomething(S item) { // здесь что-то происходит } }<p>В переопределённом методе параметр типа назван S, а не T, но переопределение остаётся корректным.</p>
62
class Box { public <T> void doSomething(T item) { // здесь что-то происходит } } class SuperBox extends Box { @Override public <S> void doSomething(S item) { // здесь что-то происходит } }<p>В переопределённом методе параметр типа назван S, а не T, но переопределение остаётся корректным.</p>
63
<p>А вот ограничения для дженерика в переопределённом методе добавлять нельзя:</p>
63
<p>А вот ограничения для дженерика в переопределённом методе добавлять нельзя:</p>
64
class SuperBox extends Box { @Override // не скомпилируется, так как это НЕ переопределение public <S extends Paper> void genericMethod(S item) { // здесь что-то происходит } }<p>Получился не переопределённый метод, а просто метод с таким же названием.</p>
64
class SuperBox extends Box { @Override // не скомпилируется, так как это НЕ переопределение public <S extends Paper> void genericMethod(S item) { // здесь что-то происходит } }<p>Получился не переопределённый метод, а просто метод с таким же названием.</p>
65
<p>Зато можно из дженерик-метода сделать обычный метод:</p>
65
<p>Зато можно из дженерик-метода сделать обычный метод:</p>
66
class SuperBox extends Box { @Override public void genericMethod(Object item) { // здесь что-то происходит } }<p>Компилятор спокоен, потому что метод в классе Box станет именно таким после type erasure - параметр типа будет заменён на Object.</p>
66
class SuperBox extends Box { @Override public void genericMethod(Object item) { // здесь что-то происходит } }<p>Компилятор спокоен, потому что метод в классе Box станет именно таким после type erasure - параметр типа будет заменён на Object.</p>
67
<p>Переопределение дженерик-метода будет корректно, если:</p>
67
<p>Переопределение дженерик-метода будет корректно, если:</p>
68
<ul><li>сигнатуры методов в классе-родителе и классе-наследнике совпадают или различаются с точностью до обозначений параметров типа;</li>
68
<ul><li>сигнатуры методов в классе-родителе и классе-наследнике совпадают или различаются с точностью до обозначений параметров типа;</li>
69
<li>тип результата переопределённого метода - подтип для типа результата исходного метода;</li>
69
<li>тип результата переопределённого метода - подтип для типа результата исходного метода;</li>
70
<li>переопределённый метод возвращает raw-type типа результата исходного метода или его подтип;</li>
70
<li>переопределённый метод возвращает raw-type типа результата исходного метода или его подтип;</li>
71
<li>сигнатуры методов будут совпадать после type erasure.</li>
71
<li>сигнатуры методов будут совпадать после type erasure.</li>
72
</ul><p>Если нужно что-то сделать с коллекциями объектов нескольких подтипов, удобны wildcards с ограничениями.</p>
72
</ul><p>Если нужно что-то сделать с коллекциями объектов нескольких подтипов, удобны wildcards с ограничениями.</p>
73
<p>Например: List<? extends Paper> означает, что список может состоять из объектов типа Paper и всех его подтипов, а в List<? super Paper> могут быть объекты типа Paper и всех супертипов - например, Garbage или Object.</p>
73
<p>Например: List<? extends Paper> означает, что список может состоять из объектов типа Paper и всех его подтипов, а в List<? super Paper> могут быть объекты типа Paper и всех супертипов - например, Garbage или Object.</p>
74
<p>С wildcards и коллекциями есть маленькая проблема - коллекции вроде тех, что в примере выше, нельзя использовать на полную катушку: свободно читать из них и записывать новые данные. Чтобы запомнить это ограничение, даже придумали принцип - принцип PECS.</p>
74
<p>С wildcards и коллекциями есть маленькая проблема - коллекции вроде тех, что в примере выше, нельзя использовать на полную катушку: свободно читать из них и записывать новые данные. Чтобы запомнить это ограничение, даже придумали принцип - принцип PECS.</p>
75
<p><strong>PECS</strong> - Producer Extends, Consumer Super. Его суть:</p>
75
<p><strong>PECS</strong> - Producer Extends, Consumer Super. Его суть:</p>
76
<ul><li>Коллекции с wildcards и ключевым словом<strong>extends</strong> - это<strong>producers</strong>(производители, генераторы), они лишь предоставляют данные.</li>
76
<ul><li>Коллекции с wildcards и ключевым словом<strong>extends</strong> - это<strong>producers</strong>(производители, генераторы), они лишь предоставляют данные.</li>
77
<li>Коллекции с wildcards и ключевым словом<strong>super</strong> - это<strong>consumers</strong>(потребители), они принимают данные, но не отдают их.</li>
77
<li>Коллекции с wildcards и ключевым словом<strong>super</strong> - это<strong>consumers</strong>(потребители), они принимают данные, но не отдают их.</li>
78
</ul><p>Получается, в коллекцию с extends нельзя добавлять, а из коллекции с super нельзя читать? Вроде бы всё понятно, но давайте проверим:</p>
78
</ul><p>Получается, в коллекцию с extends нельзя добавлять, а из коллекции с super нельзя читать? Вроде бы всё понятно, но давайте проверим:</p>
79
List<? super Garbage>> list = new ArrayList<>();<p>Попробуем положить сюда экземпляр Paper - наследника Garbage:</p>
79
List<? super Garbage>> list = new ArrayList<>();<p>Попробуем положить сюда экземпляр Paper - наследника Garbage:</p>
80
list.add(new Paper()); // не скомпилируется<p>Получим ошибку компиляции. Ладно, тогда, может, хотя бы объект типа Garbage подойдёт?</p>
80
list.add(new Paper()); // не скомпилируется<p>Получим ошибку компиляции. Ладно, тогда, может, хотя бы объект типа Garbage подойдёт?</p>
81
list.add(new Garbage()); // не скомпилируется<p>И снова нет. Принцип PECS не соврал - объект в такой список добавить нельзя. Единственное исключение - null. Вот так можно:</p>
81
list.add(new Garbage()); // не скомпилируется<p>И снова нет. Принцип PECS не соврал - объект в такой список добавить нельзя. Единственное исключение - null. Вот так можно:</p>
82
list.add(null); // OK<p>С первой частью принципа разобрались, теперь создадим коллекцию с ограничением снизу:</p>
82
list.add(null); // OK<p>С первой частью принципа разобрались, теперь создадим коллекцию с ограничением снизу:</p>
83
List<? super Paper> list = new ArrayList<>();<p>Добавим туда один объект типа Paper:</p>
83
List<? super Paper> list = new ArrayList<>();<p>Добавим туда один объект типа Paper:</p>
84
list.add(new Paper());<p>И попробуем его же прочитать. Если верить PECS, у нас это не должно получиться:</p>
84
list.add(new Paper());<p>И попробуем его же прочитать. Если верить PECS, у нас это не должно получиться:</p>
85
list.get(0); //OK<p>Но компилятору всё нравится - никаких ошибок нет. Проблемы, впрочем, начнутся, когда мы захотим сохранить полученное значение в переменной типа Paper или типа, совместимого с ним:</p>
85
list.get(0); //OK<p>Но компилятору всё нравится - никаких ошибок нет. Проблемы, впрочем, начнутся, когда мы захотим сохранить полученное значение в переменной типа Paper или типа, совместимого с ним:</p>
86
Paper p = list.get(0); // не скомпилируется<p>Вторая часть принципа PECS означает, что из коллекций, ограниченных снизу, нельзя без явного приведения типа (cast) прочитать объекты граничного класса, да и всех его родителей тоже. Единственное, что доступно, - тип Object:</p>
86
Paper p = list.get(0); // не скомпилируется<p>Вторая часть принципа PECS означает, что из коллекций, ограниченных снизу, нельзя без явного приведения типа (cast) прочитать объекты граничного класса, да и всех его родителей тоже. Единственное, что доступно, - тип Object:</p>
87
Object p = list.get(0); // OK<p>К сожалению, принцип PECS ничего не говорит о том, какие объекты можно читать из producer, а какие добавлять в customer. Мы не придумали своего принципа, но сделали табличку, чтобы собрать вместе все правила:</p>
87
Object p = list.get(0); // OK<p>К сожалению, принцип PECS ничего не говорит о том, какие объекты можно читать из producer, а какие добавлять в customer. Мы не придумали своего принципа, но сделали табличку, чтобы собрать вместе все правила:</p>
88
<strong>Тип ограничения</strong><strong>Что можно читать</strong><strong>Что можно записывать</strong><?<strong>extends</strong>SomeType>Объекты SomeType и всех его супертиповТолько null<?<strong>super</strong>SomeType>Объекты типа ObjectОбъекты типа SomeType и всех его подтипов<p>И сводный пример:</p>
88
<strong>Тип ограничения</strong><strong>Что можно читать</strong><strong>Что можно записывать</strong><?<strong>extends</strong>SomeType>Объекты SomeType и всех его супертиповТолько null<?<strong>super</strong>SomeType>Объекты типа ObjectОбъекты типа SomeType и всех его подтипов<p>И сводный пример:</p>
89
class Garbage {} class Paper extends Garbage {} class CoolPaper extends Paper{} public void testUpperBounding(List<? extends Paper> list){ Paper p = list.get(0); // OK Garbage g = list.get(1); // OK CoolPaper sp = list.get(2); // не скомпилируется list.add(new Paper()); // не скомпилируется list.add(null); // OK } public void testLowBounding(List<? super Paper> list){ Paper p = list.get(0); // не скомпилируется Garbage g = list.get(1); // не скомпилируется Object o = list.get(2); // OK list.add(new Garbage()); // не скомпилируется list.add(new Paper()); // OK list.add(new CoolPaper()); // OK }<p>И даже картинку нарисовали:</p>
89
class Garbage {} class Paper extends Garbage {} class CoolPaper extends Paper{} public void testUpperBounding(List<? extends Paper> list){ Paper p = list.get(0); // OK Garbage g = list.get(1); // OK CoolPaper sp = list.get(2); // не скомпилируется list.add(new Paper()); // не скомпилируется list.add(null); // OK } public void testLowBounding(List<? super Paper> list){ Paper p = list.get(0); // не скомпилируется Garbage g = list.get(1); // не скомпилируется Object o = list.get(2); // OK list.add(new Garbage()); // не скомпилируется list.add(new Paper()); // OK list.add(new CoolPaper()); // OK }<p>И даже картинку нарисовали:</p>
90
Какие типы можно читать из коллекции с ограниченными wildcards и записывать в неё.Инфографика: Екатерина Степанова / Skillbox Media<p>Теперь точно не запутаетесь :)</p>
90
Какие типы можно читать из коллекции с ограниченными wildcards и записывать в неё.Инфографика: Екатерина Степанова / Skillbox Media<p>Теперь точно не запутаетесь :)</p>
91
<p>Ещё больше хитростей дженериков и других особенностей Java - на курсе "<a>Профессия Java-разработчик</a>". Научим программировать на самом востребованном языке и поможем устроиться на работу.</p>
91
<p>Ещё больше хитростей дженериков и других особенностей Java - на курсе "<a>Профессия Java-разработчик</a>". Научим программировать на самом востребованном языке и поможем устроиться на работу.</p>
92
<a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>
92
<a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>