HTML Diff
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&lt;T&gt; - коробка для сбора мусора: можно было положить в неё или извлечь из неё только объект определённого типа:</p>
13 </ul><p>Воспользуемся примером из первой части рассказа о дженериках: там был класс Box&lt;T&gt; - коробка для сбора мусора: можно было положить в неё или извлечь из неё только объект определённого типа:</p>
14 class Box&lt;T&gt; { // обозначение типа - 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&lt;T&gt; { // обозначение типа - 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&lt;Paper&gt; boxForPaper = new Box&lt;Paper&gt;();<p>Можно предположить, что теперь мы имеем дело с таким классом:</p>
15 class Paper {} Box&lt;Paper&gt; boxForPaper = new Box&lt;Paper&gt;();<p>Можно предположить, что теперь мы имеем дело с таким классом:</p>
16 class Box&lt;Paper&gt; { private Paper item; public void putItem(Paper item) { this.item = item; } public Paper getItem() { return item; } }<p>Эта запись помогает понять, как класс будет работать, но она не имеет ничего общего с тем, во что<em>превращается</em>дженерик-класс или интерфейс в результате компиляции.</p>
16 class Box&lt;Paper&gt; { 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&lt;K extends Box, V&gt;{ void put(K key, V value); V get(K key); }<p>Вот что от этого останется после компиляции:</p>
24 interface BoxMap&lt;K extends Box, V&gt;{ 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&lt;Paper&gt;{ public void putItem(Paper item) { super.putItem(item); } }<p>Этому классу не всё равно, какого типа объекты приходят к нему в putItem, - нужно, чтобы они были типа Paper. Поэтому компилятору придётся немного докрутить класс - добавить в него bridge-метод с приведением типа:</p>
27 class CoolPaperBox extends Box&lt;Paper&gt;{ public void putItem(Paper item) { super.putItem(item); } }<p>Этому классу не всё равно, какого типа объекты приходят к нему в putItem, - нужно, чтобы они были типа Paper. Поэтому компилятору придётся немного докрутить класс - добавить в него bridge-метод с приведением типа:</p>
28 class CoolPaperBox extends Box&lt;Paper&gt;{ public void putItem(Paper item) { super.putItem(item); } // это и есть bridge-метод public void putItem(Object item) { putItem((Paper)item); } }<p>А вот ещё несколько примеров дженерик-типов и того, что от них останется после компиляции:</p>
28 class CoolPaperBox extends Box&lt;Paper&gt;{ public void putItem(Paper item) { super.putItem(item); } // это и есть bridge-метод public void putItem(Object item) { putItem((Paper)item); } }<p>А вот ещё несколько примеров дженерик-типов и того, что от них останется после компиляции:</p>
29 <strong>До компиляции</strong><strong>После компиляции</strong>&lt;T extends Box&lt;T&gt;&gt;Box&lt;? super Box&gt;BoxList&lt;Box&gt;[]List[]<p>Из-за стирания типов при выполнении программы точно не известно, какой<em>конкретно</em>тип будет иметь экземпляр дженерик-класса. Единственное исключение - дженерик с wildcard без ограничений, например List&lt;?&gt;. Такой список будет считаться List&lt;Object&gt;.</p>
29 <strong>До компиляции</strong><strong>После компиляции</strong>&lt;T extends Box&lt;T&gt;&gt;Box&lt;? super Box&gt;BoxList&lt;Box&gt;[]List[]<p>Из-за стирания типов при выполнении программы точно не известно, какой<em>конкретно</em>тип будет иметь экземпляр дженерик-класса. Единственное исключение - дженерик с wildcard без ограничений, например List&lt;?&gt;. Такой список будет считаться List&lt;Object&gt;.</p>
30 <p>Теперь, когда вы знаете про<strong>type erasure</strong>и его последствия, наверняка сможете ответить на вопрос, почему нельзя создать дженерик-Exception:</p>
30 <p>Теперь, когда вы знаете про<strong>type erasure</strong>и его последствия, наверняка сможете ответить на вопрос, почему нельзя создать дженерик-Exception:</p>
31 class GenericException&lt;T&gt; extends Exception { // не скомпилируется }<p><strong>Ответ:</strong></p>
31 class GenericException&lt;T&gt; 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&lt;T&gt;.</p>
33 <p>Наследник дженерик-класса может быть дженериком или обычным классом. Это зависит от того, как обращаться с параметрами типа родителя. Разберём три примера со знакомым нам Box&lt;T&gt;.</p>
34 <p><strong>Пример 1.</strong>Класс-наследник - не дженерик.</p>
34 <p><strong>Пример 1.</strong>Класс-наследник - не дженерик.</p>
35 public class SuperNonGenericBox extends Box&lt;Paper&gt; { }<p>Чтобы получить обычный, не дженерик-класс, мы должны вместо параметра T передать какой-то конкретный тип, что мы и сделали - передали Paper.</p>
35 public class SuperNonGenericBox extends Box&lt;Paper&gt; { }<p>Чтобы получить обычный, не дженерик-класс, мы должны вместо параметра T передать какой-то конкретный тип, что мы и сделали - передали Paper.</p>
36 <p><strong>Пример 2.</strong>Класс-наследник и сам дженерик с тем же числом параметров.</p>
36 <p><strong>Пример 2.</strong>Класс-наследник и сам дженерик с тем же числом параметров.</p>
37 public class SuperGenericBox&lt;T&gt; extends Box&lt;T&gt; { }<p>Параметры у Box и SuperGenericBox не обязаны обозначаться буквой T (от type) - можно брать любую. В этом примере важно, чтобы буквы были одинаковые, иначе компилятор не разберётся.</p>
37 public class SuperGenericBox&lt;T&gt; extends Box&lt;T&gt; { }<p>Параметры у Box и SuperGenericBox не обязаны обозначаться буквой T (от type) - можно брать любую. В этом примере важно, чтобы буквы были одинаковые, иначе компилятор не разберётся.</p>
38 <p><strong>Пример 3.</strong>Класс-наследник - дженерик с другим числом параметров.</p>
38 <p><strong>Пример 3.</strong>Класс-наследник - дженерик с другим числом параметров.</p>
39 public class SuperDoubleGenericBox&lt;T, V&gt; extends Box&lt;T&gt; { public void newMethod(V param) { // здесь что-то происходит } }<p>Здесь уже не один, а два параметра. Один передадим родителю, а второй используем как-нибудь ещё - например, напишем метод newMethod с параметром этого нового типа.</p>
39 public class SuperDoubleGenericBox&lt;T, V&gt; extends Box&lt;T&gt; { 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&lt;T&gt;), в которую будем класть разные типы мусора: Paper, Glass и тому подобные типы - наследники Garbage:</p>
44 <p>Теперь возьмём не простую коробку, а её дженерик-вариант (Box&lt;T&gt;), в которую будем класть разные типы мусора: Paper, Glass и тому подобные типы - наследники Garbage:</p>
45 class Garbage{} class Paper extends Garbage{} class Glass extends Garbage{} class Box&lt;T extends Garbage&gt; { // методы класса }<p>В этом случае в качестве аргумента типа можно выбрать как Garbage, так и его подтип:</p>
45 class Garbage{} class Paper extends Garbage{} class Glass extends Garbage{} class Box&lt;T extends Garbage&gt; { // методы класса }<p>В этом случае в качестве аргумента типа можно выбрать как Garbage, так и его подтип:</p>
46 Box&lt;Garbage&gt; box = new Box&lt;&gt;(); box.putItem(new Garbage()); // успешно компилируется box.putItem(new Paper()); // успешно компилируется<p>Но что, если Box&lt;Garbage&gt; станет типом параметра метода? Сможем ли мы в этом случае передать другой дженерик-тип? Напишем простой пример:</p>
46 Box&lt;Garbage&gt; box = new Box&lt;&gt;(); box.putItem(new Garbage()); // успешно компилируется box.putItem(new Paper()); // успешно компилируется<p>Но что, если Box&lt;Garbage&gt; станет типом параметра метода? Сможем ли мы в этом случае передать другой дженерик-тип? Напишем простой пример:</p>
47 public void handle(Box&lt;Garbage&gt; box) { // что-то делаем с коробкой } public void test() { handle(new Box&lt;Paper&gt;()); // не скомпилируется }<p>И убедимся, что замена тут не пройдёт. Несмотря на то что Paper - подтип Garbage, Box&lt;Paper&gt; -<strong>не</strong>подтип Box&lt;Garbage&gt;.</p>
47 public void handle(Box&lt;Garbage&gt; box) { // что-то делаем с коробкой } public void test() { handle(new Box&lt;Paper&gt;()); // не скомпилируется }<p>И убедимся, что замена тут не пройдёт. Несмотря на то что Paper - подтип Garbage, Box&lt;Paper&gt; -<strong>не</strong>подтип Box&lt;Garbage&gt;.</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&lt;Paper&gt; - не наследник Box&lt;Garbage&gt;. Они оба наследники Object. Инфографика: Екатерина Степанова / Skillbox Media<p>Потренируемся сначала на простых типах и вспомним, что при переопределении методов необязательно полностью повторять сигнатуру родительского метода. Например, у таких методов могут различаться типы возвращаемых значений.</p>
50 Несмотря на то что Paper - наследник Garbage, Box&lt;Paper&gt; - не наследник Box&lt;Garbage&gt;. Они оба наследники 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&lt;Garbage&gt; doSomething() { return Collections.emptyList(); } } class PaperBox extends Box { // корректное переопределение, // т. к. ArrayList&lt;Garbage&gt; - подтип List&lt;Garbage&gt; @Override public ArrayList&lt;Garbage&gt; doSomething() { return (ArrayList&lt;Garbage&gt;) super.doSomething(); } }<p>Дженерики добавляют ещё пару возможностей для корректного переопределения. Оно будет верным, если:</p>
53 class Box { public List&lt;Garbage&gt; doSomething() { return Collections.emptyList(); } } class PaperBox extends Box { // корректное переопределение, // т. к. ArrayList&lt;Garbage&gt; - подтип List&lt;Garbage&gt; @Override public ArrayList&lt;Garbage&gt; doSomething() { return (ArrayList&lt;Garbage&gt;) 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&lt;Garbage&gt; doSomething() { return Collections.emptyList(); } } class PaperBox extends Box { // корректное переопределение, // т. к. мы взяли raw-type от List&lt;Garbage&gt; @Override public List doSomething() { return super.doSomething(); } } class GlassBox extends Box { // корректное переопределение, т. к. мы взяли // raw-type от ArrayList&lt;Garbage&gt; - наследника List&lt;Garbage&gt; @Override public ArrayList doSomething() { return (ArrayList) super.doSomething(); } }<p>Правда, в обоих случаях компилятор покажет предупреждение о небезопасном использовании типов (<strong>unchecked warning</strong>):</p>
57 class Box { public List&lt;Garbage&gt; doSomething() { return Collections.emptyList(); } } class PaperBox extends Box { // корректное переопределение, // т. к. мы взяли raw-type от List&lt;Garbage&gt; @Override public List doSomething() { return super.doSomething(); } } class GlassBox extends Box { // корректное переопределение, т. к. мы взяли // raw-type от ArrayList&lt;Garbage&gt; - наследника List&lt;Garbage&gt; @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&lt;?&gt; doSomething() { return Collections.emptyList(); } } class PaperBox extends Box { // корректное переопределение, // и никаких unchecked warnings @Override public List doSomething() { return super.doSomething(); } }<p>При переопределении дженерик-методов с одинаковым числом параметров типа можно произвольно менять обозначения этих параметров:</p>
61 class Box { public List&lt;?&gt; doSomething() { return Collections.emptyList(); } } class PaperBox extends Box { // корректное переопределение, // и никаких unchecked warnings @Override public List doSomething() { return super.doSomething(); } }<p>При переопределении дженерик-методов с одинаковым числом параметров типа можно произвольно менять обозначения этих параметров:</p>
62 class Box { public &lt;T&gt; void doSomething(T item) { // здесь что-то происходит } } class SuperBox extends Box { @Override public &lt;S&gt; void doSomething(S item) { // здесь что-то происходит } }<p>В переопределённом методе параметр типа назван S, а не T, но переопределение остаётся корректным.</p>
62 class Box { public &lt;T&gt; void doSomething(T item) { // здесь что-то происходит } } class SuperBox extends Box { @Override public &lt;S&gt; void doSomething(S item) { // здесь что-то происходит } }<p>В переопределённом методе параметр типа назван S, а не T, но переопределение остаётся корректным.</p>
63 <p>А вот ограничения для дженерика в переопределённом методе добавлять нельзя:</p>
63 <p>А вот ограничения для дженерика в переопределённом методе добавлять нельзя:</p>
64 class SuperBox extends Box { @Override // не скомпилируется, так как это НЕ переопределение public &lt;S extends Paper&gt; void genericMethod(S item) { // здесь что-то происходит } }<p>Получился не переопределённый метод, а просто метод с таким же названием.</p>
64 class SuperBox extends Box { @Override // не скомпилируется, так как это НЕ переопределение public &lt;S extends Paper&gt; 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&lt;? extends Paper&gt; означает, что список может состоять из объектов типа Paper и всех его подтипов, а в List&lt;? super Paper&gt; могут быть объекты типа Paper и всех супертипов - например, Garbage или Object.</p>
73 <p>Например: List&lt;? extends Paper&gt; означает, что список может состоять из объектов типа Paper и всех его подтипов, а в List&lt;? super Paper&gt; могут быть объекты типа 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&lt;? super Garbage&gt;&gt; list = new ArrayList&lt;&gt;();<p>Попробуем положить сюда экземпляр Paper - наследника Garbage:</p>
79 List&lt;? super Garbage&gt;&gt; list = new ArrayList&lt;&gt;();<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&lt;? super Paper&gt; list = new ArrayList&lt;&gt;();<p>Добавим туда один объект типа Paper:</p>
83 List&lt;? super Paper&gt; list = new ArrayList&lt;&gt;();<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>&lt;?<strong>extends</strong>SomeType&gt;Объекты SomeType и всех его супертиповТолько null&lt;?<strong>super</strong>SomeType&gt;Объекты типа ObjectОбъекты типа SomeType и всех его подтипов<p>И сводный пример:</p>
88 <strong>Тип ограничения</strong><strong>Что можно читать</strong><strong>Что можно записывать</strong>&lt;?<strong>extends</strong>SomeType&gt;Объекты SomeType и всех его супертиповТолько null&lt;?<strong>super</strong>SomeType&gt;Объекты типа ObjectОбъекты типа SomeType и всех его подтипов<p>И сводный пример:</p>
89 class Garbage {} class Paper extends Garbage {} class CoolPaper extends Paper{} public void testUpperBounding(List&lt;? extends Paper&gt; 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&lt;? super Paper&gt; 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&lt;? extends Paper&gt; 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&lt;? super Paper&gt; 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>