☕ Java

Raw Types

A raw type in Java is the use of a generic class or interface without any type argument. Writing List instead of List<String>, or HashMap instead of HashMap<String, Integer>, or using the class name of a generic class as a type without angle brackets in any position — these are all raw types. Raw types exist solely for backward compatibility with pre-Java-5 code that predates generics; all generic type checking is disabled for a raw type and its members, and the compiler emits unchecked warnings wherever raw types interact with parameterized types. Understanding raw types is essential for reading legacy code, understanding the warnings the compiler emits, and grasping exactly what guarantee is lost when a raw type is used. This entry covers how raw types are defined and how they relate to the erased form of a generic type, the precise scope of type checking that is disabled for raw type members, the specific unchecked warnings that arise, the difference between raw types and wildcard types, and the migration compatibility rules that raw types enable.

What Raw Types Are and What Type Safety They Lose

A raw type is a generic type used without type arguments. For any generic class C<T>, the raw type is simply C — the class name standing alone. For a generic interface I<T, R>, the raw type is I. The raw type corresponds exactly to what the JVM sees after erasure: the erased class file. Using a raw type is explicitly reaching past the type system to speak the JVM's own language — accepting every element as Object, accepting every call as unchecked, and accepting full responsibility for the type invariants the compiler can no longer enforce. The loss of type safety is total for members accessed through a raw type. If you have a variable of type List (raw), then list.get(0) returns Object, not whatever element type was originally intended. list.add(anything) accepts any Object without warning for the add() call itself (though the assignment of the raw variable may already have emitted a warning). Iterator returned by list.iterator() is a raw Iterator with Object-returning next(). Every method on the raw type has its generic parameters replaced by their erasures — T becomes Object (or the first bound), and no generic checking is performed on arguments or return types. The crucial subtlety is that non-generic members of a generic class are not affected by raw access. A generic class may have static methods or instance methods that do not involve the class's type parameters; accessing these through a raw type variable is perfectly safe and no warnings are emitted. The raw type only disables type checking for members that actually involve the type parameter. This distinction matters when interacting with legacy code that uses raw types but calls only non-generic methods. The compiler emits two categories of warning related to raw types. "Raw type" warnings are emitted when a raw type is used where a parameterized type should be — such as List instead of List<String> in a variable declaration. "Unchecked" warnings are emitted when a raw type interacts with a parameterized type in a way that could introduce heap pollution — such as assigning a raw List to a List<String> variable, or passing a raw List to a method that expects List<String>. The two categories are distinct: raw type warnings fire on the declaration, unchecked warnings fire on the interaction.
Java
// ── Raw type declarations — what the compiler sees ──────────────────
List          rawList   = new ArrayList();       // raw type — WARNING: "raw types"
List<String>  typedList = new ArrayList<>();     // parameterized — no warning

// ── Member access through a raw type — all generics disabled ─────────
rawList.add("hello");   // accepts anything — no type checking on argument
rawList.add(42);        // Integer added to same list — compiler doesn't object
rawList.add(3.14);      // Double too — anything goes

Object first = rawList.get(0);   // get() returns Object, not String
// String s = rawList.get(0);    // COMPILE ERROR — Object cannot be assigned to String
// ... but this only fails at compile time if you try to use the typed return.
// If you cast: String s = (String) rawList.get(0);  — compiles, explodes at runtime.

// ── Non-generic members are NOT affected ─────────────────────────────
// size(), isEmpty(), clear() don't involve T — accessible safely through raw type:
List raw = new ArrayList(List.of("a", "b", "c"));
int size  = raw.size();     // OK — not generic, no warning
boolean mt = raw.isEmpty(); // OK — not generic, no warning
raw.clear();                // OK — not generic, no warning

// ── Two warning categories: raw type vs unchecked ─────────────────────
// Category 1"raw type" warning: using raw type in declaration
List rawDecl = new ArrayList();   // WARNING: List is a raw type

