Java Basics: Generic

Generic

Generic wildcards

  • unbound - List<?>. Фактически, любой тип данных кроме примитивов. Используется в том случае, если надо разрешить использовать любой тип данных:
1
2
3
List<String> list = ...;
List<Object> objects = list; // ошибка компиляции
List<?> validObjects = list;
  • upper-bound - List<? extends Number>. Любой наследник, а так же сам целевой тип. Для данного типа (де-факто) запрещена любая вставка (кроме null) - для вставки используется List<? super Number>. При этом, первичная вставка допускается:
1
2
3
4
5
6
Map<String, ? extends String> map = new HashMap() {{
put("x", "1");
put("y", "2");
}};
map.putAll(Map.of(...)); // compile error
map.put("z", "3"); // compile error

Перелистывание (например, через for-each) возможно с определением целевого типа:

1
2
for(String val : map.values()) { ... }
for(Map.Entry<String, ? extends String> entry : map.entrySet()) {}
  • lower-bound - List<? super Number>. Любой класс, от которого целевой наследуется, или сам целевой. Данный вид wildcard не может быть использован в методе. Вставка доступна, а получение - доступно только с принудительным преобразованием:
1
2
3
4
5
6
List<? super Number> nums = new ArrayList<>() {{
add(1);
}};
nums.add(2);
Number num1 = nums.get(1); // compile error
Number num2 = (Number) nums.get(1);

Ковариантность

Если объект, или массив объектов, или какой-то типа данных может быть применен к другому (родительскому) типу, то это называется ковариантность:

1
2
3
String[] a = {"a", "b", "c"};
Object[] b = a;
b[0] = 123; // ошибка ArrayStoreException;

PECS (Producer Extends Consumer Super)

Был описан выше, но для повторения:

  • если <? extends ...> - это только чтение (Producer); для коллекций он ничего в себя не принимает, только отдает;
  • если <? super ...> - это только хранение (Consumer); в случае коллекций он только принимает и ничего не отдает.

Важно! В случае unbound, это де-факто <? extends Object>, а значит допускается только чтение! Если предполагается и читать и записывать, лучше не использовать wildcards.

Переменные типа

1
public <T extends Comparable<T>> T method(...) { ... }

В примере выше присутствует конструкция, называемая, recursive bound - т.е. когда при определении Generic где-то в нем используется целевой класс - т.е. в нашем случае T используется в Comparable<T>. Это действие ограничено только правилами наследования. Так же, в примере выше нельзя использовать <T super ...>.

Multiple bound

Множественные ограничения, которые накладывают дополнительные ограничения на тип:

1
public <T extends String & Comparable<? super T>> T method(...) {...}

Важно! Второй и последующими границами могут быть только интерфейсы. Например:

1
public <T extends Number & Comparable<T> & CharSequence>void test(T item)

Type erasure

В действительности, никаких типизаций не существует в скомпилированном коде - компилятор перед использованием Generic типа выполняет принудительный type-cast:

1
2
List<String> list = ...;
String s = (String) list.get(0); // в коде: list.get(0);

В таком случае могут возникать непредвиденные ситуации:

1
2
3
4
5
6
public class MyClass implements Comparable<MyClass> {
@Override
public int compareTo(MyClass that) {...}

public int compareTo(Object that) {...}
}

В примере выше, компилятор из первого метода создает фоновый метод compareTo(Object), который мы пытаемся создать. И поскольку фоновый и созданный имеют одинаковую сигнатуру, они будут друг с другом конфликтовать.

Причем, если порядок определения методов будет соответствовать указанному выше, ошибка будет при определении класса с указанием дубликата метода compareTo. Если дефиницию методов поменять местами, то ошибка будет на первом методе (с сигнатурой Object), сигнализирующая о дублировании метода compareTo.

Игра на понижение

Для коллекций Java не дает понижать типы:

1
2
3
List<Object> objectList = null;
List<String> stringList = new ArrayList<>();
objectList = stringList; // ошибка компиляции

и даже указание generics на понижение не поможет:

1
objectList = (List<Object>) stringList;           //  ошибка компиляции

тогда можно просто отказаться от него:

1
objectList = (List) stringList;

Это плохая практика и валидация кода может не пройти, но компилятор такое пропустит. Если бы generic объект был неконвертируемым, скажем, objectList был бы объявлен как List<Number>, то при чтении из objectList (например, итерации как Number) было бы выброшено ClassCastException.

 Comments
Comment plugin failed to load
Loading comment plugin