Generic Classes in Java

Learn how to write reusable, type-safe data structures using type parameters like T, K, V in Java generic classes.

published: reading time: 15 min read author: Geek Workbench

Generic Classes in Java

Generic classes parameterize types using type parameters (<T>, <K, V>, etc.), enabling you to write a single class that works with multiple data types while maintaining compile-time type safety. Instead of Object casts scattered throughout your code, generics let the compiler enforce correct usage.

Introduction

Before generics (Java 1.4 and earlier), collections held Object — you could put anything into a List and the compiler would not complain. Retrieving an element required a cast to the actual type, and a mismatched cast threw ClassCastException at runtime. This was type-unsafe and verbose: the compiler could not help you, and runtime type errors were discovered in production rather than at compile time.

Generics fixed this by allowing classes to declare type parameters. A Box<T> is a box that holds elements of type T — the compiler tracks what T is at each usage and inserts casts automatically. If you try to put an Integer into a Box<String>, the code fails to compile with a clear error message, not a runtime crash. This is compile-time type safety, and it is one of the most impactful features for writing correct, maintainable Java code.

However, generics in Java are implemented via type erasure — the generic type information is removed at compile time, and all type parameters become Object or their bound type in the bytecode. This means Box<String> and Box<Integer> are the same class at runtime. Understanding erasure is essential for understanding the real behavior of generic code and the limitations that are not visible in source code alone.

This guide covers how to define and use generic classes, the type parameter conventions and naming standards, the common pitfalls from erasure and raw types, and the security and performance considerations that affect generic class design in production systems.

When to Use Generic Classes

  • Building collection classesList<T>, Map<K, V>, Set<T>

  • Writing utility wrappers that operate on any type

  • Creating data holders that cache or buffer elements

  • Implementing type-safe builders and fluent APIs

When NOT to Use Generic Classes

  • The class behavior is identical across all types and no casting is needed — a plain class suffices

  • You need to support primitive types directly (use wrapper classes or specialized implementations)

  • Introducing generics adds unnecessary complexity for a one-off utility

  • You need to serialize to JSON/XML with frameworks that struggle with generics (check framework support first)

Code Example: A Simple Generic Box


public class Box<T> {

    private T content;



    public void set(T content) {

        this.content = content;

    }



    public T get() {

        return content;

    }

}



// Usage

Box<String> stringBox = new Box<>();

stringBox.set("Hello");      // type-safe

String value = stringBox.get(); // no cast needed

Code Example: A Generic Pair


public class Pair<K, V> {

    private K key;

    private V value;



    public Pair(K key, V value) {

        this.key = key;

        this.value = value;

    }



    public K getKey()   { return key; }

    public V getValue() { return value; }



    public static <K, V> Pair<K, V> of(K k, V v) {

        return new Pair<>(k, v);

    }

}



// Usage

Pair<String, Integer> entry = Pair.of("age", 30);

Mermaid Diagram: Generic Class Hierarchy


classDiagram

    class Box~T~ {

        -T content

        +set(T content)

        +get() T

    }

    class Pair~K, V~ {

        -K key

        -V value

        +getKey() K

        +getValue() V

        +of(K, V) Pair~K, V~

    }

    class StringBox --|> Box

    StringBox : String content

    class IntegerBox --|> Box

    IntegerBox : Integer content

Failure Scenarios

1. Raw Type Usage


// WARNING: raw type - defeats the purpose of generics

Box rawBox = new Box();

rawBox.set("Hello");

rawBox.set(42); // compiles but no type check - danger!

Integer val = (Integer) rawBox.get(); // ClassCastException at runtime if wrong

Fix: Always use parameterized types: Box<String>.

2. Type Mismatch at Call Site


Box<Object> objBox = new Box<>();

objBox.set("text");

// Box<Object> is NOT the same as Box<String> — invariance

// Box<String> s = new Box<Object>(); // compile error

