Питання 18 · Розділ 20

Чи можна використовувати примітивні типи як параметри дженериків

Дженерики використовують type erasure — після компіляції T замінюється на Object. Примітиви не наслідуються від Object, тому не можуть бути type parameters.

Мовні версії: English Russian Ukrainian

🟢 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 байти

Типові помилки

  1. Autoboxing null:
    Integer wrapper = null;
    int primitive = wrapper;  // ❌ NullPointerException!
    
  2. 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. Чи можна перевантажувати методи, що відрізняються лише дженерик-параметрами]]