Generics

Parameterized types — writing one algorithm that works, type-safely, with many kinds of data

What this chapter is about. Generics, added by JDK 5, let you create classes, interfaces, and methods that operate on a type that is supplied as a parameter. The same stack, list, or comparison logic is written once and then used with Integer, String, Thread, or any other reference type — with the compiler guaranteeing type safety and inserting every cast for you.

1. The Big Idea

At its core, generics means parameterized types. Many algorithms are logically identical no matter what data they work on: the mechanism that supports a stack is the same whether it stores Integer, String, or Object values. With generics you define that algorithm one time, independent of any specific type, and then apply it to many types without extra effort.

Java always allowed generalized code through references of type Object (because Object is the superclass of every class). The problem was that Object-based code was not type-safe and forced you to write explicit casts. Generics add the missing type safety and make all casts automatic and implicit.

Warning for C++ programmers: Java generics are similar in spirit to C++ templates but are not the same. Do not assume the mechanics carry over — Java uses erasure (Section 15), so there is really only one compiled class, not one per type argument.

2. Why This Topic Matters

  • Run-time errors become compile-time errors. A type mismatch that an Object-based container would only discover as a ClassCastException at run time is caught by the compiler.
  • No hand-written casts. Retrieving a value from a generic container returns the right type directly.
  • The Collections Framework depends on it. The single feature of Java most changed by generics is the Collections Framework (Chapter 20): List<String>, Map<String,Integer>, and so on. Understanding generics is a prerequisite for using collections well.
  • It changed how Java code is written — generics are now an integral part of the language and its API.

3. Core Concepts and Vocabulary

Type parameter
A placeholder name, written in angle brackets, e.g. the T in class Gen<T>. By convention a single capital letter: T (type), E (element), K/V (key/value). Since JDK 10 you may not name one var.
Type argument
The real type supplied when you use the generic, e.g. the Integer in Gen<Integer>.
Generic / parameterized type
A class, interface, or method that declares one or more type parameters.
Bounded type
A type parameter limited to a superclass or interface: <T extends Number>.
Wildcard
? — "some unknown type". Can be bounded: <? extends X> (upper), <? super X> (lower).
Raw type
Using a generic class with no type argument, e.g. Gen. A legacy-compatibility feature that discards type safety.
Erasure
The compiler removes all generic type info and substitutes casts, so no type parameters exist at run time.
Diamond operator
<> — tells the compiler to infer the type arguments in a new expression (JDK 7+).

4. A Simple Generic Class, Step by Step

// A simple generic class. // T is a type parameter that is replaced by a real type // when a Gen object is created. class Gen<T> { T ob; // an object of type T Gen(T o) { // constructor parameter is type T ob = o; } T getOb() { // return type is T return ob; } void showType() { System.out.println("Type of T is " + ob.getClass().getName()); } } class GenDemo { public static void main(String[] args) { Gen<Integer> iOb; // reference to an Integer version of Gen iOb = new Gen<Integer>(88); // autoboxing wraps 88 in an Integer iOb.showType(); int v = iOb.getOb(); // no cast needed System.out.println("value: " + v); Gen<String> strOb = new Gen<String>("Generics Test"); strOb.showType(); String str = strOb.getOb(); // again, no cast needed System.out.println("value: " + str); } }
Expected output
Type of T is java.lang.Integer value: 88 Type of T is java.lang.String value: Generics Test

Reading the code

  • class Gen<T>T is a placeholder for the actual type, used anywhere inside Gen a type is needed.
  • T ob; — when String is passed for T, ob is a String in that instance.
  • Gen(T o) and T getOb() — the constructor parameter and the return type are both T, so they always agree with ob.
  • Gen<Integer> iObInteger is the type argument. Conceptually this is a version of Gen where every T is Integer.
  • int v = iOb.getOb(); — the compiler already knows the return is Integer, so it auto-unboxes to int with no cast.
The compiler does not really make one class per type. It removes the generic information and inserts casts so the code behaves as if a specific version existed. There is only one Gen class in your program (see Erasure).

5. Three Rules That Follow Immediately

