Raw Types

A raw type is a generic type used with no type argument at all — legal for backward compatibility, but it disables the compile-time type checking generics exist to provide.

A raw type is the name of a generic class or interface used without any type argument, such as List instead of List<String>. Raw types are legal Java, kept entirely for backward compatibility with code written before generics existed in Java 5, but using one in new code throws away the type safety generics were designed to provide.

What Is a Raw Type

Any generic type can be used "raw" by simply omitting the angle brackets and type argument entirely.

The raw List accepts absolutely any type of element because there is no type argument for the compiler to check insertions against.

List<String> parameterized = new ArrayList<>(); // preferred
List raw = new ArrayList();                       // raw type — legal but discouraged

raw.add("hello");
raw.add(42);        // compiles! no type checking at all on a raw type
raw.add(3.14);       // also compiles

Why Raw Types Exist: Legacy Compatibility

When generics were introduced in Java 5, an enormous amount of existing compiled and source code used collections and other generic-ready classes without any type parameter. Java's designers chose to keep raw types legal (rather than making them a compile error) specifically so that old code would keep compiling and old and new code could still interoperate, at the cost of allowing this loophole to remain available in new code too.

Raw types are a historical compatibility mechanism, not a recommended feature. Every style guide and the JDK's own compiler warnings treat raw type usage in new code as something to avoid.

Unchecked Warnings

The compiler still compiles code using raw types (it must, for compatibility), but it emits "unchecked" warnings at points where it can no longer verify type safety — most visibly when mixing a raw type with generic code, or when assigning a raw type's result into a parameterized variable without a cast.

Assigning a parameterized reference to a raw one, then adding the wrong type through the raw reference, defeats the type safety the parameterized declaration was trying to guarantee — the failure surfaces later as a runtime ClassCastException, exactly like pre-generics code.

List<String> list = new ArrayList<>();
List raw = list;              // OK: parameterized -> raw is always allowed (widening)
raw.add(42);                  // unchecked: compiles, but corrupts "list" logically

// Later, iterating "list" as if it only contains Strings:
for (String s : list) {       // throws ClassCastException at runtime on the Integer
    System.out.println(s);
}

Raw Types vs. Unbounded Wildcards

List (raw) and List<?> (unbounded wildcard) look similar but behave very differently. The wildcard form still enforces that all reads produce a consistent (if unknown) type and still forbids most writes; the raw form disables checking entirely, both for reads and writes.

Prefer List<?> over a raw List whenever you need to accept "a list of some unknown type" — it retains meaningful compile-time checks that a raw type discards entirely.

List raw = new ArrayList();
raw.add("x");
raw.add(1);              // raw: compiles, no error at all

List<?> wildcard = new ArrayList<String>();
// wildcard.add("x");    // does not compile: ? forbids add() even for a String
Object o = wildcard.get(0); // OK: reading is always allowed, as Object

How Raw Types Interact with Generic Methods

Calling a generic method through a raw type reference erases the method's generic signature too, silently converting its type parameters to raw/erased forms and losing type checking on that call as well.

class Box<T> {
    T value;
    void set(T value) { this.value = value; }
    T get() { return value; }
}

Box raw = new Box();
raw.set("hello");   // "unchecked call" warning — set(Object) effectively
raw.set(42);         // also compiles — no type checking at all
Object o = raw.get(); // return type is erased to Object too

Why to Avoid Raw Types in New Code

Using a raw type reintroduces exactly the failure mode generics were built to eliminate: a type mismatch that compiles silently and only fails much later, as a ClassCastException, at a location often far removed from the actual mistake.

List raw = new ArrayList();
raw.add("valid");
raw.add(99);                       // mistake, but compiles fine

// ... much later, possibly in a different method or file ...
for (Object o : raw) {
    String s = (String) o;         // ClassCastException here, far from the actual bug
}

Where Raw Types Legitimately Still Appear

A handful of niche cases still legitimately use raw types today: interacting with old pre-generics libraries that were never updated, and certain reflection-heavy code where the type argument genuinely cannot be known statically (e.g. working with a Class object obtained dynamically).