// Category 2"unchecked" warning: raw meets parameterized
List          raw2 = new ArrayList();
List<String>  typed;
typed = raw2;   // WARNING: "unchecked assignment" — raw List assigned to List<String>

// Adding via the raw reference to a typed list:
List<String> safe = new ArrayList<>();
List         dangerous = safe;          // raw alias — WARNING: unchecked
dangerous.add(Integer.valueOf(99));     // compiler says nothing here
String s = safe.get(0);                // ClassCastException at runtime — heap polluted

// ── Raw Iterator — generics disabled on returned generic types too ────
List<String> strings = new ArrayList<>(List.of("a", "b", "c"));
Iterator rawIter = strings.iterator();   // raw Iterator, even though strings is typed
Object element = rawIter.next();         // returns Object, not String
// String str = rawIter.next();          // COMPILE ERROR — raw Iterator.next() returns Object

Raw Types vs Wildcard Types, and Migration Compatibility

Raw types are frequently confused with unbounded wildcard types, but they represent different things with different type-safety properties. List<?> (unbounded wildcard) is a parameterized type that means "a List of some specific but unknown type." List (raw type) means "a List where we have abandoned all type checking." The difference manifests in what the compiler allows. With a List<?>, you cannot add anything except null — the compiler knows there is some element type but doesn't know what it is, so any add() other than null could violate the actual element type and is rejected. With a raw List, you can add anything — the compiler has disabled all generic checking. With a List<?>, get() returns Object because the element type is unknown but bounded; with a raw List, get() returns Object because the type parameter has been erased entirely. Both look similar to callers but differ in the guarantees they offer. Use List<?> when you need to work with a collection of unknown type in a controlled way; never use raw List in new code. The same distinction applies to Class: Class<?> means "some class whose type is unknown" while Class (raw) means "a class with no type checking." Class<?> is the correct wildcard form to use when a method returns a class of unknown type. Class<T> is the correct parameterized form to use when T is available. The raw Class was the only option before Java 5 and remains in method signatures in older APIs such as Class.forName() (which returns Class<?>, the modernized form). Migration compatibility was the primary reason raw types were retained in the language. When generics were introduced in Java 5, the designers needed a way for pre-existing code (which had no type arguments) to interoperate with new generic code without requiring a complete recompilation of all existing libraries. A method that accepts List (raw) can be called with a List<String> (parameterized) — this is called an unchecked assignment and emits a warning. A method that returns List (raw) can have its result assigned to a List<String> variable — also an unchecked assignment with a warning. These unsafe interactions are allowed to exist, warned about but not prohibited, specifically to enable incremental migration of large codebases. In modern Java, raw types should never appear in new code. The only legitimate contexts for raw types are: reflection-related operations where Class<?> or Class<T> is unavoidable or where the API predates generics, working with truly ancient third-party libraries that have never been updated, and understanding compiler warnings when maintaining legacy code. In all other cases, the unbounded wildcard ? or an appropriate type parameter is the correct substitute for a raw type.
Java
// ── Raw type vs wildcard — what each allows ─────────────────────────
List<String> strings = new ArrayList<>(List.of("a", "b", "c"));

List<?>  wildcard = strings;   // unbounded wildcard
List     raw      = strings;   // raw type

// Writing: wildcard REJECTS adds (except null); raw ACCEPTS anything
// wildcard.add("x");          // COMPILE ERROR — cannot add to List<?>
// wildcard.add(null);         // null is the only add that's legal
raw.add("x");                  // compiles — heap pollution added
raw.add(42);                   // compiles — now List<String> has an Integer in it!

// Reading: both return Object
Object wo = wildcard.get(0);   // Object — element type is unknown
Object ro = raw.get(0);        // Object — type parameter erased

// ── Class raw type vs Class<?> vs Class<T> ───────────────────────────
// Old API (returns raw Class — deprecated pattern):
// Class c = obj.getClass();   // raw — type checking disabled