5.1 Generics work only with reference types

The type argument must be a class type. A primitive is illegal:

Gen<int> intOb = new Gen<int>(53); // Error - primitive not allowed

This is not a real restriction: use the wrapper classes (Integer, Double, …), and autoboxing makes them nearly invisible.

5.2 Different type arguments produce incompatible types

Gen<Integer> iOb = new Gen<Integer>(88); Gen<String> strOb = new Gen<String>("test"); iOb = strOb; // Error - Gen<Integer> and Gen<String> are not compatible

Even though both come from Gen<T>, they are different types. This is part of how generics prevent errors.

5.3 How this improves type safety (the non-generic contrast)

A pre-generics equivalent uses Object:

class NonGen { Object ob; NonGen(Object o) { ob = o; } Object getOb() { return ob; } } // ... NonGen iOb = new NonGen(88); int v = (Integer) iOb.getOb(); // explicit cast REQUIRED NonGen strOb = new NonGen("test"); iOb = strOb; // compiles, but conceptually wrong v = (Integer) iOb.getOb(); // RUN-TIME ClassCastException
  • Explicit casts must be written by hand — an inconvenience and a source of error.
  • The mismatched assignment iOb = strOb compiles, and the bug only surfaces at run time.
Remember: with generics, that whole sequence would not compile. In essence, generics convert a class of run-time errors into compile-time errors — a major advantage.

6. More Than One Type Parameter

class TwoGen<T, V> { T ob1; V ob2; TwoGen(T o1, V o2) { ob1 = o1; ob2 = o2; } T getOb1() { return ob1; } V getOb2() { return ob2; } } TwoGen<Integer, String> tg = new TwoGen<Integer, String>(88, "Generics");

Type parameters are a comma-separated list. Two type arguments must then be supplied. The two types may be the same (TwoGen<String, String> is legal), but if they always were, you would not need two parameters.

General form of a generic class

