JV

Java Fundamentals

19 lessons

Progress0%
1. Introduction to Java
1What is Java?
2. Variables and Data Types
1Primitive Types
3. Control Flow
ConditionalsLoops
4. Methods
Defining MethodsMethod Overloading
5. Object-Oriented Programming
Classes and ObjectsInheritanceInterfaces and Abstract Classes
6. Collections
ArrayList and LinkedListHashMap and HashSet
7. Exception Handling
Checked & Unchecked Exceptionstry-with-resources & Custom Exceptions
8. Generics
Generic Classes & MethodsWildcards & Type Erasure
9. Modern Java
Lambdas & Functional InterfacesStream API & Optional
10. File I/O
java.nio.file APIBuffered I/O & try-with-resources
All Tutorials
JavaGenerics
Lesson 14 of 19 min
Chapter 8 · Lesson 1

Generic Classes & Methods

Generics, introduced in Java 5, allow classes, interfaces, and methods to operate on typed parameters. They provide compile-time type safety and eliminate the need for casts.

Type Parameters

By convention single uppercase letters are used:

  • T — Type (general purpose)
  • E — Element (collections)
  • K, V — Key, Value (maps)
  • R — Return type (functions)
  • A, B — additional type parameters on a pair/tuple

Generic Class Syntax

java
public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get()           { return value; }
}

At use-site: Box<String> box = new Box<>(); (diamond operator <> infers the type argument).

Generic Methods

A method can declare its own type parameters independently of the class:

java
public static <T> T firstOrNull(List<T> list) {
    return list.isEmpty() ? null : list.get(0);
}

The type parameter <T> appears before the return type.

Bounded Type Parameters

Restrict what types can be substituted with extends:

java
public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

Multiple bounds use &: <T extends Cloneable & Serializable>. Only one class bound is allowed and it must come first.

Generic Interfaces

java
public interface Transformer<I, O> {
    O transform(I input);
}

Raw Types — Avoid Them

A raw type is a generic class used without its type argument: List list = new ArrayList();. Raw types exist only for backwards compatibility with pre-Java-5 code. They bypass the type system, produce heap pollution, and generate compiler warnings. Always use parameterized types.

Code Examples

Generic Pair classjava
public class Pair<A, B> {
    private final A first;
    private final B second;

    public Pair(A first, B second) {
        this.first  = first;
        this.second = second;
    }

    public A getFirst()  { return first;  }
    public B getSecond() { return second; }

    // Generic static factory — convenient alternative to constructor
    public static <X, Y> Pair<X, Y> of(X x, Y y) {
        return new Pair<>(x, y);
    }

    @Override
    public String toString() {
        return "(" + first + ", " + second + ")";
    }

    public static void main(String[] args) {
        Pair<String, Integer> nameAge = Pair.of("Alice", 30);
        System.out.println("Pair   : " + nameAge);
        System.out.println("First  : " + nameAge.getFirst());
        System.out.println("Second : " + nameAge.getSecond());

        Pair<Double, Double> coords = new Pair<>(51.5074, -0.1278);
        System.out.println("Coords : " + coords);

        // Type safety — the following would be a compile error:
        // String s = nameAge.getSecond();  // int, not String
    }
}

Pair<A,B> is type-safe at compile time. The diamond operator <> on the right side lets the compiler infer the type arguments, keeping the code concise.

Generic utility method with boundjava
import java.util.Arrays;
import java.util.List;

public class GenericMethods {

    // <T extends Comparable<T>> means T must support compareTo
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }

    // Returns the first element, or null for an empty list
    public static <T> T firstOrNull(List<T> list) {
        return list.isEmpty() ? null : list.get(0);
    }

    // Swaps two elements in an array (generic method on arrays)
    public static <T> void swap(T[] arr, int i, int j) {
        T tmp  = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    public static void main(String[] args) {
        // max works for any Comparable
        System.out.println(max(3, 7));          // Integer
        System.out.println(max("apple", "banana")); // String
        System.out.println(max(3.14, 2.71));    // Double

        // firstOrNull
        List<String> names = Arrays.asList("Carol", "Dave", "Eve");
        System.out.println(firstOrNull(names));
        System.out.println(firstOrNull(List.of()));

        // swap
        String[] words = {"one", "two", "three"};
        swap(words, 0, 2);
        System.out.println(Arrays.toString(words));
    }
}

The bound <T extends Comparable<T>> lets us call compareTo() inside the method. Without a bound only methods from Object would be accessible on T.

Generic Stack implementationjava
import java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.List;

public class GenericStack<E> {
    private final List<E> elements = new ArrayList<>();

    public void push(E item) {
        elements.add(item);
    }

    public E pop() {
        if (isEmpty()) throw new EmptyStackException();
        return elements.remove(elements.size() - 1);
    }

    public E peek() {
        if (isEmpty()) throw new EmptyStackException();
        return elements.get(elements.size() - 1);
    }

    public boolean isEmpty() { return elements.isEmpty(); }
    public int size()        { return elements.size(); }

    @Override
    public String toString() { return elements.toString(); }

    public static void main(String[] args) {
        // Stack of Strings
        GenericStack<String> strStack = new GenericStack<>();
        strStack.push("first");
        strStack.push("second");
        strStack.push("third");
        System.out.println("Stack  : " + strStack);
        System.out.println("Peek   : " + strStack.peek());
        System.out.println("Pop    : " + strStack.pop());
        System.out.println("After  : " + strStack);

        // Stack of Integers — same class, different type argument
        GenericStack<Integer> intStack = new GenericStack<>();
        for (int i = 1; i <= 4; i++) intStack.push(i * 10);
        System.out.println("IntStack: " + intStack);
        System.out.println("Size    : " + intStack.size());
    }
}

GenericStack<E> is a fully type-safe container. The same implementation works for any type without duplication or casting. Note how GenericStack<String> and GenericStack<Integer> are completely separate types at compile time.

Quick Quiz

1. What does the diamond operator `<>` do in `new ArrayList<>()`?

2. What is a raw type?

3. Where does a generic method's type parameter declaration appear?

4. Given `<T extends Comparable<T> & Serializable>`, how many class bounds can T have?

Was this lesson helpful?

PreviousNext