skip to content

Favor Generics & Type Safety

Keeping the compiler's guarantees intact: no raw types, no unaddressed unchecked warnings, generic methods, and bounded wildcards following PECS. Interviewers use PECS specifically to see whether you can design an API that accepts the widest useful set of types.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

What is a raw type in Java, why should you avoid raw types, and what do you lose by using them?

level: juniorimportance: must knowfreq 70%

answer

  1. Raw = generic with no <type> filled in
  2. Exists only for pre-Java-5 backward compatibility
  3. Turns OFF compile-time checking -> ClassCastException at runtime
  4. List<?> is the SAFE 'I don't care' alternative
  5. Triggers unchecked warnings

basics

~20 s

A raw type is a generic class used without its type parameter, like List instead of List<String>. Avoid them because you lose compile-time type checking, so wrong types slip in and blow up at runtime with a ClassCastException.

solid answer

~40 s

A raw type is a generic type used without supplying its type argument, e.g. `List` instead of `List<String>`. Raw types exist only for backward compatibility with pre-Java-5 code. Using one opts out of generics: the compiler stops checking element types, so you can put any object into the collection and the error only surfaces as a ClassCastException when you read and cast it out. You also get unchecked warnings. Prefer a parameterized type for full safety, or `List<?>` (an unbounded wildcard) when you genuinely don't care about the element type but still want the compiler to forbid inserting anything except null. The rule: never use raw types in new code.

code

java · 11 lines
java
// Raw type: compiles, then explodes at runtime
List raw = new ArrayList();
raw.add("hi");
raw.add(42);                       // no complaint from the compiler
String s = (String) raw.get(1);    // ClassCastException

// Parameterized: the bug is caught at compile time
List<String> safe = new ArrayList<>();
safe.add("hi");
// safe.add(42);                   // COMPILE ERROR — fixed at the source
String t = safe.get(0);            // no cast needed

go deeper

for a junior

Recognize that List is raw and List<String> is parameterized, and that the raw one loses compile-time checking leading to a runtime ClassCastException.

for a middle

Explain raw types exist for backward compatibility, articulate the runtime-vs-compile-time error shift, and distinguish raw vs List<Object> vs List<?>.

for a senior

Reason about when raw types are still unavoidable (class literals, instanceof) due to erasure, and choose the unbounded wildcard as the safe substitute in APIs.

for a principal

Discuss the language-design trade-off of retrofitting generics via erasure, the migration-compatibility motivation, and how raw-type usage erodes the team's type-safety guarantees at scale.

## What "generics" are A **generic type** is a class or interface that takes one or more **type parameters** — placeholders for a type you fill in later. `List<E>` is the generic interface; `E` is its type parameter. When you write `List<String>`, you supply the **type argument** `String`, producing a **parameterized type**. The benefit is **compile-time type safety**: the compiler knows the list holds only `String`s, rejects anything else, and inserts the casts for you so you never write them. ## What a raw type is A **raw type** is the name of a generic type used with *no* type argument at all: just `List`, `Map`, `Comparable`. Raw types exist for one reason — **backward compatibility**. Generics arrived in Java 5 (2004); huge amounts of code already used `List`. To let old and new code interoperate, the language kept raw types legal. ## What you lose Using a raw type **silently turns off generics for that variable**. Consider: ```java List raw = new ArrayList(); // raw type raw.add("hello"); raw.add(42); // compiler allows it! String s = (String) raw.get(1); // ClassCastException at RUNTIME ``` With a raw type the compiler no longer tracks element types, so `add(42)` compiles. The mistake stays hidden until that line runs, far from where the bug was introduced. With `List<String>`, `raw.add(42)` would be a **compile error** — caught immediately, at the source. The whole point of generics is to move errors from runtime to compile time, and raw types throw that away. You also get **"unchecked" warnings** from the compiler whenever you mix raw and parameterized code — its way of saying "I can't guarantee type safety here." ## Raw type vs `List<Object>` vs `List<?>` These are three different things, often confused: - **`List` (raw)** — no checking; unsafe; legal only for compatibility. - **`List<Object>`** — explicitly "a list that holds any object." Still type-checked: you cannot pass a `List<String>` where a `List<Object>` is expected (they aren't subtypes — generics are *invariant*). - **`List<?>` (unbounded wildcard)** — "a list of some unknown type." Type-safe: you can read elements as `Object`, but the compiler forbids `add`ing anything except `null`, because it can't know the actual element type. This is the safe replacement when you truly don't care about the type parameter. ## The takeaway Never use raw types in new code. Reach for a parameterized type to get full checking, or an unbounded wildcard (`<?>`) when the element type is irrelevant but you still want safety. The only place you'll legitimately still see raw types is class literals (`List.class`, not `List<String>.class`, which is illegal) and `instanceof` (`o instanceof List`), because generics are erased at runtime.

  • If raw types are unsafe, why didn't Java just remove them?
    Backward compatibility. Generics were retrofitted in Java 5 onto a language with millions of lines of existing collection code using `List`, `Map`, etc. Removing raw types would have broken all of it, so they stay legal (with warnings) to let old and new code interoperate.
  • What's the difference between List<Object> and List<?>?
    `List<Object>` is a concrete parameterized type holding any object; you can add any object to it. `List<?>` is a list of *unknown* type; you can read elements as Object but cannot add anything except null, because the compiler can't verify what the real element type is.

A raw type is like a labelled storage box where you peeled the label off. The box still works, but nothing stops you from dumping mismatched junk in; you only discover the mistake when you reach in expecting one thing and pull out another.

saying these in an interview costs you the question

  • Saying raw types and List<Object> are the same thing — they're not; List<Object> is still type-checked.
  • Claiming a raw type causes a compile error — it compiles fine (with a warning); the failure is at runtime.
  • Thinking you can freely add elements to a List<?> — you can only add null.
  • Believing generics survive at runtime so a raw type 'remembers' its element type — erasure removes it.

context

open as a page

Explain bounded wildcards and the PECS rule. When do you use `? extends T` versus `? super T`?

level: seniorimportance: must knowfreq 60%

basics

~20 s

PECS means 'Producer Extends, Consumer Super.' If a parameter only produces (you read from) T values, use ? extends T. If it only consumes (you write into it) T values, use ? super T. This makes APIs flexible while staying type-safe.

open as a page

What is a generic method, how is it different from a method in a generic class, and why prefer one over casting?

level: middleimportance: should knowfreq 55%

basics

~20 s

A generic method declares its own type parameter (in angle brackets before the return type), so it works for many types while staying type-safe. It's better than casting because the compiler checks the types for you and you write no casts.

open as a page

What is type erasure, and what consequences and restrictions does it create for generic code (e.g. instanceof, generic arrays, overloading)?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Type erasure means the compiler removes generic type information after checking it, so at runtime a List<String> is just a List. Because of this you can't do obj instanceof List<String>, can't create new T[], and can't have two methods that differ only by their generic type argument.

open as a page

How should you handle unchecked warnings in Java, and what is the correct, safe use of @SuppressWarnings("unchecked")?

level: seniorimportance: should knowfreq 45%

basics

~20 s

An unchecked warning means the compiler can't guarantee a generic operation is type-safe. Eliminate every warning you can. If you've proven one is truly safe, suppress it with @SuppressWarnings("unchecked") on the smallest possible scope, and add a comment explaining why it's safe.

open as a page