class class-name<type-param-list> { // ... } class-name<type-arg-list> var-name = new class-name<type-arg-list>(cons-arg-list);

7. Bounded Types

Sometimes any class is too permissive. Suppose a class should compute the average of an array of numbers by calling doubleValue(). That method is defined by Number, but the compiler does not know T will only ever be a number, so it rejects the call. The fix is an upper bound:

// T must be Number or a subclass of Number. class Stats<T extends Number> { T[] nums; Stats(T[] o) { nums = o; } double average() { double sum = 0.0; for(int i = 0; i < nums.length; i++) sum += nums[i].doubleValue(); // now legal - T is known to be a Number return sum / nums.length; } } Integer[] inums = { 1, 2, 3, 4, 5 }; Stats<Integer> iob = new Stats<Integer>(inums); System.out.println(iob.average()); // 3.0 // Stats<String> would not compile - String is not a Number.
  • <T extends superclass> means T can be superclass or any subclass of it — an inclusive upper limit.
  • The bound both enables calls to methods of the bound type and prevents non-conforming type arguments.
  • A bound may be an interface, or several interfaces, or a class plus interfaces joined with & (an intersection type); the class must come first: <T extends MyClass & MyInterface>.

8. Wildcard Arguments

Now add a method to Stats that compares the average of this object with the average of another Stats object of any numeric type. Writing the parameter as Stats<T> fails — it only matches a Stats whose type equals the invoking object's. The solution is the wildcard ?, meaning "any type":

boolean isSameAvg(Stats<?> ob) { return average() == ob.average(); } // Now any two Stats objects can be compared: Stats<Integer> iob = new Stats<Integer>(inums); Stats<Double> dob = new Stats<Double>(dnums); Stats<Float> fob = new Stats<Float>(fnums); iob.isSameAvg(dob); // legal iob.isSameAvg(fob); // legal
Important: the wildcard does not change which Stats objects can be created — that is still governed by <T extends Number>. The wildcard just lets a parameter match any valid Stats.

8.1 Bounded wildcards

Wildcards can be bounded, which matters when a method must work on part of a class hierarchy. Given coordinate classes TwoDThreeDFourD and a container Coords<T extends TwoD>:

// Works for ANY Coords - every element has x and y. static void showXY(Coords<?> c) { /* ... */ } // Works ONLY if elements are ThreeD or a subclass of ThreeD. static void showXYZ(Coords<? extends ThreeD> c) { /* ... uses .z ... */ } Coords<TwoD> tdlocs = new Coords<TwoD>(td); Coords<FourD> fdlocs = new Coords<FourD>(fd); showXY(tdlocs); // OK // showXYZ(tdlocs); // Error - TwoD has no z; bound prevents the call showXYZ(fdlocs); // OK - FourD is a subclass of ThreeD
FormMeaningInclusive?
<? extends superclass>Upper bound: any type that is superclass or a subclass of itYes (the bound class itself is allowed)
<? super subclass>Lower bound: only types that are subclass or a superclass of itYes

9. Generic Methods

Methods inside a generic class are already generic over the class' type parameters. But a method can declare its own type parameters, and it may live in a non-generic class. The type parameter list goes before the return type.

class GenMethDemo { // T must be Comparable; V must be T or a subclass of T. static <T extends Comparable<T>, V extends T> boolean isIn(T x, V[] y) { for(int i = 0; i < y.length; i++) if(x.equals(y[i])) return true; return false; } public static void main(String[] args) { Integer[] nums = { 1, 2, 3, 4, 5 }; if(isIn(2, nums)) System.out.println("2 is in nums"); String[] strs = { "one", "two", "three" }; if(isIn("two", strs)) System.out.println("two is in strs"); // isIn("two", nums); // Error - Integer is not a subclass of String } }
  • Callers normally use ordinary call syntax — the type arguments are inferred from the actual arguments.
  • You can specify them explicitly: GenMethDemo.<Integer, Integer>isIn(2, nums) — rarely needed, and JDK 8 improved inference so it is needed even less.
  • Generic methods may be static or instance methods.

General form: <type-param-list> ret-type meth-name(param-list) { ... }

9.1 Generic constructors

A constructor can be generic even when its class is not:

class GenCons { private double val; <T extends Number> GenCons(T arg) { val = arg.doubleValue(); } } new GenCons(100); // Integer new GenCons(123.5F); // Float

10. Generic Interfaces

interface MinMax<T extends Comparable<T>> { T min(); T max(); } class MyClass<T extends Comparable<T>> implements MinMax<T> { T[] vals; MyClass(T[] o) { vals = o; } public T min() { /* scan vals with compareTo */ } public T max() { /* scan vals with compareTo */ } }
  • A class that implements a generic interface generally must itself be generic, at least enough to pass a type parameter to the interface. class MyClass implements MinMax<T> (with no <T> on the class) is an error.
  • If the class implements a specific version — implements MinMax<Integer> — it need not be generic.
  • Once the class' bound is established, it is passed to the interface without repeating the bound: implements MinMax<T>, never implements MinMax<T extends Comparable<T>>.

Benefits: one interface for many data types, plus the ability to constrain those types with bounds.

11. Raw Types and Legacy Code

Because generics did not exist before JDK 5, a generic class may be used with no type argument, producing a raw type that inter-operates with old code — at the cost of type safety.

Gen<Integer> iOb = new Gen<Integer>(88); Gen<String> strOb = new Gen<String>("Generics Test"); Gen raw = new Gen(Double.valueOf(98.6)); // raw type - T becomes Object double d = (Double) raw.getOb(); // cast needed; type is unknown // int i = (Integer) raw.getOb(); // compiles, RUN-TIME error (holds Double) strOb = raw; // allowed, but unsafe // String s = strOb.getOb(); // RUN-TIME error raw = iOb; // allowed, but unsafe
  • A raw reference can be assigned any Gen object, and vice-versa — both directions bypass type checking.
  • javac emits unchecked warnings when a raw use might jeopardize type safety.
  • Use raw types only to bridge legacy and generic code. They are a transitional feature, not for new code.

12. Generic Class Hierarchies

Generic classes can extend and be extended like any class. The key rule: type arguments needed by a generic superclass must be passed up the hierarchy by every subclass, similar to constructor arguments.

class Gen<T> { T ob; Gen(T o) { ob = o; } T getOb() { return ob; } } // Subclass must restate T and pass it up, even if it adds nothing of its own. class Gen2<T> extends Gen<T> { Gen2(T o) { super(o); } } // A subclass may add its own type parameters. class Gen3<T, V> extends Gen<T> { V ob2; Gen3(T o, V o2) { super(o); ob2 = o2; } V getOb2() { return ob2; } }

A non-generic class can be the superclass of a generic subclass with no special conditions: class Gen<T> extends NonGen { ... }.

12.1 Run-time type comparisons (instanceof)

Gen<Integer> iOb = new Gen<Integer>(88); Gen2<Integer> iOb2 = new Gen2<Integer>(99); iOb2 instanceof Gen2<?> // true iOb2 instanceof Gen<?> // true (Gen is the superclass) iOb instanceof Gen2<?> // false (iOb is only a Gen)

You test against a wildcard form (Gen2<?>) because the specific type argument does not exist at run time — instanceof can only confirm the object is some kind of Gen2.

12.2 Casting

You can cast one generic instance to another only if they are otherwise compatible and the type arguments are the same. Given the objects above: (Gen<Integer>) iOb2 is legal; (Gen<Long>) iOb2 is not.

12.3 Overriding methods

A method in a generic class overrides normally. The overridden version runs for subclass objects and the superclass version runs for superclass objects — exactly as with non-generic code.

13. Type Inference: the Diamond and var

// Pre-JDK 7 - type arguments written twice: MyClass<Integer, String> mc = new MyClass<Integer, String>(98, "A String"); // JDK 7+ - diamond operator infers them in the new expression: MyClass<Integer, String> mc = new MyClass<>(98, "A String"); // JDK 10+ - local variable type inference: var mc = new MyClass<Integer, String>(98, "A String");
  • <> (the diamond operator) is an empty type-argument list; the compiler infers the arguments from the declaration.
  • Inference also applies to arguments: mcOb.isSame(new MyClass<>(1, "test")).
  • var infers the whole type from the initializer — useful because generic type names get long.

14. Erasure — Why Generics Behave As They Do

Generics had to be compatible with all pre-existing non-generic code and could not change the JVM in breaking ways. Java achieves this with erasure:

