📋 Раздел 20 · Вопрос #21

В чём разница между `List<?>` и `List<Object>`

Главное отличие между List и List заключается в типобезопасности и правилах полиморфизма (ковариантности и инвариантности):


🟢 Junior Level

Главное отличие между List<?> и List<Object> заключается в типобезопасности и правилах полиморфизма (ковариантности и инвариантности):

  1. List<Object> — конкретный инвариантный список объектов:
    • Может хранить любые ссылочные типы (String, Integer, пользовательские классы).
    • В него можно свободно добавлять любые объекты через метод .add(new Object()).
    • Инвариантен: List<String> не является подтипом List<Object>. Передать List<String> в метод, ожидающий List<Object>, нельзя (ошибка компиляции).
  2. List<?> (Unbounded Wildcard) — список неизвестного типа:
    • Означает «список элементов какого-то одного конкретного типа, но компилятору неизвестно, какого именно».
    • Является супертипом для любого параметризованного списка (List<String>, List<Integer>, List<Object>).
    • Запрещает запись элементов: компилятор не позволяет добавить в него никакой объект (за исключением литерала null), так как не может гарантировать соответствие типов.
    • Чтение элементов возвращает тип Object.
List<String> names = new ArrayList<>(List.of("Alice", "Bob"));

// ❌ Ошибка компиляции: List<String> нельзя привести к List<Object>
List<Object> objList = names; 

// ✅ Корректно: List<?> является супертипом для любого List<T>
List<?> wildcardList = names; 

// ❌ Ошибка компиляции: в List<?> нельзя добавить элемент
wildcardList.add("Charlie"); 

// ✅ Разрешено: null допустим для любого ссылочного типа
wildcardList.add(null); 

// ✅ Чтение всегда безопасно, возвращает Object
Object item = wildcardList.get(0);

🟡 Middle Level

Почему List<String> не является List<Object> (Проблема ковариантности)

Если бы компилятор Java разрешил присваивание List<Object> list = new ArrayList<String>(), возникло бы разрушение типобезопасности в runtime:

// Представим, что компилятор это разрешил:
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // Допустим, скомпилировалось...

objects.add(Integer.valueOf(42)); // Добавили число в список объектов!

// В куче лежит список strings, содержащий Integer!
// Следующая строка выбросит ClassCastException:
String s = strings.get(0); // java.lang.Integer cannot be cast to java.lang.String

Именно поэтому дженерики в Java по умолчанию инвариантны. Массивы в Java исторически являются ковариантными (Object[] arr = new String[5];), что приводит к выбрасыванию ArrayStoreException во время выполнения при попытке положить несовместимый тип. Дженерики спроектированы так, чтобы ловить такие ошибки на этапе компиляции.

Захват Wildcard и запрет записи

Синтаксис List<?> эквивалентен List<? extends Object>.
Когда компилятор встречает метод list.add(E element) у типа List<?>, тип E захватывается как уникальная внутренняя анонимная переменная типа — capture of ? (или CAP#1 extends Object).

void addElement(List<?> list) {
    // Ошибка компиляции:
    // incompatible types: String cannot be converted to capture#1 of ?
    list.add("test"); 
}

Компилятор рассуждает так: «В аргумент list мог прийти List<Integer>. Попытка положить туда String разрушит кучу. Поскольку я не знаю точного типа CAP#1, я запрещаю передавать в качестве аргумента любой объект, кроме null (так как null принадлежит любому ссылочному типу)».

Сравнительная таблица

Характеристика List (Raw type) List<Object> List<?>
Проверка типов javac Отключена (Unchecked warning) Строгая типобезопасность Строгая типобезопасность
Добавление элементов Любой объект (add(Object)) Любой объект (add(Object)) Только null
Чтение элементов Object (требует явного каста) Object Object
Совместимость присваивания Принимает любой List Только List<Object> Принимает любой List<T>
Рекомендация по использованию Запрещено в современном коде Для гетерогенных коллекций Для методов чтения, не зависящих от T (size(), print())

🔴 Senior Level

Wildcard Capture Helper Pattern

Ограничение List<?> часто мешает реализации обобщенных алгоритмов (например, перестановки элементов местами или реверса списка), даже если операция абсолютно типобезопасна:

// Ошибка компиляции: capture of ?
public void swap(List<?> list, int i, int j) {
    var temp = list.get(i); // temp имеет тип Object
    list.set(i, list.get(j)); // ❌ set(int, capture#1 of ?) cannot be applied to (int, Object)
}

Для решения этой проблемы стандартная библиотека Java (включая java.util.Collections) использует паттерн вспомогательного метода захвата (Capture Helper):

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j); // Wildcard перезахватывается в конкретный T
}