// Modern form — unknown but type-safe:
Class<?> c = obj.getClass();    // Class<? extends Object> — wildcard

// Typed form — when T is known:
Class<String> stringClass = String.class;  // Class<String> — fully typed

// The difference in usage:
Class<?>       unknown = String.class;
// String s = unknown.newInstance();   // returns Object, needs cast
// Class<String> typed = unknown;      // COMPILE ERROR — Class<?> not assignable to Class<String>

Class<String> typed = String.class;
String s = typed.getDeclaredConstructor().newInstance();  // returns String — no cast

// ── Migration compatibility — raw accepting parameterized ─────────────
// Legacy method written before generics:
static void legacySort(List list, Comparator comp) {
    Collections.sort(list, comp);  // raw types — no generic checking
}

List<Integer> ints = new ArrayList<>(List.of(3, 1, 4, 1, 5));
// Calling legacy method with parameterized types — unchecked but allowed:
legacySort(ints, Comparator.naturalOrder());    // WARNING: unchecked call — compiles and works
System.out.println(ints);   // [1, 1, 3, 4, 5]

// Calling new generic method with raw type — also allowed but warned:
static <T> void modernSort(List<T> list, Comparator<? super T> comp) {
    list.sort(comp);
}
List rawInts = new ArrayList(List.of(3, 1, 4));
// modernSort(rawInts, Comparator.naturalOrder()); // WARNING: unchecked — allowed for migration

// ── The correct substitutes for raw types in modern code ──────────────
// NEVER write:         ALWAYS write:
// List                 List<?> or List<MyType>
// Map                  Map<?, ?> or Map<K, V>
// Class                Class<?> or Class<T>
// Comparable           Comparable<?> or Comparable<MyType>
// Iterator             Iterator<?> or Iterator<MyType>

// When you truly need to read from an unknown-type collection:
public static void printAll(List<?> list) {      // wildcard, not raw
    for (Object item : list) {
        System.out.println(item);
    }
}
printAll(List.of("a", "b", "c"));   // works
printAll(List.of(1, 2, 3));         // works
// No unchecked warnings — List<?> is the correct form

Unchecked Warnings In Depth, @SuppressWarnings, and Raw Types in Reflection

Unchecked warnings are the compiler's mechanism for flagging every point where the static type system cannot verify that a generic type constraint holds. They are not errors — the code compiles and runs — but they indicate locations where a ClassCastException could occur at runtime with no further warning. Understanding exactly which operations produce unchecked warnings, and why, allows you to reason precisely about which casts are safe and which represent latent bugs. Unchecked assignment: assigning a raw type to a parameterized type (List raw assigned to List<String>) is unchecked because the raw list might contain non-String elements. Unchecked call: calling a method on a raw type or passing a raw type to a generic method erases all type checking for that call. Unchecked cast: explicitly casting to a parameterized type (List<String>) from a less specific type (Object or raw List) is unchecked because the runtime cannot verify the parameterization. Unchecked conversion: similar to unchecked assignment, occurs when a raw type is used in a context requiring a parameterized type. The -Xlint:unchecked compiler flag enables verbose output about unchecked warnings, including the specific operation that triggered each one. The javac default suppresses most of these details; -Xlint:unchecked shows each location and the reason. This is the tool for auditing legacy code during a migration to generics. @SuppressWarnings("unchecked") suppresses unchecked warnings for the annotated declaration. Its correct use requires human verification that the suppressed cast or assignment is actually safe — that the type invariant the compiler cannot check is in fact maintained by surrounding code. The annotation should always be applied at the narrowest possible scope (a single method or variable, never an entire class) and accompanied by a comment explaining why the cast is safe. Applying it globally or without reasoning produces code that compiles cleanly but may carry latent ClassCastExceptions. Reflection APIs return raw types extensively because they operate at a level of abstraction below generics: Class.forName(), Class.getDeclaredMethods(), Method.getReturnType(), Field.getType() — these return raw Class and Class[] because the types are not known at compile time. The modern equivalents (getGenericReturnType(), getGenericType()) return Type objects that encode generic information, but retrieving the class from them still requires casting. This is a genuine and unavoidable use of unchecked operations in reflection-heavy code, and @SuppressWarnings("unchecked") with a documenting comment is the correct and standard approach.
Java
// ── Four unchecked warning categories ───────────────────────────────