  • At compile time all generic type information is removed.
  • Each type parameter is replaced by its bound (or Object if unbounded).
  • Appropriate casts are inserted to preserve type compatibility, and the compiler enforces it.
  • Therefore no type parameters exist at run time — they are purely a source-code mechanism.

14.1 Bridge methods

Occasionally the compiler adds an invisible bridge method so that an overriding method's erased signature matches the superclass'. Example: class Gen2 extends Gen<String> overrides String getOb(), but erasure expects Object getOb(), so the compiler generates an Object getOb() that calls the String one. Bridge methods exist only in bytecode; you never write or call them, and they are the one case where two methods legitimately differ only by return type.

15. Ambiguity Errors

class MyGenClass<T, V> { T ob1; V ob2; // These two overloads are AMBIGUOUS - will not compile. void set(T o) { ob1 = o; } void set(V o) { ob2 = o; } }

Two problems: (1) nothing forces T and V to differ — MyGenClass<String, String> makes both set methods identical; (2) after erasure both reduce to void set(Object o). Adding a bound (V extends Number) only helps until you write MyGenClass<Number, Number>. Usually the real fix is a design change — use two distinct method names.

16. Generic Restrictions (Quick Reference)

RestrictionDetail
Cannot instantiate a type parameterob = new T(); is illegal — the compiler does not know what to create.
No static member may use the class' type parameterstatic T ob; and static T getOb() are illegal. (A static generic method with its own type parameter is fine.)
Cannot create an array of a type parametervals = new T[10]; is illegal. Declaring T[] vals; and assigning an existing array is fine.
Cannot create an array of a type-specific genericnew Gen<Integer>[10] is illegal; new Gen<?>[10] is allowed (and keeps some checking, unlike raw arrays).
A generic class cannot extend ThrowableYou cannot create generic exception classes.

17. Visual Mental Model

write once class Gen<T> { ... T ... T ... T ... } | +---------------+----------------+ | | | Gen<Integer> Gen<String> Gen<Thread> <- distinct, incompatible types | | | compiler inserts casts, checks assignments, forbids mismatches | ERASURE at compile time -> one real class, T replaced by its bound / Object (no <T> survives to run time)

Wildcards sit "above" the specific types: Gen<?> matches all of them; Gen<? extends Number> matches the numeric ones only.

18. Important Comparisons

Generic (Gen<T>)Non-generic (Object field)
Casts on retrievalInserted automaticallyWritten by hand
Type mismatchCompile-time errorRun-time ClassCastException
Compiler knowledge of contentsExactNone
Type parameter <T extends X>Wildcard <? extends X>
Names a type you can useYes — T is usable in the bodyNo — ? is an unknown
Typical useDeclaring a generic class/methodA parameter that must accept many instantiations
Raw type GenWildcard Gen<?>
Type checkingEffectively none; unchecked warningsSome checking is still enforced
When to useOnly to bridge legacy codeWhenever you need "any instantiation"

19. Common Beginner Mistakes

Using a primitive as a type argumentGen<int>. Use Gen<Integer>.
Expecting Gen<Integer> and Gen<String> to be assignment-compatible because both are "a Gen". They are not.
Repeating the bound in implementsimplements MinMax<T extends Comparable<T>>. Pass just <T>.
Writing new T() or new T[n] inside a generic class.
Declaring static fields/methods that use the class' type parameter.
Reaching for raw types in new code and then ignoring the unchecked warnings.
Overloading on T vs V — ambiguous after erasure.
Trying obj instanceof Gen<Integer> — must be Gen<?>.

20. Best Practices

