Back to Generics & Type Safety
Q4Question 4 of 7

What are raw types and heap pollution?

Key Concepts
  • A raw type is a generic type used without type arguments (List instead of List<String>), which bypasses compile-time type checking.
  • Heap pollution occurs when a variable of a parameterised type refers to an object that is not of that type, typically caused by mixing raw and generic code.
  • The ClassCastException from heap pollution occurs at retrieval time (get), far from the insertion point, making the bug very difficult to trace.
  • @SafeVarargs suppresses the unchecked warning on varargs methods with generic parameters, promising that the method does not store into the varargs array.
  • Varargs with generics create an Object[] at runtime due to erasure, which can cause heap pollution if the method stores incompatible types into it.
  • Raw types exist only for backward compatibility with pre-Java-5 code and should never be used in new code.
  • Collections.checkedList can catch heap pollution at insertion time instead of retrieval time.

Definition

Raw types bypass generics and cause heap pollution where a parameterised variable holds the wrong type, leading to ClassCastException far from the bug; @SafeVarargs suppresses warnings on safe varargs methods with generics.
  • Raw type – using a generic type without type parameters: List instead of List; the compiler loses type safety and issues unchecked warnings
  • Heap pollution – a variable of type List actually contains non-String elements, typically because raw types or unchecked casts bypassed the compiler
  • Delayed ClassCastException – the CCE occurs at retrieval (get) when the compiler-inserted cast fails, not at the point where the wrong type was inserted
  • @SafeVarargs – annotation on methods with generic varargs (T... args) that promises the method does not store into the varargs array, suppressing the heap pollution warning
  • Varargs erasure – T... becomes Object[] at runtime; storing into this array can introduce the wrong type

At National Bank of India, a raw type is like an unlabelled filing drawer. Anyone can put anything in: cheques, loan forms, even a sandwich. Heap pollution is when someone tells Rajesh "this drawer contains only cheques" (List), but a sandwich (Integer) was slipped in through the unlabelled back door (raw type). When Rajesh reaches in expecting a cheque, he grabs the sandwich and the alarm goes off (ClassCastException). The alarm sounds when he reaches in, not when the sandwich was filed, making it very hard to find who put it there.

Understanding the Concepts

What are raw types?

A raw type is a generic class or interface used without specifying type arguments. Before Java 5, all collections were raw: List, Map, Set. Raw types still compile in modern Java for backward compatibility, but they bypass all generic type checking.

JAVA
package com.bank.demo;

import java.util.ArrayList;
import java.util.List;

public class RawTypeDemo {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        // Raw type: no type safety
        List rawList = new ArrayList();
        rawList.add("SAV2001");
        rawList.add(42);        // no error! compiler does not check
        rawList.add(3.14);      // no error!

        System.out.println("Raw list: " + rawList);
        System.out.println("Size: " + rawList.size());

        // Parameterised type: compile-time safety
        List<String> typedList = new ArrayList<>();
        typedList.add("SAV2001");
        // typedList.add(42);   // COMPILE ERROR
    }
}
TEXT
Raw list: [SAV2001, 42, 3.14]
Size: 3

What is heap pollution?

Heap pollution occurs when a variable of a parameterised type refers to an object that is not of that type. This typically happens when raw types are mixed with generic types.

JAVA
package com.bank.demo;

import java.util.ArrayList;
import java.util.List;

public class HeapPollutionDemo {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        List<String> accounts = new ArrayList<>();
        accounts.add("SAV2001");

        // Raw reference bypasses generics
        List rawRef = accounts;
        rawRef.add(42);         // Integer sneaks into List<String>!
        rawRef.add(new Object()); // Object sneaks in too!

        System.out.println("Size: " + accounts.size());

        // The ClassCastException occurs HERE, far from the insertion
        String first = accounts.get(0);  // OK
        System.out.println("First: " + first);

        try {
            String second = accounts.get(1); // ClassCastException!
        } catch (ClassCastException e) {
            System.out.println("Heap pollution caught at get(1): " + e.getMessage());
        }
    }
}
TEXT
Size: 3
First: SAV2001
Heap pollution caught at get(1): class java.lang.Integer cannot be cast to class java.lang.String

Why is heap pollution dangerous?

The ClassCastException from heap pollution occurs at the point of retrieval, not at the point of insertion. The stack trace points to the get() call, which might be in a completely different method, class, or even module from where the wrong type was inserted. This makes the bug extremely hard to trace.

JAVA
package com.bank.demo;

import java.util.ArrayList;
import java.util.List;

public class DelayedCCEDemo {

    // This method inserts the wrong type (the bug is HERE)
    @SuppressWarnings("unchecked")
    static void legacyPopulate(List list) {
        list.add("SAV2001");
        list.add(99999);  // Integer inserted into what will be List<String>
    }

    // This method reads the list (the CCE happens HERE)
    static void processAccounts(List<String> accounts) {
        for (String account : accounts) {
            System.out.println("Processing: " + account);
        }
    }

    public static void main(String[] args) {
        List<String> accounts = new ArrayList<>();
        legacyPopulate(accounts); // bug introduced here

        try {
            processAccounts(accounts); // CCE happens here, far from the bug
        } catch (ClassCastException e) {
            System.out.println("CCE in processAccounts: " + e.getMessage());
        }
    }
}
TEXT
Processing: SAV2001
CCE in processAccounts: class java.lang.Integer cannot be cast to class java.lang.String

What is @SafeVarargs?

When a method has generic varargs (T... args), the compiler creates an Object[] at runtime because of type erasure. This creates a potential for heap pollution if the method stores into the varargs array. The @SafeVarargs annotation tells the compiler that the method does not store into the array, suppressing the warning.

