Java Trainer

πŸŽ“ Java Learning Module

Type Safety Magic: Understanding Generics in Java

πŸ‘‹ Meet Your Java Trainer!

Hi! I'm Remsey, your Java instructor. Ready to master Java together?

🎧 Listen to the Audio Version

Prefer to learn by listening? Hit play below and watch the audio come to life!

70%
0:00 / 0:00

πŸ”‘ Generics Quick Reference

πŸ”‘ Generics = Type Placeholders

They let you write code that works with any type, safely.

🧺 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

πŸ”§ Make 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());

🎯 Make a Generic Method:

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

Use it:

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

🧠 Summary:

  • β€’ <T> = type placeholder
  • β€’ Works with any object type
  • β€’ Adds flexibility and safety
πŸŽ‰ Done! Generics = Reusable + Type-Safe Magic

Today we're diving into Generics β€” one of Java's most powerful features that makes your code safer, cleaner, and more reusable. 🎯

Imagine you have a magic box. You can put anything in it β€” apples, books, or even unicorns. πŸ¦„ But what if you want to make sure only apples go in? That's where Generics come in!

Generics let you write type-safe code that works with different types while catching errors at compile-time instead of runtime.

What Are Generics?

Generics allow you to write classes, interfaces, and methods that work with any type, while still maintaining type safety. Think of them as blueprints with placeholders.

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's like saying "T can be any type you want!" When you use the Box, you specify what type T should be:

🎯 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 had to use Object and cast everywhere. It was messy and error-prone! 😱

❌ 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're perfect for utility functions that 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 restrict what types can be used. You can do this with bounds!

🎯 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
    }
}

Real-World Example: Pair Class

Let's create a generic Pair class that holds two values of potentially different types:

πŸ‘« Generic Pair Class
public class Pair<K, V> {
    private K key;
    private V value;
    
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() {
        return key;
    }
    
    public V getValue() {
        return value;
    }
    
    public static void main(String[] args) {
        // Name and Age
        Pair<String, Integer> person = new Pair<>("Alice", 25);
        System.out.println(person.getKey() + " is " + person.getValue() + " years old");
        
        // Country and Capital
        Pair<String, String> country = new Pair<>("Netherlands", "Amsterdam");
        System.out.println(country.getKey() + " β†’ " + country.getValue());
        
        // Coordinates
        Pair<Double, Double> coords = new Pair<>(52.3676, 4.9041);
        System.out.println("πŸ“ (" + coords.getKey() + ", " + coords.getValue() + ")");
    }
}
πŸ“€ Expected Output:
Alice is 25 years old
Netherlands β†’ Amsterdam
πŸ“ (52.3676, 4.9041)

🧩 Mini-Challenge

Create a generic Container class that can hold a list of items and has methods to add items and get the count. Try it with different types!

πŸ’ͺ 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: Create a generic Stack<T> class with methods push(T item), pop(), and isEmpty(). Use an ArrayList internally.

πŸ’‘ Hints
  • Use ArrayList<T> to store items
  • Push adds to the end, pop removes from the end
  • Check if the list is empty before popping
βœ… 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: Create generic methods findMin and findMax that work with any Comparable type. Test with Integers and Strings.

πŸ’‘ Hints
  • Use <T extends Comparable<T>>
  • Loop through array and use compareTo()
  • compareTo returns negative if less, 0 if equal, positive if greater
βœ… 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: Create a generic Cache<K, V> class that stores key-value pairs using a HashMap. Add methods to put, get, and check if 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, and most collections use Generics. Understanding them is key to writing modern, type-safe Java code! πŸš€