У чому різниця між `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 Tricky Questions
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<?>зберігає абсолютну типобезпеку.