// Legitimate, narrow use case: dynamic class loading where T is unknown until runtime
Class rawClass = Class.forName(someClassName); // Class<?> is usually still preferable

Practical Usage: Migrating Away from Raw Types

When maintaining old code that uses raw types, the standard fix is to add the appropriate type argument (inferring it from how the collection is actually used) and let the compiler surface any real type mismatches that the raw type had been hiding.

// Before:
List names = new ArrayList();
names.add("Alice");
names.add("Bob");

// After migrating:
List<String> names2 = new ArrayList<>();
names2.add("Alice");
names2.add("Bob");
// names2.add(42); // now the compiler would catch this mistake immediately

Edge Cases

A raw type can silently "contaminate" otherwise well-typed code: assigning a parameterized reference to a raw variable and back can slip past the compiler with only warnings, not errors, so a codebase mixing raw and parameterized usage can have its type safety guarantees quietly broken in ways the compiler will warn about but not stop.

Passing a List<String> to a method that declares a raw List parameter is legal (parameterized types can always be used where a raw type is expected), but it lets that method corrupt the list's contents without the caller's knowledge.

static void corrupt(List raw) {
    raw.add(999); // unchecked, compiles
}

List<String> strings = new ArrayList<>();
strings.add("safe");
corrupt(strings);           // legal: List<String> is passed to a raw List parameter
String s = strings.get(1);  // compiles, but throws ClassCastException at runtime

Mental Model

Think of a raw type as "turning off" the compiler's generics type-checker for that one variable or reference, exactly the way things worked before Java 5. It's still valid, compilable Java — it just forfeits the safety net generics exist to provide, which is why virtually all new code should use a parameterized type or, where genuinely appropriate, an unbounded wildcard instead.

Quick Reference

List<String> parameterized = new ArrayList<>(); // preferred: type-checked
List<?>       wildcard      = new ArrayList<String>(); // "some unknown type," still checked
List          raw           = new ArrayList();   // legacy only: no checking at all

Raw type allowed for backward compatibility only.
Compiler emits "unchecked" warnings around raw-type usage.
Prefer List<?> over raw List whenever the element type is genuinely unknown.

Key Takeaways

A raw type is a generic type used with no type argument, e.g. List instead of List<String>.

Raw types exist purely for backward compatibility with pre-Java-5 code; they remain legal but should not be used in new code.

Using a raw type disables compile-time type checking for that usage, reintroducing the risk of a runtime ClassCastException far from the actual mistake.

A raw type is different from an unbounded wildcard (List<?>): the wildcard still enforces meaningful, if limited, compile-time checks, while the raw type disables checking entirely.

The compiler emits "unchecked" warnings around raw-type usage; treat these warnings as signals to migrate the code to a properly parameterized type.

List names = new ArrayList(); // raw type — avoid in new code names.add("Alice"); names.add(42); // compiles, but a bug

List<String> names = new ArrayList<>(); names.add("Alice"); // names.add(42); // compile-time error — caught immediately

A raw type accepts any element type with no compile-time checking; declaring the intended type argument catches the mistake immediately instead of letting it surface later as a runtime ClassCastException.

List<String> strings = new ArrayList<>(); List raw = strings; // legal, but dangerous raw.add(123); // corrupts strings without warning at this line String s = strings.get(0); // ClassCastException here, far from the real bug

List<String> strings = new ArrayList<>(); // keep all references parameterized — never "downgrade" to a raw reference strings.add("safe");

Assigning a parameterized reference into a raw-typed variable is legal but discards type checking for anything done through that raw reference, letting bad data silently corrupt the original parameterized collection.

// treating "List raw" and "List<?> wildcard" as equivalent List raw = new ArrayList(); raw.add("this compiles with a raw type");

List<?> wildcard = new ArrayList<String>(); // wildcard.add("this would NOT compile"); — the wildcard still enforces safety

A raw type and an unbounded wildcard are not interchangeable: the wildcard still forbids unsafe writes at compile time, while a raw type allows anything — prefer the wildcard whenever the intent is genuinely "some unknown type," reserving raw types for legacy interoperability only.

