Question 18 · Section 20

Can you use primitive types as generic parameters

Generics use type erasure — after compilation, T is replaced with Object. Primitives do not inherit from Object, so they cannot be type parameters.

Language versions: English Russian Ukrainian

🟢 Junior Level

No, you cannot. Generics in Java work only with reference types, not with primitives.

// ❌ Cannot use primitives
List<int> ints = new ArrayList<>();       // compilation error
Map<String, double> map = new HashMap<>(); // compilation error
Box<char> box = new Box<>();              // compilation error

// ✅ Use wrapper classes
List<Integer> ints = new ArrayList<>();       // OK
Map<String, Double> map = new HashMap<>();     // OK
Box<Character> box = new Box<>();              // OK

All primitives and their wrapper classes:

Primitive Wrapper
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character

🟡 Middle Level

Why is it this way?

Generics use type erasure — after compilation, T is replaced with Object. Primitives do not inherit from Object, so they cannot be type parameters.

public class Box<T> {
    private T value;
}

// After type erasure:
public class Box {
    private Object value;  // T -> Object
}

// int cannot be Object — that's why Box<int> is not allowed

Autoboxing and Unboxing

Java automatically converts primitives ↔ wrapper classes:

List<Integer> list = new ArrayList<>();

// Autoboxing: int -> Integer
list.add(42);  // compiler: list.add(Integer.valueOf(42))

// Unboxing: Integer -> int
int value = list.get(0);  // compiler: list.get(0).intValue()

Autoboxing overhead:

// Primitive — 4 bytes, on stack
int primitive = 42;

// Wrapper — ~24 bytes, on heap
Integer wrapper = Integer.valueOf(42);
// - Object header: 12-16 bytes
// - int value: 4 bytes
// - Padding: 4 bytes

Common mistakes

  1. Autoboxing null:
    Integer wrapper = null;
    int primitive = wrapper;  // ❌ NullPointerException!
    
  2. Autoboxing performance: ```java // ❌ Slow — Integer created for every iteration List list = new ArrayList<>(); for (int i = 0; i < 1_000_000; i++) { list.add(i); // autoboxing every time }

// ✅ Faster — primitive collection 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 caches values from -128 to 127
Integer a = 100;
Integer b = 100;
System.out.println(a == b);  // true (same object from cache)

Integer c = 200;
Integer d = 200;
System.out.println(c == d);  // false (different objects)

// Integer.valueOf(int) checks cache:
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:

Type         | Size      | Location
-------------|-----------|-------------
int          | 4 bytes   | Stack
Integer      | ~24 bytes | Heap
int[]        | 4N bytes  | Heap
Integer[]    | ~24N bytes| Heap
ArrayList<Integer> | ~32N bytes | Heap

Architectural Trade-offs

Wrapper vs primitives:

Aspect Wrapper Primitives
Memory 24 bytes 4 bytes
Speed Slower Faster
Null Allowed Not allowed
Generics
Collections

Edge Cases

1. Autoboxing in conditions:

Integer count = 0;
for (int i = 0; i < 1000; i++) {
    count = count + i;  // unboxing + boxing every time
}
// 2000 autoboxing/unboxing operations!

// ✅ Better
int count = 0;
for (int i = 0; i < 1000; i++) {
    count += i;
}

2. Ternary operator with autoboxing:

Integer a = 1;
Integer b = null;

// ⚠️ Dangerous — unboxing null
Integer result = condition ? a : b;  // OK
int primitive = condition ? a : b;   // ❌ NPE if a OR b == null when unboxing to int.

Performance

Benchmark (JMH, Java 21):

Operation                | Time
-------------------------|--------
int addition             | 0.3 ns
Integer addition         | 1.5 ns (5x slower)
// Approximate values (JMH). Depend on JVM, CPU, warmup.
int[] access             | 1 ns
ArrayList<Integer> get   | 3 ns (3x slower)
Autoboxing int->Integer  | 5 ns
Unboxing Integer->int    | 1 ns

Highload consequences:

// ❌ Problem — 1M operations with autoboxing
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
    numbers.add(i);  // 1M autoboxing = ~5ms + 24MB memory
}

// ✅ Solution — primitive collections
// Eclipse Collections, fastutil, or arrays
int[] numbers = new int[1_000_000];  // 4MB memory, zero autoboxing

Production Experience

Primitive collections:

// fastutil — library for primitive collections
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);

// Or just arrays
int[] array = new int[100];

Stream API with primitives:

// ❌ Stream<Integer> — autoboxing
List<Integer> list = List.of(1, 2, 3);
int sum = list.stream()
    .mapToInt(Integer::intValue)  // unboxing
    .sum();

// ✅ IntStream — no autoboxing
int[] array = {1, 2, 3};
int sum = Arrays.stream(array).sum();

// ✅ IntStream range
int sum = IntStream.rangeClosed(1, 100).sum();

Best Practices

// ✅ Wrapper for collections and generics
List<Integer> list = new ArrayList<>();
Map<String, Double> map = new HashMap<>();

// ✅ Primitives for computations
int sum = 0;
for (int i = 0; i < 1000; i++) {
    sum += i;
}

// ✅ Primitive streams
int sum = IntStream.range(0, 100).sum();

// ✅ Primitive collections for highload
int[] array = new int[1_000_000];

// ❌ Wrapper for computations
// ❌ Ignoring autoboxing overhead
// ❌ Autoboxing null

🎯 Interview Cheat Sheet

Must know:

  • Primitives cannot be used as type parameters: List<int> — compilation error
  • Reason: type erasure replaces T with Object, primitives do not inherit Object
  • Autoboxing: primitive -> wrapper (int -> Integer), unboxing: the reverse
  • Integer caches values -128..127 (IntegerCache), other wrappers have caches too
  • Autoboxing overhead: ~24 bytes per Integer vs 4 bytes int, ~5 ns per boxing
  • For highload: primitive collections (fastutil, Eclipse Collections) or arrays

Frequent follow-up questions:

  • Why can’t primitives be used in generics? — Type erasure: T -> Object, primitives are not Object
  • What is the overhead of autoboxing? — Memory: 24 bytes vs 4 bytes, time: ~5 ns per boxing
  • What happens when unboxing null? — NullPointerException: Integer x = null; int y = x;
  • When to use primitive collections? — Highload, large data volumes (1M+ elements)

Red flags (DO NOT say):

  • ❌ “List works in new Java" — Forbidden forever, no plans to add it
  • ❌ “Autoboxing is free” — 6x memory + time for boxing/unboxing
  • ❌ “Integer a = 100; Integer b = 100; a == b is always true” — Only for cache (-128..127)
  • ❌ “Stream is as fast as IntStream" — IntStream without autoboxing is significantly faster

Related topics:

  • [[11. What are Generics in Java]]
  • [[13. What is type erasure]]
  • [[14. Can you create an array of generic type]]
  • [[22. Can you overload methods that differ only in generic parameters]]