Back to Generics & Type Safety
Q7Question 7 of 7

What does Collections.checkedList do?

Key Concepts
  • Collections.checkedList(list, type) wraps a collection with runtime type checking that verifies every element on insertion.
  • Without the checked wrapper, heap pollution causes ClassCastException at retrieval time, far from where the wrong type was inserted.
  • With the checked wrapper, ClassCastException occurs at insertion time, with a stack trace pointing directly to the buggy code.
  • checkedMap(map, keyType, valueType) checks both key and value types on every put operation.
  • The performance overhead is one instanceof check per insertion, which is negligible for most use cases.
  • Checked wrappers are especially useful when passing collections to legacy code that uses raw types.
  • The checked wrapper does not prevent heap pollution through the underlying collection reference; always wrap at creation time.

Definition

Collections.checkedList/Map/Set wraps a collection with runtime type checking on every insertion, catching heap pollution at add time instead of get time.
  • Runtime type enforcement – every add(), set(), put() call checks instanceof against the declared element type; wrong types throw ClassCastException immediately
  • Catches pollution at source – without the wrapper, a wrong type sneaks in silently and causes ClassCastException later at retrieval; with the wrapper, the exception occurs at insertion with a clear stack trace
  • Three variants – checkedList(list, elementType), checkedSet(set, elementType), checkedMap(map, keyType, valueType)
  • Performance – one instanceof check per insertion; negligible overhead for most applications
  • Use case – debugging heap pollution, protecting collections passed to legacy raw-type code, and as a safety net during generics migration

At National Bank of India, a normal filing drawer (plain List) accepts anything through the back door (raw type reference). Someone files a number where a name should be. Months later, a clerk reads the drawer and finds the wrong item. The checked wrapper is like a security scanner at the drawer entrance: every item is scanned on filing. If the wrong type is inserted, the scanner rejects it immediately with an alarm that identifies exactly who tried to file the wrong item.

Understanding the Concepts

What does Collections.checkedList do?

Collections.checkedList wraps a List with a type-checking decorator. Every insertion operation (add, set, addAll) verifies that the element is an instance of the declared type. If the check fails, a ClassCastException is thrown immediately at the point of insertion.

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) {
        // WITHOUT checked wrapper: CCE at get time
        List<String> unsafe = new ArrayList<>();
        List rawRef = unsafe;
        rawRef.add(42); // Integer sneaks in silently

        try {
            String s = unsafe.get(0); // CCE HERE, far from the bug
        } catch (ClassCastException e) {
            System.out.println("Unsafe: CCE at get: " + e.getMessage());
        }

        // WITH checked wrapper: CCE at add time
        List<String> safe = Collections.checkedList(new ArrayList<>(), String.class);
        List rawSafe = safe;

        try {
            rawSafe.add(42); // CCE HERE, at insertion!
        } catch (ClassCastException e) {
            System.out.println("Safe: CCE at add: " + e.getMessage());
        }

        // Correct types pass through
        safe.add("SAV2001");
        safe.add("CUR3001");
        System.out.println("Safe list: " + safe);
    }
}
TEXT
Unsafe: CCE at get: class java.lang.Integer cannot be cast to class java.lang.String
Safe: CCE at add: Attempt to insert class java.lang.Integer element into collection with element type class java.lang.String
Safe list: [SAV2001, CUR3001]

How does checkedMap work?

checkedMap verifies both the key type and the value type on every put operation.

JAVA
package com.bank.demo;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class CheckedMapDemo {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Map<String, Integer> balances = Collections.checkedMap(
            new HashMap<>(), String.class, Integer.class
        );

        balances.put("SAV2001", 500000);
        balances.put("CUR3001", 1200000);
        System.out.println("Balances: " + balances);

        // Wrong key type
        Map rawRef = balances;
        try {
            rawRef.put(42, "wrong"); // CCE at put time
        } catch (ClassCastException e) {
            System.out.println("Wrong key: " + e.getMessage());
        }

        // Wrong value type
        try {
            rawRef.put("FD4001", "not-a-number"); // CCE at put time
        } catch (ClassCastException e) {
            System.out.println("Wrong value: " + e.getMessage());
        }
    }
}
TEXT
Balances: {SAV2001=500000, CUR3001=1200000}
Wrong key: Attempt to insert class java.lang.Integer key into collection with key type class java.lang.String
Wrong value: Attempt to insert class java.lang.String value into collection with value type class java.lang.Integer

When should you use checked collections?

Checked collections are most valuable in three scenarios.

  • Legacy code integration. When passing a typed collection to a legacy API that uses raw types, wrap it first to catch any type violations immediately.
  • Debugging heap pollution. When a mysterious ClassCastException occurs at retrieval time, wrap the collection at creation to find where the wrong type is inserted.
  • API boundaries. When exposing a collection to third-party code or plugins, wrap it to protect against accidental type violations.
JAVA
package com.bank.demo;

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

public class LegacyIntegrationDemo {

    // Simulates a legacy API that uses raw types
    @SuppressWarnings("unchecked")
    static void legacyProcess(List accounts) {
        accounts.add("SAV2001");
        accounts.add(42); // bug in legacy code!
    }

