Generics Basics

Su Suriya Ravichandran Updated 14 Nov 2026
7 min read ·Lesson 87 of 133

Generics let a class, interface, or method work with any type while keeping full compile-time type checking. Instead of writing one list for strings, another for integers, and another for products, you write List<E> once, and users choose the type: List<String>, List<Integer>, List<Product>. The compiler then guarantees that only the right kind of element goes in and that every element coming out already has the right type. Every collection in the previous lessons is generic.

The Problem Generics Solve

Before Java 5, collections held plain Object references:

List names = new ArrayList();          // a "raw" list: holds any Object
names.add("Ada");
names.add(42);                         // nothing stops this

String first = (String) names.get(0);  // cast required
String second = (String) names.get(1); // ClassCastException at runtime

Two problems are visible here:

  1. No type safety. The compiler cannot stop an Integer from entering a list meant for names. The mistake only surfaces later, at runtime, far from where it was made.
  2. Casts everywhere. Every read needs a cast, which is noisy and is exactly where the failure happens.

With generics:

List<String> names = new ArrayList<>();
names.add("Ada");
names.add(42);                 // compile error: int cannot be converted to String

String first = names.get(0);   // no cast needed

The error moves from runtime to compile time, and the casts disappear. That is the central benefit of generics: errors are caught by the compiler instead of by your users.

Reading Generic Types

List<String>                       // a List whose elements are Strings
Map<String, Integer>               // a Map from String keys to Integer values
Map<String, List<Order>>           // a Map from String to lists of Orders
Optional<LocalDate>                // an Optional that may hold a LocalDate
Comparator<Product>                // a Comparator that compares Products
TermExampleMeaning
Generic typeList<E>A type declared with type parameters
Type parameterE in List<E>A placeholder name used inside the declaration
Type argumentString in List<String>The actual type supplied by the user
Parameterized typeList<String>A generic type with its arguments filled in

By convention, type parameters are single uppercase letters:

LetterUsual meaning
TType
EElement (collections)
K, VKey, Value (maps)
RReturn type (functions)
S, UAdditional types

The Diamond Operator

The type arguments on the right side of an assignment can usually be inferred from the left:

Map<String, List<Integer>> scores = new HashMap<String, List<Integer>>();   // verbose
Map<String, List<Integer>> scores = new HashMap<>();                        // diamond
var scores = new HashMap<String, List<Integer>>();                          // var: type on the right

<> (the "diamond") tells the compiler to infer the arguments. With var, the type arguments must appear on the right instead, because there is nothing on the left to infer from.

Writing a Generic Class

Declare type parameters in angle brackets after the class name, then use them like ordinary types inside the class:

public class Box<T> {
    private T value;

    public Box(T value) { this.value = value; }

    public T get() { return value; }

    public void set(T value) { this.value = value; }

    public boolean isEmpty() { return value == null; }
}
Box<String> label = new Box<>("Fragile");
String text = label.get();         // T is String here: no cast

Box<Integer> count = new Box<>(3);
int n = count.get();               // T is Integer: unboxed automatically

label.set(99);                     // compile error: int cannot be converted to String

One class definition serves every type. A record can be generic too:

public record Pair<A, B>(A first, B second) { }

Pair<String, Integer> p = new Pair<>("Ada", 36);
String name = p.first();
int age = p.second();

Generic classes have their own lesson.

Writing a Generic Method

A method can declare its own type parameters, placed before the return type:

public static <T> T firstOrDefault(List<T> list, T fallback) {
    return list.isEmpty() ? fallback : list.get(0);
}
String s = firstOrDefault(List.of("x", "y"), "none");     // T inferred as String
Integer i = firstOrDefault(List.<Integer>of(), 0);         // T inferred as Integer

The compiler infers T from the arguments at each call. The method works for any type and returns exactly the type that was passed in. Generic methods have their own lesson.

Bounded Type Parameters (Preview)

Sometimes a generic method needs to call methods on T. A bound restricts which types are allowed:

public static <T extends Comparable<T>> T maxOf(List<T> items) {
    T best = items.get(0);
    for (T item : items) {
        if (item.compareTo(best) > 0) best = item;
    }
    return best;
}

T extends Comparable<T> means "any type that can be compared with itself." Inside the method, item.compareTo(best) is therefore allowed. Calling maxOf with a list of a type that is not Comparable is a compile error. Bounded types and wildcards (? extends, ? super) are covered in later lessons.

Generics Work Only with Reference Types

