В чём разница между `List<?>` и `List<Object>`
Главное отличие между List> и List
🟢 Junior Level
Главное отличие между List<?> и List<Object> заключается в типобезопасности и правилах полиморфизма (ковариантности и инвариантности):
List<Object>— конкретный инвариантный список объектов:- Может хранить любые ссылочные типы (
String,Integer, пользовательские классы). - В него можно свободно добавлять любые объекты через метод
.add(new Object()). - Инвариантен:
List<String>не является подтипомList<Object>. ПередатьList<String>в метод, ожидающийList<Object>, нельзя (ошибка компиляции).
- Может хранить любые ссылочные типы (
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
ListvsList<?>: Raw type отключает проверку типов и опасен;List<?>сохраняет абсолютную типобезопасность.