Перейти к основному содержимому

Java Generics — <?> против <? расширяет объект>

· 5 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Обзор

В этом кратком руководстве мы увидим сходства и различия между <?> и <? расширяет Object> в Java Generics .

Однако, поскольку это сложная тема, необходимо получить общее представление о предмете, прежде чем мы погрузимся в суть дела.

2. Общие сведения о дженериках

Обобщения были введены в JDK 5 для устранения ошибок во время выполнения и повышения безопасности типов. Эта дополнительная безопасность типов устраняет приведение типов в некоторых случаях использования и позволяет программистам писать общие алгоритмы, которые могут привести к более читаемому коду.

Например, до JDK 5 нам приходилось работать с элементами списка, используя приведение типов. Это, в свою очередь, создало определенный класс ошибок времени выполнения:

List aList = new ArrayList();
aList.add(new Integer(1));
aList.add("a_string");

for (int i = 0; i < aList.size(); i++) {
Integer x = (Integer) aList.get(i);
}

Теперь в этом коде есть две проблемы, которые мы хотели бы решить:

  • Нам нужно явное приведение для извлечения значений из aList — тип зависит от типа переменной слева — в данном случае Integer .
  • Мы получим ошибку времени выполнения на второй итерации, когда попытаемся преобразовать a_string в Integer .

Дженерики заполняют для нас эту роль:

List<Integer> iList = new ArrayList<>();
iList.add(1);
iList.add("a_string"); // compile time error

for (int i = 0; i < iList.size(); i++) {
int x = iList.get(i);
}

Компилятор сообщит нам, что невозможно добавить a_string в список типа Integer , что лучше, чем выяснить это во время выполнения.

Более того, никакого явного приведения не требуется, так как компилятор уже знает, что iList содержит Integer s. Кроме того, благодаря волшебству распаковки нам даже не понадобился тип Integer , достаточно его примитивной формы.

3. Подстановочные знаки в дженериках

Знак вопроса или подстановочный знак используется в дженериках для обозначения неизвестного типа. Он может иметь три формы:

  • Неограниченные подстановочные знаки : List<?> представляет собой список неизвестного типа.
  • Подстановочные знаки с верхней границей : Список<? extends Number> представляет собой список чисел или их подтипов, таких как Integer и Double .
  • Нижние подстановочные знаки : список <? super Integer> представляет собой список Integer или его супертипов Number и Object

Теперь, поскольку Object является неотъемлемым супертипом всех типов в Java, у нас может возникнуть соблазн подумать, что он также может представлять неизвестный тип. Другими словами, List<?> и List<Object> могут служить той же цели. Но это не так.

Рассмотрим эти два метода:

public static void printListObject(List<Object> list) {    
for (Object element : list) {        
System.out.print(element + " ");    
}        
}    

public static void printListWildCard(List<?> list) {    
for (Object element: list) {        
System.out.print(element + " ");    
}    
}

Учитывая список Integer s, скажем:

List<Integer> li = Arrays.asList(1, 2, 3);

printListObject(li) не будет компилироваться, и мы получим эту ошибку:

The method printListObject(List<Object>) is not applicable for the arguments (List<Integer>)

Тогда как printListWildCard(li) скомпилируется и выведет 1 2 3 на консоль.

4. <?> и <? extends Object> – Сходства

В приведенном выше примере, если мы изменим сигнатуру метода для printListWildCard на:

public static void printListWildCard(List<? extends Object> list)

Он будет работать так же, как и printListWildCard(List<?> list) . Это связано с тем, что Object является супертипом всех объектов Java, и в основном все расширяет Object . Таким образом, List of Integer также обрабатывается.

Короче, что это значит ? и ? extends Object являются синонимами в этом примере .

Хотя в большинстве случаев это верно, но есть и несколько отличий . Давайте рассмотрим их в следующем разделе.

5. <?> и <? extends Object> – Разница

Реализуемые типы — это те, тип которых не стирается во время компиляции. Другими словами, представление нереализуемого типа во время выполнения будет иметь меньше информации, чем его аналог во время компиляции, потому что часть ее будет стерта.

Как правило, параметризованные типы не подлежат повторению. Это означает , что List<String> и Map<Integer, String> не могут быть повторены. Компилятор стирает их тип и обрабатывает их как список и карту соответственно.

Единственным исключением из этого правила являются неограниченные типы подстановочных знаков. Это означает , что List<?> и Map<?,?> можно повторно использовать .

С другой стороны, List<? extends Object> не может быть повторен . Несмотря на тонкость, это заметная разница.

Нереифицируемые типы нельзя использовать в определенных ситуациях , например, в операторе instanceof или в качестве элементов массива.

Итак, если мы напишем:

List someList = new ArrayList<>();
boolean instanceTest = someList instanceof List<?>

Этот код компилируется, и instanceTest имеет значение true .

Но если мы используем оператор instanceof для List<? расширяет Объект> :

List anotherList = new ArrayList<>();
boolean instanceTest = anotherList instanceof List<? extends Object>;

то строка 2 не компилируется.

Точно так же в приведенном ниже фрагменте строка 1 компилируется, а строка 2 — нет:

List<?>[] arrayOfList = new List<?>[1];
List<? extends Object>[] arrayOfAnotherList = new List<? extends Object>[1]

6. Заключение

В этом коротком уроке мы увидели сходства и различия в <?> и <? расширяет Объект> .

Хотя в основном они похожи, между ними есть тонкие различия с точки зрения их возможности повторения или нет.