JAVA
package com.bank.demo;

import java.util.Arrays;
import java.util.List;

public class SafeVarargsDemo {

    // SAFE: only reads from the varargs array
    @SafeVarargs
    static <T> List<T> toList(T... elements) {
        return Arrays.asList(elements);
    }

    // UNSAFE: stores into the varargs array
    static <T> void unsafeStore(T... elements) {
        Object[] arr = elements; // varargs is Object[] at runtime
        arr[0] = 42;            // stores Integer, might corrupt T[]
    }

    public static void main(String[] args) {
        List<String> accounts = toList("SAV2001", "CUR3001", "FD4001");
        System.out.println("Safe varargs: " + accounts);

        try {
            unsafeStore("SAV2001", "CUR3001");
        } catch (ArrayStoreException e) {
            System.out.println("Unsafe store: " + e.getClass().getSimpleName());
        }
    }
}
TEXT
Safe varargs: [SAV2001, CUR3001, FD4001]
Unsafe store: ArrayStoreException

What are common mistakes?

Mistake: Using raw types in new code

JAVA
// WRONG: raw type, unchecked
Map map = new HashMap();
map.put("SAV2001", "Rajesh");
map.put(42, 3.14); // no error, corrupted map
JAVA
// CORRECT: parameterised type
Map<String, String> map = new HashMap<>();
map.put("SAV2001", "Rajesh");
// map.put(42, 3.14); // COMPILE ERROR

Mistake: Using @SafeVarargs on an unsafe method

JAVA
// WRONG: method stores into varargs array
@SafeVarargs // LYING to the compiler!
static <T> void dangerous(T... items) {
    Object[] array = items;
    array[0] = new Object(); // heap pollution!
}
JAVA
// CORRECT: @SafeVarargs only on methods that READ varargs
@SafeVarargs
static <T> List<T> safe(T... items) {
    return List.of(items); // only reads, never stores
}

How can you detect heap pollution early?

Use Collections.checkedList to catch the wrong type at insertion time instead of retrieval time.

JAVA
package com.bank.demo;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CheckedListDemo {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        List<String> checked = Collections.checkedList(new ArrayList<>(), String.class);
        List rawRef = checked;

        try {
            rawRef.add(42); // ClassCastException HERE, at insertion!
        } catch (ClassCastException e) {
            System.out.println("Caught at add: " + e.getMessage());
        }
    }
}
TEXT
Caught at add: Attempt to insert class java.lang.Integer element into collection with element type class java.lang.String

What should a senior engineer know?

  • unchecked warning is meaningful. Every unchecked warning is a potential heap pollution. Treat them seriously; do not blindly suppress them.
  • @SafeVarargs restrictions. The annotation can only be placed on static methods, final methods, constructors, and private methods (since Java 9), because the compiler must ensure the method cannot be overridden with an unsafe implementation.
  • Legacy migration strategy. When migrating from raw types to generics, use Collections.checkedList/Map/Set at API boundaries to catch pollution from legacy code.
  • SonarQube and IDE warnings. Modern IDEs and static analysis tools flag raw type usage and unchecked casts. Configure them as errors, not warnings.

What are common interview follow-up questions?

  • "What is heap pollution?" : A variable of a generic type holds a reference to an object of a different type, causing ClassCastException at retrieval.
  • "When does the ClassCastException occur with heap pollution?" : At retrieval time (get), not at insertion time, making it hard to debug.
  • "What does @SafeVarargs promise?" : That the method does not store into the varargs array, so no heap pollution can occur through varargs.
  • "How do you prevent heap pollution?" : Never use raw types, never suppress unchecked warnings without verification, and use Collections.checkedList at legacy boundaries.

Visual Overview

DIAGRAM
  Heap Pollution Flow
  ====================

  1. Raw reference bypasses generics:
  ┌────────────────────────────────────┐
  │ List<String> typed = new ArrayList │
  │ List raw = typed                   │ <-- raw reference
  │ raw.add(42)                        │ <-- Integer sneaks in!
  └────────────────────────────────────┘

  2. List state: ["SAV2001", 42]
     Type declaration says: List<String>
     Actual content: String + Integer
     This IS heap pollution.

  3. ClassCastException at retrieval:
  ┌────────────────────────────────────┐
  │ String s = typed.get(1)            │
  │ Bytecode: (String) typed.get(1)    │
  │ 42 cannot be cast to String        │
  │ -> ClassCastException!             │
  │                                    │
  │ Stack trace points HERE            │
  │ Bug was introduced at raw.add(42)  │
  │ which could be in another class!   │
  └────────────────────────────────────┘

Key Takeaway

Raw types are generic types used without type parameters, and they bypass all compile-time type checking. When raw and generic code mix, heap pollution can occur: a List variable secretly contains non-String elements. The resulting ClassCastException appears at retrieval time, far from the point of insertion, making it one of the hardest bugs to trace in a large codebase.

@SafeVarargs is an annotation for methods that accept generic varargs (T... args). Because varargs become Object[] at runtime, storing into the array can introduce the wrong type. @SafeVarargs tells the compiler that the method only reads from the array, never writes to it. It can only be used on methods that cannot be overridden (static, final, private, or constructors).

At National Bank of India, Rajesh encountered heap pollution when a legacy module returned a raw List. The CCE appeared in the reporting module, three layers away from the legacy code. Pankaj's fix was to wrap the legacy boundary with Collections.checkedList(result, Account.class), which caught the wrong type at insertion time with a clear stack trace pointing to the offending legacy method.

Remember one sentence: never use raw types in new code because they cause heap pollution where the ClassCastException occurs at retrieval, far from where the wrong type was inserted.

---

Share
Use ← → arrow keys to navigate