JVM / Java / Generics / Generic Classes and Methods

1. Declaring a Generic Class

A generic class declares type parameters in angle brackets, like class Box, letting one definition work with any reference type while keeping compile-time checking -- no casts needed, wrong types rejected. T is just a placeholder, replaced by a concrete type argument like Box at the point of use.
 
public class DeclaringGenericClassExample {
    public static void main(String[] args) {
        
        Box<String> strinBox = new Box<>("Hello");
        System.out.println(strinBox.get());  // no cast needed

        // stringBox.set(42);  // compile error: incompatible types
    }
}

class Box<T> {
    private T value;

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

    T get() {
        return value;
    }
}

2. Generic Methods

A generic method declares its own type parameter before the return type -- static T firstOf(List list) -- independent of whether the enclosing class is generic. The type argument is almost always inferred from the call site, so a single static method works across every type with no boilerplate.
 
import java.util.List;

/*
 * ==========================================================
 * GENERIC METHOD EXAMPLE
 * ==========================================================
 *
 * This example demonstrates a method-level type parameter:
 *
 *     static <T> T firstOf(List<T> list)
 *
 * The declaration "<T>" introduces a type variable that
 * exists only within the scope of this method.
 *
 * The compiler infers T from the argument passed:
 *
 *     List<String>  -> T becomes String
 *     List<Integer> -> T becomes Integer
 *
 * Therefore the same method can safely return either a
 * String, Integer, Double, or any other type.
 *
 * Without the "<T>" declaration, the compiler would not know
 * what T means and the code would not compile.
 */

public class GenericMethods {

    public static void main(String[] args) {

        String firstName =
                MyList.firstOf(List.of("Alice", "Bob"));

        Integer firstNumber =
                MyList.firstOf(List.of(1, 2, 3));

        Double firstDecimal =
                MyList.firstOf(List.of(3.14, 2.71));

        System.out.println(firstName);       // Alice
        System.out.println(firstNumber);     // 1
        System.out.println(firstDecimal);    // 3.14

        // Type inference:
        // T = String
        String name = MyList.firstOf(List.of("Tom", "Jerry"));

        // T = Integer
        Integer number = MyList.firstOf(List.of(10, 20, 30));

        System.out.println(name + " " + number);
    }
}

/*
 * ==========================================================
 * NON-GENERIC CLASS WITH A GENERIC METHOD
 * ==========================================================
 */
class MyList {

    /*
     * "<T>" declares a method-level type parameter.
     *
     * The method returns the same type that the list contains.
     */
    static <T> T firstOf(List<T> list) {
        return list.get(0);
    }

    /*
     * ------------------------------------------------------
     * THIS DOES NOT COMPILE:
     * ------------------------------------------------------
     *
     * static T firstOf(List<T> list) {
     *     return list.get(0);
     * }
     *
     * Compiler error:
     *      cannot find symbol: class T
     *
     * Reason:
     *      T has never been declared.
     *
     * A type variable must be declared either:
     *
     * 1. By the class
     *      class MyList<T> { ... }
     *
     * OR
     *
     * 2. By the method
     *      static <T> T firstOf(...) { ... }
     *
     * Without either declaration, T has no meaning.
     */
}

/*
 * ==========================================================
 *  WHY NOT MAKE THE CLASS GENERIC?
 * ==========================================================
 * 
 * You could write:
 *
 *      class MyList<T> {
 *          T firstOf(List<T> list) {
 *              return list.get(0);
 *          }
 *      }
 * 
 * but then you'd need two instances:
 * 
 *      MyList<String> ml = new MyList<>();
 *      String s = ml.firstOf(List.of("Alice", "Bob"));
 * 
 *      MyList<Integer> ml2 = new MyList<>();
 *      Integer n = ml2.firstOf(List.of(1, 2, 3));
 * 

 */

3. Multiple Type Parameters

A class or method can declare multiple type parameters, separated by commas -- class Pair. By convention: T for a general type, E for a collection element, K`/`V for a map's key/value, R for a return type -- any identifier works, but conventions aid readability.
 
public class MultipleParameters {
    public static void main(String[] args) {
        
        Pair<String, Integer> item = new Pair<>("Ann", 30);
        System.out.println(item.getValue());
    }
}

class Pair<K, V> {
    private final K key;
    private final V value;
    
    Pair(K key, V value) { 
        this.key = key; 
        this.value = value; 
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}