Вопрос 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()

Оверhead 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. Можно ли перегружать методы, отличающиеся только дженерик-параметрами]]