3. Primitive Types Not Supported


// Box<int> box = new Box<>(); // compile error - int is not a reference type

Box<Integer> intBox = new Box<>(); // use wrapper Integer instead

Trade-Off Table

| Aspect | Generic Approach | Raw Type / Object Approach |

| --------------- | ---------------------------- | --------------------------------------- |

| Type safety | Compile-time enforced | Runtime ClassCastException risk |

| Code verbosity | Slightly more at declaration | Less at declaration, more at cast sites |

| Performance | No runtime penalty (erasure) | No runtime penalty |

| Refactor safety | Rename type param is safe | Rename field risks casts breaking |

| IDE support | Full autocompletion for T | Limited — IDE sees Object |

Observability Checklist

  • Verify generic type arguments are consistent across the call chain

  • Check for raw type warnings in static analysis (SpotBugs, CheckStyle)

  • Ensure serialization frameworks handle generics correctly (Jackson @JsonTypeInfo, Genson, etc.)

  • Add equals() / hashCode() implementations that account for type parameters

  • Document type parameter contracts in Javadoc (@param <T>)

Security Notes

  • Deserialization attacks: Untrusted data deserialized into Object or raw types can trigger unsafe casts. Generics provide no runtime protection — use input validation and allowlists.

  • Type parameter not enforced at runtime: Because of type erasure, Box<String> and Box<Integer> are the same class at runtime. Do not rely on generics for security decisions.

  • Reflection: Class.forName(parametricType) with untrusted input can load arbitrary classes. Validate classnames against an allowlist.

Pitfalls

  1. Bounded wildcards needed for arithmetic: T cannot be used in +/- operators. If you need numeric operations, bound with Number or use a strategy pattern.

  2. Generic array creation is illegal: new T[10] does not compile — use Object[] and cast, or use Array.newInstance().

  3. instanceof with generics does not work: if (obj instanceof Box<String>) is a compile error.

Quick Recap

  • Generic classes declare a type parameter section: class Box<T>

  • Instantiation requires type arguments: Box<String>

  • Type erasure removes generics at runtime — all Box<T> become Box (raw type)

  • Raw types bypass type checking — avoid them

  • Primitives require wrapper classes — no Box<int>


Key Takeaways

Generic classes are the foundation of Java’s type-safe collections. A class like Box<T> lets you write one implementation that works with any reference type — the compiler enforces correct usage at compile time, eliminating ClassCastException risks in generic-aware code paths.

The key lesson from Box<T> is that generics are a compile-time contract, not a runtime one. Because of type erasure, Box<String> and Box<Integer> are the same class at runtime. This means you cannot use instanceof Box<String>, create new T[], or rely on the type argument for security decisions. The raw type is all the JVM sees.

Raw types are the escape hatch that undermines this safety net. Using Box instead of Box<String> silences the compiler and lets you insert any object. Avoid raw types in new code — the warning exists for a reason.

For a deeper look at how generic classes actually disappear at compile time, see Type Erasure in Java Generics. For writing flexible methods that operate on generic types, Generic Methods in Java covers static utility patterns and type inference.


Interview Questions

::: info

These questions use the .qa-card CSS class structure. Each card has a .qa-question and .qa-answer div.

:::

1. What is a generic class in Java?

Model Answer: "A generic class is a class defined with one or more type parameters (e.g., `class Box`). It lets you write reusable, type-safe code without casts. Instead of storing everything as `Object` and casting at retrieval, generics let the compiler verify types at compile time, eliminating `ClassCastException` risks in generic-aware code paths."

2. Can you use primitive types as type arguments for generics?

Model Answer: "No. Java generics do not support primitive types — only reference types. `Box` is a compile error. You must use wrapper classes: `Box`. This is because generics are implemented via erasure, and the erased type must be `Object` or a subclass, which primitives are not."

3. What happens if you use a raw type instead of a parameterized generic?