  • Prefer generics over Object for any container or algorithm that should be type-safe.
  • Use the diamond operator (new ArrayList<>()) to avoid repeating type arguments; consider var for long generic types in local code.
  • Bound type parameters to the smallest type that supports the operations you need.
  • Use wildcards for parameters that only read from a structure of "some" instantiation; use ? extends for producers and ? super for consumers.
  • Confine raw types to code that must talk to pre-generics APIs; eliminate unchecked warnings elsewhere.
  • Follow the naming convention (T, E, K, V) — never var.
  • If two overloads become ambiguous under erasure, rename them — it usually signals a design issue.

21. "Remember" Points

Key takeaways
  • Generics = parameterized types: one definition, many type-safe uses.
  • They turn many run-time cast errors into compile-time errors and remove hand-written casts.
  • Type arguments must be reference types; different type arguments give incompatible types.
  • extends creates an inclusive upper bound for both type parameters and wildcards; super creates a lower bound for wildcards.
  • The wildcard ? means "some unknown type" — use it for flexible parameters, and in instanceof.
  • Erasure removes all generic info at compile time — no <T> exists at run time, which explains the array, static, new T(), and Throwable restrictions.
  • Raw types exist only for legacy compatibility.

22. Interview / Revision Questions

  • What does "generics means parameterized types" mean, and what problem did it solve over Object-based code?
  • Why can't a primitive be a type argument, and why is that not a serious limitation?
  • Are Gen<Integer> and Gen<Number> assignment-compatible? Why or why not?
  • Write a bounded type parameter that allows only Number and its subclasses, and explain both effects of the bound.
  • What is a wildcard? Contrast <?>, <? extends X>, and <? super X>.
  • Where does the type-parameter list go in a generic method declaration?
  • Can a generic constructor appear in a non-generic class? Give an example.
  • Why must a class implementing a generic interface usually be generic itself?
  • What is a raw type, when is it acceptable, and what does javac do when it sees a risky use?
  • Explain erasure. How does it explain the restriction against new T[10] and against generic subclasses of Throwable?
  • What is a bridge method and why is it sometimes generated?
  • Why is overloading set(T) and set(V) ambiguous?
  • Rewrite new MyClass<Integer,String>(...) using the diamond operator and again using var.

23. Practice Exercises