// Приватный generic-метод фиксирует тип T на время выполнения вызова:
private static <T> void swapHelper(List<T> list, int i, int j) {
    list.set(i, list.set(j, list.get(i))); // ✅ Полная типобезопасность
}

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

Инициализация: new ArrayList<?> против new ArrayList<Object>

Спецификация JLS §15.9 запрещает использование wildcard в операторе new:

List<?> list1 = new ArrayList<?>(); // ❌ Ошибка компиляции: wildcard cannot be instantiated
List<?> list2 = new ArrayList<Object>(); // ✅ Валидно
List<?> list3 = new ArrayList<String>(); // ✅ Валидно
List<?> list4 = new ArrayList<>();       // ✅ Валидно (diamond оператор выведет Object)

Оператор new требует конкретный reifiable или однозначно параметризованный тип для аллокации экземпляра в куче, тогда как подстановочный знак ? предназначен исключительно для использования на стороне объявления переменных или параметров методов (Use-site variance).


4 каверзных вопроса

1. Является ли List<?> действительно read-only коллекцией?

Ответ:
Нет, List<?> не является неизменяемой (immutable) или read-only коллекцией.
С подстановочным знаком запрещены только вызовы методов, принимающих параметр типа (add(E), addAll(Collection<? extends E>), set(int, E)).
Однако методы, не зависящие от типа элемента, прекрасно работают и изменяют список:

List<?> list = new ArrayList<>(List.of(1, 2, 3));
list.remove(0); // ✅ Удалит элемент по индексу
list.clear();    // ✅ Очистит всю коллекцию
list.add(null);  // ✅ Добавит элемент null

Если требуется настоящая защита от модификации, следует использовать Collections.unmodifiableList(...) или List.copyOf(...).

2. В чём разница между void process(List<?> list) и <T> void process(List<T> list)? Когда что использовать?

Ответ:

  • <T> void process(List<T> list) вводит переменную типа T. Это необходимо, если тип аргумента взаимосвязан с другими аргументами метода, возвращаемым значением или если внутри метода требуется создавать новые экземпляры или складывать элементы из одного списка в другой.
  • void process(List<?> list) не вводит дополнительных переменных типа. Согласно правилу эффективного проектирования API (Effective Java), если параметр типа появляется в сигнатуре метода всего один раз и не зависит от возвращаемого значения, следует предпочесть подстановочный знак ?, так как он делает интерфейс метода чище и проще для вызывающего кода.

3. Можно ли передать List<String> в метод void inspect(List<Object> list) с помощью явного кастинга? Что произойдет?

Ответ:
Прямой каст (List<Object>) list приведет к ошибке компиляции: incompatible types: List<String> cannot be converted to List<Object>.
Обойти компилятор можно только через «двойной каст» или raw type:

List<Object> dangerous = (List<Object>) (List<?>) list; // Unchecked warning
dangerous.add(100);

При попытке вызывающего кода прочитать этот элемент из оригинального List<String> возникнет немедленный ClassCastException в месте вызова метода get().

4. Почему компилятор запрещает List<?>[] arrayOfLists = new ArrayList<?>[10], но разрешает сам тип ссылки List<?>[]?

Ответ:
На самом деле new ArrayList<?>[10] разрешён!
Компилятор запрещает создание массивов дженериков с конкретными параметрами (например, new ArrayList<String>[10]), так как из-за стирания типов массив List[] в runtime не способен проверить, что в него кладут именно ArrayList<String>, а не ArrayList<Integer>.
Но List<?> является неограниченным подстановочным знаком (unbounded wildcard type), который считается reifiable-типом (стирание типа List<?> совпадает с его представлением в байт-коде). Поэтому массив List<?>[] создать через new ArrayList<?>[10] компилятор позволяет безопасно.


🎯 Шпаргалка для интервью

  • Ключевое отличие:
    • List<Object> — конкретный тип, инвариантен, можно писать любые объекты, List<String> в него передать нельзя.
    • List<?> — супертип для любых списков (List<String>, List<Integer>), писать нельзя (только null), читать можно только как Object.
  • Почему generics инвариантны: Для предотвращения загрязнения кучи (Heap Pollution). Если бы List<String> был подтипом List<Object>, в список строк можно было бы положить число через ссылку на базовый тип.
  • List<?> не read-only: Можно вызывать clear(), remove(i) и add(null). Запрещены только операции, принимающие параметризованный тип E.
  • Capture Helper Pattern: Чтобы сделать мутирующую операцию над элементами List<?> (например, swap), используют приватный дженерик-метод <T> void helper(List<T> list).
  • Raw List vs List<?>: Raw type отключает проверку типов и опасен; List<?> сохраняет абсолютную типобезопасность.

Связанные темы

☕ Java Interview Questions and Answers · Open Source Knowledge Portal

Built from the GitHub repository. Fully synced across English, Ukrainian, and Russian.