Model Answer: "The compiler accepts it with a warning, but you lose compile-time type checking. You can put any object into a raw `Box`, and retrieving it requires an unchecked cast. If the wrong type is inserted, you get a `ClassCastException` at runtime rather than a compile error. It also undermines IDE autocompletion for the stored type."

4. Can two different instantiations of a generic class share the same runtime class?

Model Answer: "Yes. Due to type erasure, all `Box` instances are just `Box` (raw type) at runtime, regardless of what `T` is. The type argument is a compile-time fiction. This is why `instanceof Box` is illegal — there is no `Box` class at runtime to check against."

5. Can a generic class have multiple type parameters?

Model Answer: "Yes. A generic class can declare any number of type parameters in its definition: `class Entry { ... }`. Each parameter is independent, though the usual naming conventions apply (`K` for key, `V` for value, `T` for single type, etc.)."

6. What is the difference between `` and ``?

Model Answer: "There is no functional difference — both unbounded declarations produce the same erased type (`Object`). However, `` makes the intent explicit: you intend `T` to be a reference type but are not constraining it further. In practice, `` is preferred for brevity; `` is rarely used unless documentation or a framework requires it."

7. Why can't you create an array of a generic type like `new T[10]`?

Model Answer: "Because at runtime, after type erasure, `T` becomes `Object` (or the leftmost bound), so `new T[10]` would resolve to `new Object[10]` — the wrong type. Additionally, arrays carry runtime type information about their element type, which would be lost if you could instantiate a reified generic array. The standard workaround is `Array.newInstance(Class, size)` with an explicit `Class` token, which reifies the type at runtime."

8. Can a generic class have multiple constructors with different type parameter usage?

Model Answer: "Yes. A generic class can have multiple constructors, each using the class's type parameter(s) in different ways. For example, `Pair` can have `Pair(K key, V value)` and `Pair(Pair other)` — both are valid. Constructors do not declare their own type parameters; they use the ones declared on the class itself."

9. What is a generic pair and when would you use one?

Model Answer: "A generic pair (`Pair`) is a simple data holder that stores two related values of potentially different types. It is useful for returning two values from a method, mapping keys to values in simple contexts, or as a building block for more complex generic structures. For production code, prefer specialized types (e.g., `Map.Entry` from the JDK) over ad-hoc pairs, as the semantics are clearer."

10. How can you prevent null values in a generic container?

Model Answer: "You can use `Objects.requireNonNull()` in the setter, or bound the type parameter with a custom `NonNull` marker interface. A cleaner approach is using `Optional` as the field type rather than `T` — this makes nullability explicit at the call site and forces callers to handle the absent case. Another option is a dedicated `NonNullBox` class that validates on set and throws `NullPointerException` if a null is passed."

11. Can you constrain a type parameter to a specific type hierarchy?

Model Answer: "Yes. `class NumericBox` restricts `T` to `Number` or any subclass (`Integer`, `Double`, `BigDecimal`, etc.). This lets you call `Number` methods like `doubleValue()` inside the class. The same rules apply as with methods — multiple bounds use `&`, and the first bound (if a class) sets the erasure base type."

12. How does implementing equals() work on a generic class?

Model Answer: "When implementing `equals(T obj)` on a generic class, the signature after erasure becomes `equals(Object obj)`. Your `equals` implementation should handle the type check internally (using `instanceof` or `getClass()`) rather than using the generic parameter as the check type, because `Box.equals` and `Box.equals` both override `Object.equals`. Always override both `equals()` and `hashCode()` together when storing generic objects in collections."

13. How does hashCode() work in a generic class?

Model Answer: "`hashCode()` is declared on `Object` with signature `int hashCode()` — no generic parameter. So a generic class's `hashCode()` method does not need a bridge and is not affected by erasure in signature. However, if your `hashCode()` implementation calls a method on `T`, that method must be accessible after erasure (i.e., declared in the bound or in `Object`). For example, using `t.hashCode()` on an unbounded `T` works because `hashCode()` is on `Object`."

