skip to content

What is the difference between List<?>, the raw type List, and List<Object>, especially regarding type safety and what you can put in them?

level: middleimportance: should knowfreq 50%

answer

  1. List<?> = unknown type, add only null, read as Object
  2. Raw List = no checking, unsafe, legacy only
  3. List<Object> = element type is Object, freely add
  4. List<String> assignable to List<?> but NOT List<Object>
  5. Invariance blocks List<String> -> List<Object>

basics

~20 s

List<?> is a list of some unknown type and is type-safe — you can't add anything but null. Raw List has no type checking and lets you add anything (unsafe, legacy). List<Object> is a list that explicitly holds any Object and lets you add anything safely.

solid answer

~40 s

List<?> (unbounded wildcard) means 'a list of some specific but unknown element type.' It is type-safe: because the element type is unknown, the compiler lets you read elements as Object but only lets you add null. The raw type List (no type argument) exists for backward compatibility with pre-generics code; it disables generic type checking entirely, so you can add any object and you get unchecked-warning'd, unsafe code that can blow up with ClassCastException later. List<Object> is a fully specified type whose element type really is Object, so you can add any object safely and read elements as Object. The key distinctions: List<?> is safe but read-mostly; raw List is unsafe and should be avoided; List<Object> is safe and freely mutable but is invariant, so a List<String> is not assignable to it.

go deeper

for a junior

Knows raw types are discouraged and that List<?> exists; may blur the distinctions.

for a middle

Clearly separates all three: List<?> safe/read-mostly, raw unsafe, List<Object> safe/invariant, with the add/assign rules.

for a senior

Explains the soundness reasons (invariance, unchecked deferral) and chooses the right one per API contract.

for a principal

Sets team conventions (ban raw types, prefer wildcards on consumer APIs), and reasons about migration compatibility and erasure-driven warnings at scale.

## Three things that look similar - **`List<?>`** — an *unbounded wildcard*: "a list of some single, unknown element type." - **`List`** — the *raw type*: the generic class used with no type argument at all. - **`List<Object>`** — a fully parameterized type whose element type is exactly `Object`. They are easy to confuse but behave very differently. ## `List<?>` — safe, read-mostly The element type is unknown, so the compiler enforces safety conservatively. You can: - read elements, but only as `Object` (that's the only type guaranteed); - add **only `null`** (no other value is provably the right type). Use `List<?>` when you genuinely don't care about the element type — e.g. a method that only needs `size()` or `clear()`. A `List<String>` *is* assignable to a `List<?>` reference, because any specific list is "a list of some type." ## Raw `List` — legacy, unsafe The raw type predates generics and exists only for migration compatibility. Using it **turns off** generic type checking: you can `add` anything, and the compiler only emits *unchecked* warnings. This lets bad values slip in and surface as a `ClassCastException` at some unrelated later point. Rule: **never use raw types in new code.** `List<?>` gives you the "I don't know the element type" capability *without* sacrificing safety. ## `List<Object>` — safe, freely mutable, but invariant Here the element type really is `Object`. You can add any object (all are Objects) and read elements as `Object`. The catch is **invariance**: `List<Object>` is a distinct type from `List<String>`, and you **cannot** pass a `List<String>` where a `List<Object>` is required (that would let you insert a non-String into the String list). By contrast you *can* pass a `List<String>` where a `List<?>` is required. ## Quick comparison | | accepts List<String>? | add arbitrary object? | type-safe? | |---|---|---|---| | `List<?>` | yes | no (only null) | yes | | raw `List` | yes | yes | no (unchecked) | | `List<Object>` | no | yes | yes | ## Bottom line - Want "any list, I won't mutate it" → `List<?>` (safe). - Want "a list that holds Objects, mutable" → `List<Object>` (safe, but invariant). - Never reach for raw `List` — it only buys you unchecked, brittle code.

  • Can you pass a List<String> to a method taking List<Object>?
    No. Generics are invariant, so List<String> is not a subtype of List<Object>. You'd use List<?> or List<? extends Object> for that.
  • Why does raw List produce 'unchecked' warnings?
    Because the compiler cannot verify that operations respect the intended type argument; the safety check is deferred and may fail at runtime as a ClassCastException.

saying these in an interview costs you the question

  • Treating raw List and List<?> as the same
  • Thinking you can add any object to List<?>
  • Believing List<String> is a List<Object>
  • Using raw types in new code
  • Saying List<?> is unsafe

context