Hi, I'm Remsey. In this module you'll see how Generics make your code safer, cleaner, and easier to reuse.
Prefer listening? Start the audio below and follow the explanation along.
You write code that works with different types while staying compile-time safe.
ArrayList list = new ArrayList(); // no type safety
list.add("hello");
list.add(123); // π¬ might cause errors later
ArrayList<String> list = new ArrayList<>();
list.add("hello"); // β
list.add(123); // β compile error
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());
public static <T> void printItem(T item) {
System.out.println(item);
}
Use it:
printItem("Hello");
printItem(42);
<T> = type parameter
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:
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.
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!
}
}
Before Generics (pre-Java 5), you used Object everywhere and had to cast manually. That was harder to read and easier to break.
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! π₯
Box<String> box = new Box<>();
box.put("Hello");
String text = box.get(); // No cast needed! π
// box.put(123); β Compile error - caught early!
You can also create generic methods. They are useful for utility functions that need to work with any type.
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
}
}
Sometimes you want to limit generics to a subset of types. Then you use bounds, for example <T extends Number>.
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.
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.
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; }
}
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.
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);
}
}
Test your understanding of Generics with these exercises.
Task: Build a generic Stack<T> class
with the methods push(T item), pop(), and isEmpty(). Use an internal ArrayList.
ArrayList<T> to store itemspush adds to the end, pop removes from the endimport 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();
}
}
Task: Build the generic methods findMin and
findMax that work with any Comparable type. Test with Integer and String.
<T extends Comparable<T>>compareTo()compareTo returns a negative, zero, or positive valuepublic 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));
}
}
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.
HashMap<K, V> internallyput(K key, V value), get(K key),
contains(K key)
put(), get(), containsKey()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.