14. Can a generic class be declared final?

Model Answer: "Yes. `final` and generics are independent modifiers. A `final` generic class cannot be extended, but it can still be instantiated with different type arguments. For example, `java.util.Collections.SingletonMap` is final and generic. The type parameter exists only at compile time; at runtime, the class is just the raw type."

15. What are the naming conventions for type parameters?

Model Answer: "The Java convention (from the JLS) is single uppercase letters: `T` for type, `K` for key, `V` for value, `E` for element, `N` for number, `R` for return type. Multi-letter names like `Key` or `Value` are used informally but are not JLS-compliant. Descriptive multi-letter names (like `CustomType`) are valid but uncommon in idiomatic Java."

16. How does type inference work with generic constructors?

Model Answer: "The compiler can often infer the type argument from the context — `new Box<>("hello")` infers `String`. In diamond syntax `new Box<>()`, the compiler infers from the declared type on the left side if available. For chained calls or ambiguous contexts, explicit `` may be needed. Type inference also works through method arguments: `List list = new ArrayList<>()` infers `ArrayList`."

17. Can static methods in a generic class declare their own type parameters?

Model Answer: "Yes. Static methods in any class — generic or not — can declare their own type parameters. The type parameter on a static method is independent of any class-level type parameter. For example, `public static T of(T value)` in a non-generic class is valid. The method's type parameter is scoped only to that method and cannot be accessed from the class's instance members."

18. What is a Class token and why is it useful with generics?

Model Answer: "`Class` is a reifiable type token that carries `T` at runtime. Because erasure removes generic type information, `T.class` is illegal, but you can pass `String.class` (which is `Class`) to a method that needs to know the type at runtime. This enables `newInstance()`, `cast()`, `asSubclass()`, and `Array.newInstance()`. The caller controls the token, so the method gets a concrete, reifiable type rather than an erased one."

19. Can a generic class implement a generic interface?

Model Answer: "Yes. A generic class can implement a generic interface while using its own type parameter: `class StringList implements List`. Here `StringList` is not generic itself — it implements the concrete parameterized type `List`. Alternatively, `class AnyList implements List` uses the class's own type parameter for the interface. Both patterns are valid."

20. What is Optional in the context of generics?

Model Answer: "`Optional` is a generic class designed to represent the presence or absence of a value of type `T` without using `null`. It provides `map()`, `flatMap()`, `filter()`, and `orElse()` methods that propagate the generic type safely. Using `Optional` in APIs forces callers to handle the absent case explicitly and eliminates ambiguous null returns. It is a value-based class (not a collection) but follows similar generic patterns."


Further Reading

  • Generic Methods — writing flexible methods with type parameters
  • Wildcards? extends T and ? super T for flexible type ranges
  • Type Erasure — how generics are implemented at compile time
  • Type Bounds — constraining type parameters with upper and lower bounds
  • Bridge Methods — compiler-generated methods from type erasure

Summary

Generic classes form the foundation of type-safe data structures in Java. By declaring a type parameter like T or K, V, a single class definition becomes reusable across any reference type while letting the compiler enforce correct usage at every call site. This eliminates the defensive Object casts and ClassCastException risks that plagued pre-generics collection code.

The key trade-off is that generics are implemented via erasure — there is no runtime type information for Box<String> vs Box<Integer>. Both are just Box at runtime. This means you cannot use instanceof, new T(), or T.class with a generic type parameter. For cases where you need runtime type tokens, pass an explicit Class<T> object.

When designing generic classes, prefer bounded type parameters (<T extends Number>) only when the class actually needs to call type-specific methods. Using bounds unnecessarily restricts which types can instantiate your class. For continued learning on generics, see Generic Methods in Java which covers generic method declarations, type inference, and bounded type parameters in depth.

Category

Related Posts

Abstract Classes in Java

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals