Definition
- Runtime type enforcement – every
add(),set(),put()call checksinstanceofagainst 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
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.
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);
}
}
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.
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());
}
}
}
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.
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);
}
}
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
// 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.
// 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
// 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!
// 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.checkedSortedMapandcheckedSortedSetmaintain 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
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.
---