What is a raw type, and give an example.

A raw type is a generic type used without any type argument, e.g. writing List instead of List<String>. List raw = new ArrayList(); is a raw-type declaration.

Why do raw types still exist in Java at all if they're discouraged?

They exist purely for backward compatibility, so that code written before generics were introduced in Java 5 continues to compile, and so old and new code can still interoperate.

What happens to compile-time type checking when you use a raw type?

It is disabled entirely for that usage — any type of element can be added, and any generic method call through the raw reference loses its generic type checking as well, producing "unchecked" compiler warnings instead of errors.

How does List (raw) differ in behavior from List<?> (unbounded wildcard)?

List<?> still enforces that the list holds some single, consistent (if unknown) type — it forbids most add() calls and only allows reading as Object. A raw List disables checking entirely: any element type can be added with no compile-time restriction at all.

What kind of bug does using a raw type reintroduce that generics were designed to eliminate?

A type mismatch that compiles without error and only surfaces later, at runtime, as a ClassCastException — often at a location far removed from where the wrong-typed element was actually inserted.

Interview: Is it legal to pass a List<String> argument to a method parameter declared as a raw List? What risk does this create?

Yes, it's legal — a parameterized type can always be used where a raw type is expected. The risk is that the method, working through its raw List parameter, can add elements of any type into the caller's originally well-typed List<String>, silently corrupting it in a way the caller has no compile-time warning about.

Interview: If raw types disable all compile-time checking, why doesn't the compiler simply refuse to compile them?

Refusing raw types outright would have broken compilation of every pre-Java-5 codebase and any code that still interoperates with un-migrated legacy APIs; Java's generics design deliberately prioritized backward/migration compatibility, allowing raw types to remain legal (with unchecked warnings) rather than becoming compile errors.

Rewrite this raw-type code to use a properly parameterized type, and explain what new compile-time safety it gains.

List<Integer> scores = new ArrayList<>(); scores.add(90); scores.add(85); // scores.add("bonus"); // now a compile-time error

Declaring List<Integer> instead of the raw List lets the compiler reject the add("bonus") call immediately, catching the type mismatch at compile time rather than letting it slip through and fail later as a runtime ClassCastException wherever the list is eventually used as all-Integer.

What happens when this code runs?

It throws a ClassCastException. Assigning typed into the raw variable raw and adding an Integer (100) through it compiles (with an unchecked warning) and corrupts the underlying list. The for-each loop over typed as String then fails at runtime trying to treat the Integer 100 as a String.

Write a small generic class Box<T> and demonstrate, in code, the difference in what compiles between using it raw versus using it parameterized.

class Box<T> { private T value; void set(T value) { this.value = value; } T get() { return value; } } // Parameterized: type-checked Box<String> typedBox = new Box<>(); typedBox.set("hello"); // typedBox.set(42); // compile-time error // Raw: no checking Box rawBox = new Box(); rawBox.set("hello"); rawBox.set(42); // compiles fine, no error at all

The parameterized Box<String> rejects set(42) at compile time, while the raw Box accepts any type for set(), with the compiler emitting only an unchecked warning — demonstrating exactly how much safety is lost by using the raw form.

Does this code compile and run without error? What warning, if any, would a compiler typically emit?

Yes, it compiles and runs fine, printing 2 — nothing here actually violates type consistency (both elements happen to be Strings). However, a compiler typically still emits an "unchecked" warning (rawtypes / unchecked operation) at the new ArrayList() and add() calls, because raw-type usage disables the type-safety checks generics normally provide, even though this particular usage doesn't happen to trigger a real bug.

List scores = new ArrayList();
scores.add(90);
scores.add(85);
scores.add("bonus"); // bug: not an int
List<String> typed = new ArrayList<>();
typed.add("a");

List raw = typed;
raw.add(100);

for (String s : typed) {
    System.out.println(s);
}
List rawList = new ArrayList();
rawList.add("one");
rawList.add("two");
System.out.println(rawList.size());