Java Trainer

πŸŽ“ Java leeronderdeel

Type-safe reuse: understanding Generics in Java

πŸ‘‹ Meet your Java trainer

Hi, I'm Remsey. In this module you'll see how Generics make your code safer, cleaner, and easier to reuse.

🎧 Audio version

Prefer listening? Start the audio below and follow the explanation along.

70%
0:00 / 0:00

πŸ”‘ Generics quick reference

πŸ”‘ Generics = type placeholders

You write code that works with different types while staying compile-time safe.

🧺 Without Generics:

ArrayList list = new ArrayList();  // no type safety
list.add("hello");
list.add(123);  // 😬 might cause errors later

βœ… With Generics:

ArrayList<String> list = new ArrayList<>();
list.add("hello");         // βœ…
list.add(123);             // ❌ compile error

πŸ”§ Build your own generic class:

public class Box<T> {
    T value;

    public void set(T val) { value = val; }
    public T get() { return value; }
}

Use it:

Box<String> b = new Box<>();
b.set("Java");
System.out.println(b.get());

🎯 Build a generic method:

public static <T> void printItem(T item) {
    System.out.println(item);
}

Use it:

printItem("Hello");
printItem(42);

🧠 Summary:

  • β€’ <T> = type parameter
  • β€’ Works with any object type
  • β€’ Adds flexibility and safety
πŸŽ‰ Done! Generics = reusable and type-safe

What are Generics?

Generics let you write classes, interfaces, and methods that work with different types while keeping type safety intact.

Think of them as reusable templates: the structure stays the same, but the type is filled in later.

In a webshop, that means one cart or repository can work for books, food, or electronics without sacrificing compile-time safety.

Let's start with a simple example β€” a Box that can hold anything:

πŸ“¦ Generic Box Class
public class Box<T> {
    private T item;
    
    public void put(T item) {
        this.item = item;
    }
    
    public T get() {
        return item;
    }
}

The <T> is a type parameter. It tells the Box to be filled with a concrete type later.

🎯 Using Generic Box
public class Main {
    public static void main(String[] args) {
        // A box that holds Strings
        Box<String> stringBox = new Box<>();
        stringBox.put("Hello Generics!");
        System.out.println(stringBox.get());
        
        // A box that holds Integers
        Box<Integer> intBox = new Box<>();
        intBox.put(42);
        System.out.println(intBox.get());
        
        // Type safety! This won't compile:
        // stringBox.put(123); ❌ Error!
    }
}
πŸ“€ Expected Output:
Hello Generics!
42

Why Use Generics?

Before Generics (pre-Java 5), you used Object everywhere and had to cast manually. That was harder to read and easier to break.

❌ Without Generics (The Old Way)
public class OldBox {
    private Object item;
    
    public void put(Object item) {
        this.item = item;
    }
    
    public Object get() {
        return item;
    }
}

// Usage - lots of casting!
OldBox box = new OldBox();
box.put("Hello");
String text = (String) box.get(); // Manual cast 😒

box.put(123);
String oops = (String) box.get(); // Runtime error! πŸ’₯
βœ… With Generics (The Modern Way)
Box<String> box = new Box<>();
box.put("Hello");
String text = box.get(); // No cast needed! 😊

// box.put(123); ❌ Compile error - caught early!

Generic methods

You can also create generic methods. They are useful for utility functions that need to work with any type.

πŸ”§ Generic Method Example
public class ArrayUtils {
    
    // Generic method to print any type of array
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
    
    // Generic method to swap elements
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    public static void main(String[] args) {
        Integer[] numbers = {1, 2, 3, 4, 5};
        String[] words = {"Java", "is", "awesome"};
        
        printArray(numbers); // Works with Integer[]
        printArray(words);   // Works with String[]
        
        swap(numbers, 0, 4);
        printArray(numbers); // 5 2 3 4 1
    }
}
πŸ“€ Expected Output:
1 2 3 4 5
Java is awesome
5 2 3 4 1

Bounded type parameters

Sometimes you want to limit generics to a subset of types. Then you use bounds, for example <T extends Number>.

🎯 Upper Bounded Type - Numbers Only
public class NumberBox<T extends Number> {
    private T number;
    
    public NumberBox(T number) {
        this.number = number;
    }
    
    public double getDoubleValue() {
                    return number.doubleValue(); // Works because T extends Number
    }
    
    public static void main(String[] args) {
        NumberBox<Integer> intBox = new NumberBox<>(42);
        NumberBox<Double> doubleBox = new NumberBox<>(3.14);
        
        System.out.println(intBox.getDoubleValue());    // 42.0
        System.out.println(doubleBox.getDoubleValue()); // 3.14
        
        // NumberBox<String> won't compile! βœ… Type safety
    }
}

Webshop tip: in a webshop you can use generics for a <T extends Product> shopping cart or a Repository<T> for inventory. That helps prevent customers, orders, or promo codes from landing in the wrong list.

Real-world example: a food webshop cart

A webshop is one of the clearest real-world uses for Generics. You want a cart that only accepts products, but still works for different kinds of products like groceries, drinks, or digital items.

πŸ‘« Food webshop cart with generics
public class Main {
    public static void main(String[] args) {
        ShoppingCart<Product> cart = new ShoppingCart<>();
        cart.addItem(new FoodProduct("Sourdough Bread", 3.00), 2);
        cart.addItem(new FoodProduct("Organic Milk", 2.00), 3);
        cart.addItem(new FoodProduct("Pasta", 1.50), 4);

        System.out.println("Items in cart: " + cart.getItemCount());
        System.out.println("Total: €" + cart.getTotal());
    }
}

class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() { return name; }
    public double getPrice() { return price; }
}