  • Box: Write class Box<T> with set(T)/get(). Store a String and an Integer in separate boxes; confirm no casts are needed and that boxA = boxB across types will not compile.
  • Bounded average: Recreate Stats<T extends Number> and add isSameAvg(Stats<?>). Test it with Integer, Double, and Float arrays.
  • Generic method: Write static <T> T first(T[] a) that returns the first element, and a static <T extends Comparable<T>> T max(T[] a).
  • Bounded wildcard: Build the TwoD/ThreeD/FourD hierarchy and Coords<T extends TwoD>; write showXY and showXYZ and prove the compiler blocks showXYZ(Coords<TwoD>).
  • Generic interface: Implement MinMax<T extends Comparable<T>> for an array of Character and one of Integer.
  • Raw type experiment: Create a raw Box, put a Double in it, then trigger a run-time ClassCastException. Note where javac printed unchecked warnings.
  • Restrictions: Try to compile new T(), new T[5], a static T field, and class Bad<T> extends Exception; record each error message.

24. Quick Memory Map

GENERICS |-- What: parameterized types - write an algorithm once, use with many types |-- Why: type safety at COMPILE time + automatic casts | |-- Declaring | class C<T> / interface I<T> / <T> ret meth(...) (method: params before return type) | multiple params: <T, V> | |-- Bounds | <T extends Number> upper bound (inclusive), enables + restricts | <T extends A & B> intersection (class first) | |-- Wildcards | <?> any instantiation | <? extends X> upper-bounded (producers) | <? super X> lower-bounded (consumers) | used in instanceof: obj instanceof Gen<?> | |-- Hierarchies: subclasses must pass type args up to a generic superclass |-- Inference: diamond <> (JDK 7), var (JDK 10) | |-- Erasure: all <T> removed at compile; replaced by bound/Object + casts | => no new T(), no new T[], no static T, no generic Throwable, | no array of Gen<Type>, instanceof needs <?> | |-- Raw types: legacy bridge only; lose type safety; unchecked warnings `-- Ambiguity: overloads that erase to the same signature won't compile

25. Complete Chapter Revision

  1. Generics = parameterized types. A class/interface/method operates on a type supplied as a parameter, written in < >.
  2. Benefit: compile-time type safety and automatic, implicit casts — run-time cast errors become compile-time errors.
  3. Type parameter vs. type argument: T is the placeholder in the declaration; Integer is the real type at the use site.
  4. Reference types only; use wrappers for primitives. Different type arguments → incompatible types.
  5. Multiple type parameters are a comma list; they may coincide.
  6. Bounded types (<T extends X>) both enable calls to X's members and forbid non-conforming arguments; bounds may combine a class and interfaces with &.
  7. Wildcards (?) represent an unknown type; bounded wildcards (? extends / ? super) restrict which instantiations a method accepts.
  8. Generic methods declare their own type parameters (before the return type) and can live in non-generic classes; constructors can be generic too.
  9. Generic interfaces force the implementing class to be generic (unless a specific instantiation is implemented); the bound is stated once, on the class.
  10. Raw types drop type safety and exist only for legacy interoperability; javac issues unchecked warnings.
  11. Generic hierarchies must pass type arguments up to a generic superclass; instanceof and casts work with wildcard forms and matching type arguments.
  12. Type inference: the diamond <> (JDK 7) and var (JDK 10) shorten declarations.
  13. Erasure removes all generic information at compile time (replacing parameters with their bound/Object and inserting casts), which explains every generic restriction: no new T(), no new T[], no static members using T, no type-specific generic arrays, and no generic subclass of Throwable. Bridge methods patch mismatched erased signatures.
  14. Ambiguity errors arise when two generic declarations erase to the same thing.

26. Final Takeaway

Generics let you say what a class or method does independently of the type it does it to, and hand the compiler enough information to prove your code is type-correct before it ever runs. Write the algorithm once with a type parameter, bound that parameter only as much as the logic requires, use wildcards where a method must accept many instantiations, and remember that none of the angle brackets survive to run time — erasure is the reason generics are both backward-compatible and occasionally surprising.