// 1. Unchecked assignment — raw to parameterized
List rawSource = new ArrayList(List.of("a", "b"));
List<String> typed = rawSource;     // WARNING: unchecked assignment
// No ClassCastException yet — just potential heap pollution

// 2. Unchecked call — calling generic method with raw type argument
public static <T> void reverse(List<T> list) { Collections.reverse(list); }
List rawArg = new ArrayList(List.of(1, 2, 3));
reverse(rawArg);   // WARNING: unchecked method invocation — T inferred as Object

// 3. Unchecked cast — explicit cast to parameterized type
Object obj = new ArrayList<>(List.of("x", "y"));
List<String> fromObj = (List<String>) obj;  // WARNING: unchecked cast
// Compiles and works here because the actual object IS an ArrayList of Strings

Object obj2 = new ArrayList<>(List.of(1, 2, 3));
List<String> badCast = (List<String>) obj2;  // WARNING: unchecked cast — NO exception here
String s = badCast.get(0);                   // ClassCastException HERE — at read time

// 4. Unchecked conversion — method return of raw type used as parameterized
@SuppressWarnings("rawtypes")
static List getRaw() { return new ArrayList(List.of("p", "q")); }
List<String> fromMethod = getRaw();   // WARNING: unchecked conversion

// ── -Xlint:unchecked — enabling full warnings ────────────────────────
// Compile with: javac -Xlint:unchecked MyClass.java
// Output shows each unchecked location and what type was involved:
// MyClass.java:12: warning: [unchecked] unchecked assignment
//   List<String> typed = rawSource;
//                        ^
//   required: List<String>, found: List

// ── @SuppressWarnings("unchecked") — with documented justification ────
// BAD — no justification:
@SuppressWarnings("unchecked")
public <T> T readFromCache(String key) {
    return (T) cache.get(key);   // dangerous: caller-chosen T may not match stored value
}

// GOOD — with verified invariant documented:
/**
 * Reads from the cache. Safe because items are always stored via
 * storeInCache(String, T) which maintains the key→T invariant.
 * No other code path writes to the backing map.
 */
@SuppressWarnings("unchecked")
public <T> T readFromCache(String key) {
    return (T) cache.get(key);
}

// Narrowest scope — suppress only the single statement where needed:
public Map<String, Integer> parseConfig(String json) {
    Object parsed = jsonParser.parse(json);
    @SuppressWarnings("unchecked")
    Map<String, Integer> result = (Map<String, Integer>) parsed;
    // Safe: the JSON schema guarantees string keys and integer values
    return result;
}

// ── Raw types in reflection — unavoidable and idiomatic ──────────────
import java.lang.reflect.*;

// getDeclaredMethods() returns Method[] — no generic info on the array:
Method[] methods = String.class.getDeclaredMethods();  // raw, no alternative

// getReturnType() returns raw Class:
Class returnType = methods[0].getReturnType();   // raw — getReturnType() cannot be generic

// getGenericReturnType() returns Type (may be ParameterizedType, TypeVariable, etc.):
Type genericReturn = methods[0].getGenericReturnType();  // preserves generic signature

// Pattern: use raw Class when only the erasure matters, Type when you need generics:
for (Method m : String.class.getDeclaredMethods()) {
    System.out.printf("%-20s  erased=%-15s  generic=%s%n",
        m.getName(),
        m.getReturnType().getSimpleName(),
        m.getGenericReturnType().getTypeName());
}

