Java Generics and Generic Classes

Java generics let a class or method express the type of values it accepts and returns.

Lesson 63 of 127Intermediate14 min read

Java generics let a class or method express the type of values it accepts and returns. A Box<String> can return a String without an unchecked cast, while a Box<Integer> follows the same implementation with a different type argument. This lesson explains that safety without pretending generics create a new runtime class for every argument.

Review the related foundation lesson when you need a prerequisite. The complete programs below use Java 17-compatible syntax.

Parameterized types

Generics parameterize a type or method with reference types. Box<String> and Box<Integer> share a definition while providing different compile-time checks.

A type parameter such as T is a placeholder in a declaration. A type argument such as String supplies the chosen type at a use site. Inside Box<T>, a field and getter can consistently use T, so the compiler checks assignments and call results. The diamond operator lets the compiler infer suitable arguments from context while retaining the same checked generic type.

Reference-type arguments

Generic type arguments cannot be primitive types. Use Integer rather than int. Avoid raw types because they bypass useful type checking.

Generic arguments must be reference types, so use Integer rather than int. Bounds such as T extends Number restrict allowable arguments and permit operations promised by the bound. A generic method declares its type parameters before the return type. It can introduce a parameter independently of any class parameter, which is useful for utilities that work across several input types.

Invariance and wildcards

Generic types are invariant: List<Integer> is not a subtype of List<Number>. Wildcards express useful flexibility without pretending those lists are interchangeable.

Generic types are invariant: List<Integer> is not a subtype of List<Number>, even though Integer is a subtype of Number. Otherwise a List<Number> reference could insert a Double into storage promised to contain only Integer. Wildcards describe safe flexibility. List<? extends Number> allows reading elements as Number but generally not adding specific non-null values of a guessed subtype.

Bounds and type erasure

An extends wildcard is useful for reading values of a bounded type; a super wildcard is useful for writing values of that type. Type erasure means most generic type arguments are not retained as ordinary runtime type distinctions.

List<? super Integer> can accept Integer values because its unknown element type is Integer or a supertype, but reading yields only Object without additional knowledge. This producer-extends, consumer-super pattern concerns safe operations, not conversion of all elements. Erasure removes much generic information from runtime representation; it does not erase the compiler's source-level checks. Raw types and unchecked casts bypass those checks and should not be the normal design.

Running the examples

Save name.java, compile it with javac --release 17 name.java, and run java name. Use each program's own public class name and filename.

How GenericEquipmentBox produces its output

Box<T> stores one final item and returns that same typed value from get. The Box<String> declaration selects String as its argument, and the diamond infers that argument for construction. Passing Tripod initializes the item, so printing box.get() displays Tripod. No cast is needed because the field and getter retain the same checked type parameter.

How TypedStorage produces its output

The String argument establishes a String box, so get exposes the String length operation. The Integer box returns an Integer, which arithmetic unboxes before adding two. The linked field and return type prevent a caller from inserting an unrelated type into either checked box. Both still use the same Box declaration.

How NumberProducer produces its output

List.of supplies integer wrappers. The wildcard parameter can accept that list because every element is a Number subtype, and the loop safely reads each as Number. intValue yields three and six, producing nine. The method does not attempt to add a Double or another guessed subtype to the unknown element type.

Keep runtime limits separate from compile-time safety

Erasure means that a generic container does not automatically retain a separate runtime class for every type argument. It does not mean generics are pointless: the compiler still rejects many invalid insertions and preserves useful return types. Some operations, such as creating new T[] or testing instanceof List<String>, are unavailable because the required concrete runtime type information is missing. An instanceof List<?> check can test the broad list category without claiming its elements are strings. For practice, write a generic identity method that returns its argument unchanged, then call it with a String and an Integer. Observe how the caller receives different checked result types from one declaration.

Java Summary

  • Generics parameterize a type or method with reference types. Box<String> and Box<Integer> share a definition while providing different compile-time checks.
  • Generic type arguments cannot be primitive types. Use Integer rather than int. Avoid raw types because they bypass useful type checking.
  • Generic types are invariant: List<Integer> is not a subtype of List<Number>. Wildcards express useful flexibility without pretending those lists are interchangeable.
  • An extends wildcard is useful for reading values of a bounded type; a super wildcard is useful for writing values of that type. Type erasure means most generic type arguments are not retained as ordinary runtime type distinctions.
  • Continue with Java exercises, Java quizzes and the Java study roadmap.

Java Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Java Generics and Generic Classes in a complete program

Java
public class GenericEquipmentBox  {
    static class Box<T>  {
        private final T item;
        Box(T item) {
            this.item=item;
        }
        T get() {
            return item;
        }
    }
    public static void main(String[] args)  {
        Box<String> box = new Box<>("Tripod");
        System.out.println(box.get());
    }
}
Output
Tripod
Line-by-line explanation
  1. Line 13Evaluates box.get() and prints Tripod.
Example 2

A generic class preserves its value type

Java
public class TypedStorage  {
    static class Box<T>  {
        private final T value;
        Box(T value)  {
            this.value = value;
        }
        T get()  {
            return value;
        }
    }
    public static void main(String[] args)  {
        Box<String> word = new Box<>("Map");
        Box<Integer> number = new Box<>(8);
        System.out.println(word.get().length());
        System.out.println(number.get() + 2);
    }
}
Output
3
10
Line-by-line explanation
  1. Line 14Evaluates word.get().length() and prints 3.
  2. Line 15Evaluates number.get() + 2 and prints 10.
Example 3

An upper-bounded wildcard reads numbers

Java
public class NumberProducer  {
    static int sum(java.util.List<? extends Number> values)  {
        int total = 0;
        for (Number value : values) total += value.intValue();
        return total;
    }
    public static void main(String[] args)  {
        System.out.println(sum(java.util.List.of(3, 6)));
    }
}
Output
9
Line-by-line explanation
  1. Line 8Evaluates sum(java.util.List.of(3, 6)) and prints 9.

Java FAQ

Can I write new T() inside a generic class?

Not generally. The type parameter does not provide a known constructor at runtime. Pass a factory such as Supplier<T> when creation is part of the design, or accept already constructed values. A bound supplies member contracts, not a universal no-argument constructor.

Why is List<Integer> not List<Number>?

Allowing that assignment would let a Number-typed caller insert a Double into an integer-only list. Invariance preserves the element promise. Use an appropriate wildcard when an operation needs only safe reading or only safe consumption.

Can a generic class have more than one type parameter?

Yes. List them separated by commas: class Pair<K, V> has a key type and a value type, and Map<K, V> works the same way. Each one can have its own bound, such as <K extends Comparable<K>, V>.

Can a static method use the class's type parameter?

No. A class's type parameter belongs to each instance, such as one Box<String> or another Box<Integer>, while a static method belongs to the class as a whole. A static method that needs a type parameter declares its own, as in static <T> Box<T> of(T value).

What does <T extends Comparable<T>> mean?

It's a bound that allows only types that can be compared with themselves, such as String, Integer or LocalDate. Inside the method you can then call a.compareTo(b) safely, which is how a generic max or sort method knows how to order its values.

Java Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

What is T in Box<T>?

Multiple choicemedium

Which declaration can read Integer-list elements as Number?

Java Quiz

2 questions to check what you learned.

  1. Can a generic argument be int?

    • No, use Integer
    • Yes when the diamond is empty
    • Only inside a private class
  2. Why avoid raw Box variables?

    • They lose useful generic checks and can introduce unchecked operations
    • They are always faster
    • They create a different constructor