    public static void main(String[] args) {
        List<String> accounts = Collections.checkedList(new ArrayList<>(), String.class);

        try {
            legacyProcess(accounts); // CCE on the add(42) call
        } catch (ClassCastException e) {
            System.out.println("Caught at legacy boundary: " + e.getMessage());
        }

        System.out.println("Accounts: " + accounts);
    }
}
TEXT
Caught at legacy boundary: Attempt to insert class java.lang.Integer element into collection with element type class java.lang.String
Accounts: [SAV2001]

What are common mistakes?

Mistake: Wrapping after pollution has already occurred

JAVA
// WRONG: wrap after data is already polluted
List<String> list = new ArrayList<>();
List rawRef = list;
rawRef.add(42); // pollution already happened
List<String> checked = Collections.checkedList(list, String.class);
// The 42 is already inside! Checked wrapper only protects future adds.
JAVA
// CORRECT: wrap at creation, before any data enters
List<String> checked = Collections.checkedList(new ArrayList<>(), String.class);
checked.add("SAV2001"); // type-checked

Mistake: Assuming the checked wrapper protects the underlying collection

JAVA
// WRONG: accessing the original bypasses the wrapper
List<String> original = new ArrayList<>();
List<String> checked = Collections.checkedList(original, String.class);
List rawOriginal = original;
rawOriginal.add(42); // bypasses the checked wrapper!
System.out.println(checked.get(1)); // CCE at get!
JAVA
// CORRECT: never retain a reference to the underlying collection
List<String> checked = Collections.checkedList(new ArrayList<>(), String.class);
// Only use 'checked', never the underlying list directly

How is this different from generics?

| Aspect | Generics | Checked collections |
|---|---|---|
| When checked | Compile time | Runtime |
| Detection of wrong type | COMPILE ERROR | ClassCastException at add |
| Works with raw types | No (bypassed) | Yes (catches at runtime) |
| Performance cost | None | One instanceof per add |
| Use case | All new code | Legacy integration, debugging |

What should a senior engineer know?

  • checkedNavigableMap and checkedNavigableSet. Java 8 added checked wrappers for NavigableMap and NavigableSet.
  • No generic array enforcement. Checked collections cannot prevent generic array creation issues (new List[]) since arrays are reified differently.
  • SortedMap/SortedSet variants. Collections.checkedSortedMap and checkedSortedSet maintain sorted semantics while adding type checks.
  • Not a replacement for generics. Checked collections add a runtime safety net but are not a substitute for proper generic type usage. They are a debugging and migration tool.

What are common interview follow-up questions?

  • "What does Collections.checkedList do?" : It wraps a list with runtime type checking on every insertion, throwing ClassCastException at add time instead of get time.
  • "When would you use it?" : When passing collections to legacy raw-type code, debugging heap pollution, or protecting API boundaries.
  • "Does it have a performance cost?" : Yes, one instanceof check per insertion, but it is negligible for most use cases.
  • "Does it protect against heap pollution through the original reference?" : No. You must not retain a reference to the underlying collection.

Visual Overview

DIAGRAM
  Without Checked Wrapper            With Checked Wrapper
  =======================            =====================

  List<String> list = new ArrayList  List<String> list = checkedList(
  List raw = list                       new ArrayList<>(), String.class)
  raw.add(42) -> silent!             List raw = list
                                     raw.add(42) -> CCE at add!
  list.get(0)                        ┌───────────────────────────┐
  ┌──────────────────────┐           │ Scanner at the entrance:  │
  │ (String) list.get(0) │           │ instanceof String? NO     │
  │ 42 is not String     │           │ -> ClassCastException!    │
  │ -> CCE at GET!       │           │                           │
  │                      │           │ Stack trace points to     │
  │ Stack trace points   │           │ the EXACT add() call      │
  │ to get(), not add()  │           └───────────────────────────┘
  └──────────────────────┘

Key Takeaway

Collections.checkedList, checkedMap, and checkedSet wrap collections with runtime type checking on every insertion operation. Without the wrapper, heap pollution from raw types causes ClassCastException at retrieval time, with a stack trace pointing to the innocent get() call instead of the buggy add(). With the wrapper, the exception occurs immediately at insertion, with a clear message identifying the wrong type and a stack trace pointing directly to the offending code.

The checked wrapper is not a replacement for generics. Generics catch type errors at compile time, which is always better. Checked collections are a runtime safety net for situations where generics cannot help: legacy code that uses raw types, third-party APIs that bypass type parameters, and debugging sessions where you need to find the source of mysterious ClassCastExceptions.

At National Bank of India, Rajesh wrapped all collections passed to the legacy payment gateway module with Collections.checkedList. When the gateway's raw-type code tried to insert an Integer where a String was expected, the checked wrapper caught it immediately. Without the wrapper, the error would have appeared hours later in the reporting module, three layers removed from the source.

Remember one sentence: Collections.checkedList adds a runtime type scanner on every insertion, catching heap pollution at add time instead of get time, which is essential when integrating with legacy raw-type code.

---

Share
Use ← → arrow keys to navigate