Чи можна використовувати примітивні типи як параметри дженериків
Дженерики використовують type erasure — після компіляції T замінюється на Object. Примітиви не наслідуються від Object, тому не можуть бути type parameters.
🟢 Junior Level
Ні, не можна. Дженерики в Java працюють тільки з об’єктними типами (reference types), а не з примітивами.
// ❌ Не можна використовувати примітиви
List<int> ints = new ArrayList<>(); // compilation error
Map<String, double> map = new HashMap<>(); // compilation error
Box<char> box = new Box<>(); // compilation error
// ✅ Використовуйте wrapper-класи
List<Integer> ints = new ArrayList<>(); // OK
Map<String, Double> map = new HashMap<>(); // OK
Box<Character> box = new Box<>(); // OK
Всі примітиви та їхні wrapper-класи:
| Примітив | Wrapper |
|---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
boolean |
Boolean |
char |
Character |
🟡 Middle Level
Чому так?
Дженерики використовують type erasure — після компіляції T замінюється на Object. Примітиви не наслідуються від Object, тому не можуть бути type parameters.
public class Box<T> {
private T value;
}
// Після type erasure:
public class Box {
private Object value; // T -> Object
}
// int не може бути Object — тому не можна Box<int>
Autoboxing та Unboxing
Java автоматично конвертує примітиви ↔ wrapper-класи:
List<Integer> list = new ArrayList<>();
// Autoboxing: int -> Integer
list.add(42); // компілятор: list.add(Integer.valueOf(42))
// Unboxing: Integer -> int
int value = list.get(0); // компілятор: list.get(0).intValue()
Overhead autoboxing:
// Примітив — 4 байти, на стеку
int primitive = 42;
// Wrapper — ~24 байти, в купі
Integer wrapper = Integer.valueOf(42);
// - Заголовок об'єкта: 12-16 байт
// - Значення int: 4 байти
// - Вирівнювання: 4 байти
Типові помилки
- Autoboxing null:
Integer wrapper = null; int primitive = wrapper; // ❌ NullPointerException! - Autoboxing performance:
```java
// ❌ Повільно — створюється Integer для кожної ітерації
List
list = new ArrayList<>(); for (int i = 0; i < 1_000_000; i++) { list.add(i); // autoboxing щоразу }
// ✅ Швидше — примітивна колекція int[] array = new int[1_000_000]; for (int i = 0; i < 1_000_000; i++) { array[i] = i; }
---
## 🔴 Senior Level
### Internal Implementation
**Autoboxing cache:**
```java
// Integer кешує значення від -128 до 127
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true (один об'єкт з кешу)
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false (різні об'єкти)
// Integer.valueOf(int) перевіряє кеш:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Memory overhead:
Тип | Розмір | Розташування
-------------|------------|-------------
int | 4 bytes | Stack
Integer | ~24 bytes | Heap
int[] | 4N bytes | Heap
Integer[] | ~24N bytes | Heap
ArrayList<Integer> | ~32N bytes | Heap
Архітектурні Trade-offs
Wrapper vs примітиви:
| Аспект | Wrapper | Примітиви |
|---|---|---|
| Пам’ять | 24 байти | 4 байти |
| Швидкість | Повільніше | Швидше |
| Null | Можна | Не можна |
| Дженерики | ✅ | ❌ |
| Колекції | ✅ | ❌ |
Edge Cases
1. Autoboxing в умовах:
Integer count = 0;
for (int i = 0; i < 1000; i++) {
count = count + i; // unboxing + boxing щоразу
}
// 2000 операцій autoboxing/unboxing!
// ✅ Краще
int count = 0;
for (int i = 0; i < 1000; i++) {
count += i;
}
2. Ternary operator з autoboxing:
Integer a = 1;
Integer b = null;
// ⚠️ Небезпечно — unboxing null
Integer result = condition ? a : b; // OK
int primitive = condition ? a : b; // ❌ NPE якщо a АБО b == null при unboxing в int.
Продуктивність
Бенчмарк (JMH, Java 21):
Операція | Час
-------------------------|--------
int addition | 0.3 ns
Integer addition | 1.5 ns (5x повільніше)
// Приблизні значення (JMH). Залежать від JVM, CPU, warmup.
int[] access | 1 ns
ArrayList<Integer> get | 3 ns (3x повільніше)
Autoboxing int->Integer | 5 ns
Unboxing Integer->int | 1 ns
Highload наслідки:
// ❌ Проблема — 1M операцій з autoboxing
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
numbers.add(i); // 1M autoboxing = ~5ms + 24MB пам'яті
}
// ✅ Рішення — примітивні колекції
// Eclipse Collections, fastutil, або масиви
int[] numbers = new int[1_000_000]; // 4MB пам'яті, zero autoboxing
Production Experience
Примітивні колекції:
// fastutil — бібліотека для примітивних колекцій
IntList numbers = new IntArrayList();
numbers.add(42); // no autoboxing
int value = numbers.getInt(0); // no unboxing
// Eclipse Collections
MutableIntList list = IntLists.mutable.empty();
list.add(1);
list.add(2);
// Або просто масиви
int[] array = new int[100];
Stream API з примітивами:
// ❌ Stream<Integer> — autoboxing
List<Integer> list = List.of(1, 2, 3);
int sum = list.stream()
.mapToInt(Integer::intValue) // unboxing
.sum();
// ✅ IntStream — без autoboxing
int[] array = {1, 2, 3};
int sum = Arrays.stream(array).sum();
// ✅ IntStream range
int sum = IntStream.rangeClosed(1, 100).sum();
Best Practices
// ✅ Wrapper для колекцій та дженериків
List<Integer> list = new ArrayList<>();
Map<String, Double> map = new HashMap<>();
// ✅ Примітиви для обчислень
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
// ✅ Примітивні стріми
int sum = IntStream.range(0, 100).sum();
// ✅ Примітивні колекції для highload
int[] array = new int[1_000_000];
// ❌ Wrapper для обчислень
// ❌ Ігнорування autoboxing overhead
// ❌ Autoboxing null
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- Примітиви не можна використовувати як type parameters:
List<int>— compilation error - Причина: type erasure замінює T на Object, примітиви не наслідують Object
- Autoboxing: примітив -> wrapper (
int->Integer), unboxing: навпаки - Integer кешує значення -128..127 (IntegerCache), інші wrapper-и теж мають кеш
- Autoboxing overhead: ~24 байти на Integer vs 4 байти int, ~5 ns на boxing
- Для highload: примітивні колекції (fastutil, Eclipse Collections) або масиви
Часті уточнюючі запитання:
- Чому примітиви не можна в дженерики? — Type erasure: T -> Object, примітиви не Object
- Який overhead у autoboxing? — Пам’ять: 24 байти vs 4 байти, час: ~5 ns на boxing
- Що буде при unboxing null? — NullPointerException:
Integer x = null; int y = x; - Коли використовувати примітивні колекції? — Highload, великі об’єми даних (1M+ елементів)
Червоні прапорці (НЕ говорити):
- ❌ “List
працює в новій Java" — Заборонено назавжди, немає планів додати - ❌ “Autoboxing безкоштовний” — 6x пам’яті + час на boxing/unboxing
- ❌ “Integer a = 100; Integer b = 100; a == b завжди true” — Тільки для кешу (-128..127)
- ❌ “Stream
такий самий швидкий як IntStream" — IntStream без autoboxing значно швидший
Пов’язані теми:
- [[11. Що таке дженерики (Generics) в Java]]
- [[13. Що таке type erasure (стирання типів)]]
- [[14. Чи можна створити масив дженерик-типу]]
- [[22. Чи можна перевантажувати методи, що відрізняються лише дженерик-параметрами]]