Generics
Boxing
Unboxing
Programming
Type System

Boxing and unboxing with generics

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Boxing converts a value type (like int, double, bool) into a reference type by wrapping it in a heap-allocated object. Unboxing extracts the value type from that object. In C# and Java, boxing happens implicitly when value types are assigned to object or interface references. Generics eliminate most boxing by preserving type information at compile time, but there are subtle cases where boxing still occurs.

Boxing and Unboxing in C#

csharp
1// Boxing: int (stack) → object (heap)
2int x = 42;
3object boxed = x;  // Boxing — allocates on heap
4
5// Unboxing: object (heap) → int (stack)
6int y = (int)boxed;  // Unboxing — copies value back to stack

Each boxing operation allocates memory on the heap and copies the value. Each unboxing operation type-checks and copies the value back. Both are expensive compared to working directly with value types.

The Cost of Boxing

csharp
1// Without generics: ArrayList stores objects — every int is boxed
2ArrayList list = new ArrayList();
3for (int i = 0; i < 1_000_000; i++)
4{
5    list.Add(i);  // Boxing: int → object (1 million heap allocations!)
6}
7
8int sum = 0;
9foreach (object obj in list)
10{
11    sum += (int)obj;  // Unboxing: object → int
12}

Each Add(i) allocates ~16 bytes on the heap (object header + value). For 1 million ints, that is ~16 MB of unnecessary heap allocations plus GC pressure.

Generics Eliminate Boxing

csharp
1// With generics: List<int> stores ints directly — no boxing
2List<int> list = new List<int>();
3for (int i = 0; i < 1_000_000; i++)
4{
5    list.Add(i);  // No boxing — stored as int in internal array
6}
7
8int sum = 0;
9foreach (int val in list)
10{
11    sum += val;  // No unboxing — already an int
12}

List<int> uses an internal int[] array. No heap allocation per element, no type-checking on retrieval. The CLR generates specialized code for each value type used with a generic class.

How C# Generics Avoid Boxing (Reification)

C# generics are reified — the runtime generates distinct native code for each value type:

csharp
1// The CLR creates separate implementations:
2// List<int>   → uses int[] internally
3// List<double> → uses double[] internally
4// List<string> → uses string[] (reference type, no boxing anyway)

This is different from Java's approach (see below). The generic type parameter is preserved at runtime, enabling the JIT compiler to emit optimized code.

Java Generics and Type Erasure

Java generics use type erasure — the compiler removes type parameters and replaces them with Object at runtime:

java
1// What you write
2List<Integer> list = new ArrayList<>();
3list.add(42);
4int val = list.get(0);
5
6// What the JVM sees after erasure
7List list = new ArrayList();
8list.add(Integer.valueOf(42));    // Autoboxing: int → Integer
9int val = (Integer) list.get(0);  // Cast + unboxing

In Java, List<int> is impossible — you must use List<Integer>, and every primitive is autoboxed. This is why Java has wrapper classes (Integer, Double, Boolean).

When Boxing Still Happens in C# Generics

Even with generics, boxing occurs in these cases:

1. Calling Interface Methods on Constrained Generics

csharp
1// No boxing — constrained call
2public static bool IsDefault<T>(T value) where T : IEquatable<T>
3{
4    return value.Equals(default(T));  // Direct call, no boxing
5}
6
7// Boxing — calling through non-generic interface
8public static string Stringify<T>(T value)
9{
10    return value.ToString();  // May or may not box (depends on override)
11}

2. Casting to Object or Interface

csharp
1public static void Process<T>(T value)
2{
3    object obj = value;           // Boxing if T is a value type
4    IComparable comp = (IComparable)value;  // Boxing
5}

3. Using Non-Generic Collections

csharp
// Hashtable, ArrayList, etc. store objects — always box value types
Hashtable table = new Hashtable();
table[1] = "one";  // Key 1 is boxed to object

Benchmarking Boxing vs Generics

csharp
1using BenchmarkDotNet.Attributes;
2
3[MemoryDiagnoser]
4public class BoxingBenchmark
5{
6    [Benchmark]
7    public int WithBoxing()
8    {
9        ArrayList list = new ArrayList();
10        for (int i = 0; i < 10000; i++)
11            list.Add(i);  // Boxing
12
13        int sum = 0;
14        foreach (object obj in list)
15            sum += (int)obj;  // Unboxing
16        return sum;
17    }
18
19    [Benchmark]
20    public int WithoutBoxing()
21    {
22        List<int> list = new List<int>();
23        for (int i = 0; i < 10000; i++)
24            list.Add(i);  // No boxing
25
26        int sum = 0;
27        foreach (int val in list)
28            sum += val;  // No unboxing
29        return sum;
30    }
31}
32
33// Results:
34// |         Method |       Mean | Allocated |
35// |--------------- |-----------:|----------:|
36// |     WithBoxing | 350.2 μs   |  234.4 KB |
37// |  WithoutBoxing |  45.1 μs   |   39.1 KB |

Generics are roughly 8x faster and use 6x less memory in this scenario.

C# vs Java Generics Comparison

FeatureC# GenericsJava Generics
ImplementationReification (runtime types)Type erasure (compile-time only)
Value typesNo boxing (List<int>)Always autoboxing (List<Integer>)
Runtime type infoAvailable (typeof(T))Lost (cannot do instanceof T)
Primitive supportDirect (List<int>, List<double>)Wrapper classes only
PerformanceBetter for value typesGC overhead from autoboxing

Common Pitfalls

  • Hidden boxing: Calling object.ToString(), object.GetHashCode(), or object.Equals() on a value type through a non-constrained generic parameter may cause boxing. Use where T : IEquatable<T> constraints to enable direct dispatch.
  • Java autoboxing in loops: for (int i : integerList) unboxes every element. For performance-critical loops with millions of elements, consider using int[] instead of List<Integer>.
  • Null with unboxing: In Java, unboxing a null Integer throws NullPointerException. In C#, unboxing a null object to a value type throws NullReferenceException. Always null-check before unboxing.
  • Dictionary key boxing: In C#, using a struct as a Dictionary key without implementing IEquatable<T> causes boxing on every lookup because the default EqualityComparer uses object.Equals.
  • Java identity comparisons: Integer.valueOf(127) == Integer.valueOf(127) is true (cached), but Integer.valueOf(128) == Integer.valueOf(128) is false (different objects). Always use .equals() for wrapper comparisons.

Summary

  • Boxing wraps value types in heap objects; unboxing extracts them — both are expensive
  • C# generics (reified) eliminate boxing by generating specialized code for each value type
  • Java generics (type-erased) always autobox primitives into wrapper classes
  • Use List<int> instead of ArrayList in C# to avoid boxing
  • Add interface constraints (where T : IEquatable<T>) to prevent boxing in generic methods
  • Benchmark with MemoryDiagnoser to detect hidden boxing allocations

Course illustration
Course illustration

All Rights Reserved.