// Instantiating via reflection — always returns raw type, must suppress:
@SuppressWarnings("unchecked")
public static <T> T newInstance(Class<T> clazz) throws ReflectiveOperationException {
    // getDeclaredConstructor().newInstance() is safe here because clazz IS Class<T>:
    return (T) clazz.getDeclaredConstructor().newInstance();
    // Actually: clazz.getDeclaredConstructor().newInstance() already returns T via Class<T>.
    // For Constructor<T>, newInstance() returns T — no cast needed:
}

// The truly safe form using Class<T> — no suppression needed:
public static <T> T safeNewInstance(Class<T> clazz) throws ReflectiveOperationException {
    return clazz.getDeclaredConstructor().newInstance();  // returns T directly — no cast
}

Related Topics in Generics

Generic Class
A generic class in Java is a class parameterized by one or more type parameters, written as class Box<T> or class Pair<K, V>. Type parameters allow a single class definition to work correctly with many different types while preserving full compile-time type safety — eliminating the need for casts, preventing ClassCastException at runtime, and making the intended type contract explicit in the API. The compiler erases type parameters at bytecode level (type erasure), replacing them with their bounds or Object, but all type checking occurs at compile time before erasure. This entry covers type parameter declaration and naming conventions, bounded type parameters (extends, super), multiple type parameters, raw types and why they are unsafe, type erasure and its consequences, wildcards (?, ? extends T, ? super T), the PECS principle, recursive type bounds, generic interfaces, inheritance with generic classes, and how to design generic classes that work correctly across all parameterizations.
Generic Method
A generic method is a method that declares its own type parameters, independent of any type parameters on its enclosing class. The type parameters appear in angle brackets before the return type: public static <T> T identity(T value). Generic methods can appear in non-generic classes, in generic classes (adding their own type parameters beyond the class's), and as both static and instance methods. The compiler infers the type arguments for generic method calls from the argument types, eliminating the need for explicit type witness syntax in most cases. This entry covers type parameter declaration on methods, type inference rules and when inference fails, bounded type parameters on methods, returning computed type relationships, generic methods that operate on multiple type parameters, static generic utility methods (the standard library pattern), the interaction between method type parameters and class type parameters, explicit type witness syntax, and the difference between using a class type parameter vs a method type parameter.
Type Parameters
Type parameters are the placeholders — written as single uppercase letters like T, E, K, V — that make Java generics work. A type parameter declares a variable that stands in for a concrete type, to be supplied by the caller or inferred by the compiler. They appear on classes (class Box<T>), interfaces (interface Comparable<T>), and methods (public static <T> T identity(T value)), and they are the mechanism by which the Java type system achieves compile-time type safety without duplicating code for each concrete type. This entry covers where and how type parameters are declared, the naming conventions and why they exist, the scope rules for type parameters (where they are in scope and where they are not), how type parameters interact with inheritance, how the compiler resolves a type parameter to a concrete type at each call site, and the distinction between a type parameter (a declaration) and a type argument (a concrete substitution).
Bounded Types
Bounded type parameters constrain which concrete types may be substituted for a type parameter. An upper bound (T extends Number) restricts T to Number and its subtypes, making Number's methods available on values of type T within the generic code. A lower bound cannot be applied to type parameters (only to wildcards), but multiple upper bounds can be combined with & (T extends Number & Comparable<T> & Serializable). Bounds serve two purposes simultaneously: they restrict the valid type arguments that callers can supply, and they expand what the generic code can do with values of type T by exposing the bound's API. Without a bound, only Object's methods are available on T values. This entry covers single and multiple upper bounds, recursive bounds, bounds on class type parameters vs method type parameters, how bounds interact with type erasure, bridge methods generated by the compiler, and practical patterns where bounds enable algorithms impossible with unbounded type parameters.