root@coding-prodigies:~# โ–Š
// lesson 2 of 11 ยท 18 min

Generics

โš‘ Report an issue with this lesson
public class Box<T> {
    private T contents;

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

Box<String> stringBox = new Box<>();
stringBox.set("hello");
String value = stringBox.get(); // no cast needed

Generics give you compile-time type safety without writing a separate class for every type. Bounded generics restrict what's allowed:

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

Generics are erased at compile time -- a process called type erasure. Box<String> and Box<Integer> are both just Box at runtime; the compiler inserts the casts and checks for you at compile time, then throws that type information away. This is why you can't write new T(), can't do instanceof Box<String>, and can't create an array of a generic type directly -- the JVM literally has no way to know T at runtime to check against. It's also why generics are backward-compatible with older Java code compiled before generics existed: the bytecode looks the same either way, just with or without compiler-inserted casts.

Wildcards describe generic types you don't need to name precisely. List<? extends Number> ("upper bounded") accepts a list of Number or any subtype, and you can safely read Numbers out of it -- but you can't add to it, since the compiler can't guarantee what specific subtype the list actually holds. List<? super Integer> ("lower bounded") is the mirror image: you can safely add Integers to it, but reading only guarantees you get back an Object. This is the "PECS" rule -- Producer Extends, Consumer Super -- and it's the reasoning behind method signatures like Collections.copy(List<? super T> dest, List<? extends T> src) in the standard library.

static double sum(List<? extends Number> nums) {
    double total = 0;
    for (Number n : nums) {
        total += n.doubleValue(); // reading is fine
    }
    // nums.add(5); // would NOT compile -- compiler can't guarantee nums is List<Integer>
    return total;
}

A subtle gotcha that trips up even experienced Java developers: generics are not covariant the way arrays are. List<Integer> is not a List<Number>, even though Integer is a Number -- if it were allowed, you could put a Double into what was declared as a List<Integer> through a List<Number> reference, and the compile-time type safety generics are supposed to give you would be gone. This is exactly the bug that raw Java arrays (which *are* covariant) can produce at runtime with an ArrayStoreException -- generics trade that runtime surprise for a compile-time error instead.

Try it yourself

Exercise: Complete the generic Box<T> class's set() and get() methods, then create a Box<String>, set it to "hello", and print get().
Expected output:
hello
java
Output

      
    

Run your code and get it working before marking this lesson complete.

// free preview

8 more lessons โ€” including Project: build a simple task scheduler โ€” and a final exam plus a certificate are waiting.

Unlock the full course โ€” $99.99