class FoodProduct extends Product {
    public FoodProduct(String name, double price) {
        super(name, price);
    }
}

class ShoppingCart<T extends Product> {
    private java.util.List<CartItem<T>> items = new java.util.ArrayList<>();

    public void addItem(T product, int quantity) {
        items.add(new CartItem<>(product, quantity));
    }

    public int getItemCount() {
        return items.size();
    }

    public double getTotal() {
        double total = 0;
        for (CartItem<T> item : items) {
            total += item.getQuantity() * item.getProduct().getPrice();
        }
        return total;
    }
}

class CartItem<T extends Product> {
    private T product;
    private int quantity;

    public CartItem(T product, int quantity) {
        this.product = product;
        this.quantity = quantity;
    }

    public T getProduct() { return product; }
    public int getQuantity() { return quantity; }
}
πŸ“€ Expected Output:
Items in cart: 3
Total: €18.00

🧩 Mini challenge

Build a generic Container class that can store a list of items. Try it with different types and think about a webshop: products, coupons, or shipping options.

πŸ’ͺ Challenge - Generic Container
import java.util.ArrayList;
import java.util.List;

public class Container<T> {
    private List<T> items;
    
    public Container() {
        items = new ArrayList<>();
    }
    
    // TODO: Add method to add an item
    // TODO: Add method to get count
    // TODO: Add method to display all items
    
    public static void main(String[] args) {
        // Test with Strings
        Container<String> fruits = new Container<>();
        // fruits.add("Apple");
        // fruits.add("Banana");
        
        // Test with Integers
        Container<Integer> numbers = new Container<>();
        // numbers.add(1);
        // numbers.add(2);
    }
}

πŸ’ͺ Practice Exercises

Test your understanding of Generics with these exercises.

πŸ“š Exercise 1: Generic Stack

Task: Build a generic Stack<T> class with the methods push(T item), pop(), and isEmpty(). Use an internal ArrayList.

πŸ’‘ Hints
  • Use ArrayList<T> to store items
  • push adds to the end, pop removes from the end
  • Check whether the list is empty before removing anything
βœ… Solution
πŸ“š Exercise 1 solution
import java.util.ArrayList;

public class Stack<T> {
    private ArrayList<T> items;
    
    public Stack() {
        items = new ArrayList<>();
    }
    
    public void push(T item) {
        items.add(item);
        System.out.println("πŸ“₯ Pushed: " + item);
    }
    
    public T pop() {
        if (isEmpty()) {
            System.out.println("❌ Stack is empty!");
            return null;
        }
        T item = items.remove(items.size() - 1);
        System.out.println("πŸ“€ Popped: " + item);
        return item;
    }
    
    public boolean isEmpty() {
        return items.isEmpty();
    }
    
    public static void main(String[] args) {
        Stack<String> stack = new Stack<>();
        stack.push("First");
        stack.push("Second");
        stack.push("Third");
        stack.pop();
        stack.pop();
    }
}

πŸ”’ Exercise 2: Find Min & Max

Task: Build the generic methods findMin and findMax that work with any Comparable type. Test with Integer and String.

πŸ’‘ Hints
  • Use <T extends Comparable<T>>
  • Loop through the array and use compareTo()
  • compareTo returns a negative, zero, or positive value
βœ… Solution
πŸ”’ Exercise 2 solution
public class MinMax {
    
    public static <T extends Comparable<T>> T findMin(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        
        T min = array[0];
        for (T item : array) {
            if (item.compareTo(min) < 0) {
                min = item;
            }
        }
        return min;
    }
    
    public static <T extends Comparable<T>> T findMax(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        
        T max = array[0];
        for (T item : array) {
            if (item.compareTo(max) > 0) {
                max = item;
            }
        }
        return max;
    }
    
    public static void main(String[] args) {
        Integer[] numbers = {5, 2, 9, 1, 7};
        System.out.println("Min: " + findMin(numbers));
        System.out.println("Max: " + findMax(numbers));
        
        String[] words = {"zebra", "apple", "mango"};
        System.out.println("Min: " + findMin(words));
        System.out.println("Max: " + findMax(words));
    }
}

πŸ’Ύ Exercise 3: Generic Cache

Task: Build a generic Cache<K, V> class that stores key-value pairs with a HashMap. Add methods to store, retrieve, and check whether a key exists.

πŸ’‘ Hints
  • Use HashMap<K, V> internally
  • Method signatures: put(K key, V value), get(K key), contains(K key)
  • HashMap methods: put(), get(), containsKey()
βœ… Solution
πŸ’Ύ Exercise 3 solution
import java.util.HashMap;

public class Cache<K, V> {
    private HashMap<K, V> cache;
    
    public Cache() {
        cache = new HashMap<>();
    }
    
    public void put(K key, V value) {
        cache.put(key, value);
        System.out.println("πŸ’Ύ Cached: " + key + " β†’ " + value);
    }
    
    public V get(K key) {
        if (contains(key)) {
            System.out.println("βœ… Cache hit: " + key);
            return cache.get(key);
        }
        System.out.println("❌ Cache miss: " + key);
        return null;
    }
    
    public boolean contains(K key) {
        return cache.containsKey(key);
    }
    
    public static void main(String[] args) {
        Cache<String, Integer> ageCache = new Cache<>();
        
        ageCache.put("Alice", 25);
        ageCache.put("Bob", 30);
        
        System.out.println("\nRetrieving values:");
        ageCache.get("Alice");
        ageCache.get("Charlie");
    }
}

πŸ’‘ Pro tip: Generics are everywhere in Java: ArrayList, HashMap, API clients, and webshop structures use them constantly. Understanding why you choose <T extends Product>, ? extends, or ? super is essential for modern, type-safe Java code.