Type arguments must be classes or interfaces, not primitives:

List<int> numbers;          // compile error
List<Integer> numbers;      // correct: autoboxing converts between int and Integer

For large amounts of numeric data, boxing has a cost: each Integer is a separate object. Primitive arrays (int[]) or primitive streams (IntStream) avoid it.

Type Erasure (In Brief)

Generics are a compile-time feature. After checking types, the compiler erases the type arguments: at runtime a List<String> and a List<Integer> are both just List. This design kept new generic code compatible with older libraries, and it has a few visible consequences:

List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());   // true: same runtime class

// Not allowed, because T is unknown at runtime:
// new T()               cannot create an instance of a type parameter
// new T[10]             cannot create a generic array
// x instanceof List<String>   cannot test a parameterized type (except with an unbounded ?)

Type erasure has its own lesson. For everyday code, the rule is simple: let the compiler check the types, and avoid working around it.

Raw Types: Do Not Use Them

Using a generic type without type arguments creates a raw type:

List names = new ArrayList();   // raw: warning "unchecked call"

Raw types exist only for compatibility with pre-2004 code. They switch off type checking for that variable, bringing back the runtime ClassCastExceptions that generics were designed to prevent. Always supply type arguments (or <>), and treat "unchecked" compiler warnings as real problems.

Practical Example: A Generic Page of Results

APIs often return results one page at a time. The same paging logic applies whether the items are courses, users, or orders, so it is a natural candidate for generics.

import java.util.*;
import java.util.function.Function;

public class Gen {
    static final class Page<T> {
        private final List<T> items;
        private final int number, totalPages;

        Page(List<T> items, int number, int totalPages) {
            this.items = List.copyOf(items);
            this.number = number;
            this.totalPages = totalPages;
        }

        List<T> items() { return items; }
        boolean hasNext() { return number < totalPages; }

        <R> Page<R> map(Function<T, R> f) {
            List<R> out = new ArrayList<>();
            for (T item : items) out.add(f.apply(item));
            return new Page<>(out, number, totalPages);
        }

        @Override
        public String toString() { return "Page " + number + "/" + totalPages + " " + items; }
    }

    static <T> Page<T> paginate(List<T> all, int pageNumber, int pageSize) {
        int total = Math.max(1, (all.size() + pageSize - 1) / pageSize);
        int from = Math.min((pageNumber - 1) * pageSize, all.size());
        int to = Math.min(from + pageSize, all.size());
        return new Page<>(all.subList(from, to), pageNumber, total);
    }

    static <T extends Comparable<T>> T maxOf(List<T> items) {
        T best = items.get(0);
        for (T item : items) {
            if (item.compareTo(best) > 0) best = item;
        }
        return best;
    }

    record Course(String code, int hours) { }

    public static void main(String[] args) {
        List<Course> catalog = List.of(
            new Course("JAVA101", 20), new Course("DOCK210", 12), new Course("DJ201", 30),
            new Course("CPP301", 40), new Course("BASH110", 8));

        Page<Course> p1 = paginate(catalog, 1, 2);
        System.out.println(p1);
        Page<String> codes = p1.map(Course::code);
        System.out.println(codes + ", has next: " + codes.hasNext());

        Page<Integer> hours = paginate(catalog, 3, 2).map(Course::hours);
        System.out.println(hours + ", has next: " + hours.hasNext());

        System.out.println("Last code alphabetically: " + maxOf(List.of("JAVA101", "DOCK210", "DJ201")));
        System.out.println("Most hours: " + maxOf(List.of(20, 12, 30, 40, 8)));
    }
}

Output:

Page 1/3 [Course[code=JAVA101, hours=20], Course[code=DOCK210, hours=12]]
Page 1/3 [JAVA101, DOCK210], has next: true
Page 3/3 [8], has next: false
Last code alphabetically: JAVA101
Most hours: 40

Page<T> is a generic class: one definition holds a page of courses, strings, or integers, and the compiler knows the element type in each case. paginate is a generic method whose T is inferred from the list passed in, so paginate(catalog, 1, 2) returns a Page<Course>. The map method introduces a second type parameter, R, letting a Page<Course> become a Page<String> or Page<Integer> while keeping the page number and total. maxOf uses a bounded type parameter so it can call compareTo, and it works for both strings and integers because both are Comparable. No casts appear anywhere, and passing the wrong type to any of these methods would be caught at compile time.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Generics Basics and get answers drawn from it.

Signed-in readers only.