Generic Classes in Java
Learn how to create reusable classes that work with any data type
Think of generic classes like boxes that can hold any type of toy! You have one box design, but you can use it for LEGO blocks, action figures, or toy cars. Instead of making a separate box for each toy type, you make ONE smart box that works with everything. Generic classes let you write code once and use it with any data type!
The Problem Without Generics
Before generics, we had to use Object type and cast everything, leading to type safety issues:
// OLD WAY: Without generics (Before Java 5)public class OldBox { private Object item; // Can hold anything! public void set(Object item) { this.item = item; } public Object get() { return item; }}public class ProblemsWithoutGenerics { public static void main(String[] args) { // Problem 1: No type safety! OldBox box = new OldBox(); box.set("Hello"); // Put a String // DANGER! This compiles but crashes at runtime! Integer num = (Integer) box.get(); // 💥 ClassCastException! // Problem 2: Must cast everything! OldBox stringBox = new OldBox(); stringBox.set("World"); String str = (String) stringBox.get(); // Annoying cast! // Problem 3: Can mix types accidentally! OldBox mixedBox = new OldBox(); mixedBox.set("Text"); mixedBox.set(123); // Oops! Changed from String to Integer // No warning! Compiler doesn't know! // Problem 4: Easy to make mistakes OldBox box1 = new OldBox(); OldBox box2 = new OldBox(); box1.set("Apple"); box2.set(42); // Which box has String? Which has Integer? // Compiler can't help you! 😫 // Solution: Generics! 👇 }}What is a Generic Class?
A generic class is a class that can work with any type. You specify the type in angle brackets <T> when you create the class. The T is a type parameter - a placeholder for the actual type you'll use!
// Generic Box class - Works with ANY type!// T is a type parameter (placeholder for actual type)public class Box<T> { private T item; public void set(T item) { this.item = item; } public T get() { return item; }}public class GenericClassIntro { public static void main(String[] args) { // Create Box for Strings Box<String> stringBox = new Box<>(); // T becomes String stringBox.set("Hello"); String str = stringBox.get(); // No cast needed! System.out.println(str); // stringBox.set(123); // ✗ Compile error! Type safe! // Create Box for Integers Box<Integer> intBox = new Box<>(); // T becomes Integer intBox.set(42); Integer num = intBox.get(); // No cast needed! System.out.println(num); // intBox.set("text"); // ✗ Compile error! Type safe! // Create Box for any type! Box<Double> doubleBox = new Box<>(); doubleBox.set(3.14); Box<Boolean> boolBox = new Box<>(); boolBox.set(true); Box<Person> personBox = new Box<>(); personBox.set(new Person("Alice")); // Benefits: // ✓ Type safety - errors caught at compile time // ✓ No casting needed // ✓ One class works with all types // ✓ Self-documenting code // Diamond operator <> (Java 7+) Box<String> box1 = new Box<>(); // Cleaner! Box<String> box2 = new Box<String>(); // Older style }}class Person { String name; Person(String name) { this.name = name; }}Creating Generic Classes
Let's see how to create and use generic classes:
// Example 1: Simple generic containerpublic class Container<T> { private T value; public Container(T value) { this.value = value; } public T getValue() { return value; } public void setValue(T value) { this.value = value; } public boolean isEmpty() { return value == null; } @Override public String toString() { return "Container{" + value + "}"; }}// Example 2: Generic stack (data structure)public class Stack<E> { // E for Element (common convention) private E[] elements; private int size = 0; private static final int DEFAULT_CAPACITY = 10; @SuppressWarnings("unchecked") public Stack() { // Can't create generic array directly! // elements = new E[DEFAULT_CAPACITY]; // ✗ Won't compile // Workaround: Create Object array and cast elements = (E[]) new Object[DEFAULT_CAPACITY]; } public void push(E element) { if (size == elements.length) { resize(); } elements[size++] = element; } public E pop() { if (isEmpty()) { throw new IllegalStateException("Stack is empty"); } E element = elements[--size]; elements[size] = null; // Prevent memory leak return element; } public E peek() { if (isEmpty()) { throw new IllegalStateException("Stack is empty"); } return elements[size - 1]; } public boolean isEmpty() { return size == 0; } public int size() { return size; } @SuppressWarnings("unchecked") private void resize() { E[] newElements = (E[]) new Object[elements.length * 2]; System.arraycopy(elements, 0, newElements, 0, size); elements = newElements; }}public class GenericClassExamples { public static void main(String[] args) { // Using Container Container<String> nameContainer = new Container<>("Alice"); System.out.println(nameContainer.getValue()); // Alice Container<Integer> ageContainer = new Container<>(25); System.out.println(ageContainer.getValue()); // 25 // Using Stack Stack<String> stringStack = new Stack<>(); stringStack.push("First"); stringStack.push("Second"); stringStack.push("Third"); System.out.println(stringStack.pop()); // Third System.out.println(stringStack.pop()); // Second System.out.println(stringStack.peek()); // First (doesn't remove) System.out.println(stringStack.size()); // 1 // Stack of Integers Stack<Integer> intStack = new Stack<>(); for (int i = 1; i <= 5; i++) { intStack.push(i); } while (!intStack.isEmpty()) { System.out.print(intStack.pop() + " "); // 5 4 3 2 1 } }}Multiple Type Parameters
Generic classes can have multiple type parameters:
// Generic class with TWO type parameterspublic class Pair<K, V> { // K for Key, V for Value 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 void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } @Override public String toString() { return "Pair{" + key + " = " + value + "}"; }}// Generic class with THREE type parameterspublic class Triple<A, B, C> { private A first; private B second; private C third; public Triple(A first, B second, C third) { this.first = first; this.second = second; this.third = third; } public A getFirst() { return first; } public B getSecond() { return second; } public C getThird() { return third; } @Override public String toString() { return "Triple{" + first + ", " + second + ", " + third + "}"; }}// More complex example: Generic cachepublic class Cache<K, V> { private Map<K, V> storage = new HashMap<>(); public void put(K key, V value) { storage.put(key, value); } public V get(K key) { return storage.get(key); } public boolean containsKey(K key) { return storage.containsKey(key); } public void clear() { storage.clear(); } public int size() { return storage.size(); }}public class MultipleTypeParameters { public static void main(String[] args) { // Using Pair Pair<String, Integer> person = new Pair<>("Alice", 25); System.out.println(person); // Pair{Alice = 25} System.out.println("Name: " + person.getKey()); // Alice System.out.println("Age: " + person.getValue()); // 25 // Different types! Pair<Integer, String> reverseMap = new Pair<>(1, "One"); Pair<String, Double> priceMap = new Pair<>("Apple", 1.99); Pair<Boolean, String> statusMap = new Pair<>(true, "Active"); // Using Triple Triple<String, Integer, Double> student = new Triple<>("Bob", 20, 3.8); // name, age, GPA System.out.println(student); Triple<Integer, Integer, Integer> coordinates = new Triple<>(10, 20, 30); // x, y, z // Using Cache Cache<String, User> userCache = new Cache<>(); userCache.put("user123", new User("Alice", 25)); userCache.put("user456", new User("Bob", 30)); User user = userCache.get("user123"); System.out.println("Cached user: " + user.name); // Cache with different types Cache<Integer, String> nameCache = new Cache<>(); nameCache.put(1, "One"); nameCache.put(2, "Two"); Cache<String, List<String>> listCache = new Cache<>(); listCache.put("fruits", Arrays.asList("Apple", "Banana")); // Real-world example: Configuration map Pair<String, String> config1 = new Pair<>("database.url", "localhost:5432"); Pair<String, Integer> config2 = new Pair<>("max.connections", 100); Pair<String, Boolean> config3 = new Pair<>("debug.enabled", true); }}class User { String name; int age; User(String name, int age) { this.name = name; this.age = age; }}Real-World Generic Classes
See how generic classes are used in real applications:
import java.util.*;// Example 1: Generic Result wrapper (API responses)public class Result<T> { private boolean success; private T data; private String errorMessage; private Result(boolean success, T data, String errorMessage) { this.success = success; this.data = data; this.errorMessage = errorMessage; } public static <T> Result<T> success(T data) { return new Result<>(true, data, null); } public static <T> Result<T> failure(String errorMessage) { return new Result<>(false, null, errorMessage); } public boolean isSuccess() { return success; } public T getData() { return data; } public String getErrorMessage() { return errorMessage; }}// Example 2: Generic Node (for trees, linked lists)public class Node<T> { private T data; private Node<T> next; public Node(T data) { this.data = data; } public T getData() { return data; } public void setData(T data) { this.data = data; } public Node<T> getNext() { return next; } public void setNext(Node<T> next) { this.next = next; }}// Example 3: Generic Response envelopepublic class Response<T> { private int statusCode; private String message; private T payload; private long timestamp; public Response(int statusCode, String message, T payload) { this.statusCode = statusCode; this.message = message; this.payload = payload; this.timestamp = System.currentTimeMillis(); } public int getStatusCode() { return statusCode; } public String getMessage() { return message; } public T getPayload() { return payload; } public long getTimestamp() { return timestamp; } @Override public String toString() { return String.format("Response{code=%d, message='%s', payload=%s}", statusCode, message, payload); }}// Example 4: Generic builder patternpublic class Builder<T> { private Map<String, Object> properties = new HashMap<>(); private Class<T> type; public Builder(Class<T> type) { this.type = type; } public Builder<T> with(String property, Object value) { properties.put(property, value); return this; } public Map<String, Object> getProperties() { return properties; }}public class RealWorldGenerics { public static void main(String[] args) { // Using Result wrapper Result<User> userResult = fetchUser(123); if (userResult.isSuccess()) { User user = userResult.getData(); System.out.println("User: " + user.name); } else { System.out.println("Error: " + userResult.getErrorMessage()); } Result<List<String>> listResult = fetchUsernames(); if (listResult.isSuccess()) { System.out.println("Usernames: " + listResult.getData()); } // Using Node (Linked list) Node<String> head = new Node<>("First"); Node<String> second = new Node<>("Second"); Node<String> third = new Node<>("Third"); head.setNext(second); second.setNext(third); // Traverse list Node<String> current = head; while (current != null) { System.out.print(current.getData() + " -> "); current = current.getNext(); } System.out.println("null"); // Using Response envelope Response<User> response = new Response<>( 200, "Success", new User("Alice", 25) ); System.out.println(response); Response<List<String>> listResponse = new Response<>( 200, "Success", Arrays.asList("Item1", "Item2", "Item3") ); // Using Builder Builder<User> userBuilder = new Builder<>(User.class); userBuilder .with("name", "Charlie") .with("age", 30) .with("email", "charlie@example.com"); System.out.println("Built properties: " + userBuilder.getProperties()); } static Result<User> fetchUser(int id) { if (id > 0) { return Result.success(new User("Alice", 25)); } return Result.failure("User not found"); } static Result<List<String>> fetchUsernames() { return Result.success(Arrays.asList("Alice", "Bob", "Charlie")); }}Advanced Example: Generic Repository
A practical example showing a generic repository pattern:
import java.util.*;// Generic Repository interfacepublic interface Repository<T, ID> { void save(T entity); Optional<T> findById(ID id); List<T> findAll(); void update(T entity); void deleteById(ID id); boolean existsById(ID id);}// Generic implementationpublic class InMemoryRepository<T, ID> implements Repository<T, ID> { private Map<ID, T> storage = new HashMap<>(); @Override public void save(T entity) { ID id = extractId(entity); storage.put(id, entity); } @Override public Optional<T> findById(ID id) { return Optional.ofNullable(storage.get(id)); } @Override public List<T> findAll() { return new ArrayList<>(storage.values()); } @Override public void update(T entity) { ID id = extractId(entity); if (storage.containsKey(id)) { storage.put(id, entity); } else { throw new IllegalArgumentException("Entity not found"); } } @Override public void deleteById(ID id) { storage.remove(id); } @Override public boolean existsById(ID id) { return storage.containsKey(id); } // Helper method to extract ID (simplified) @SuppressWarnings("unchecked") private ID extractId(T entity) { if (entity instanceof User) { return (ID) ((User) entity).getId(); } else if (entity instanceof Product) { return (ID) ((Product) entity).getId(); } throw new IllegalArgumentException("Unknown entity type"); }}// Entity classesclass User { private Integer id; private String name; private String email; public User(Integer id, String name, String email) { this.id = id; this.name = name; this.email = email; } public Integer getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } @Override public String toString() { return "User{id=" + id + ", name='" + name + "'}"; }}class Product { private String id; private String name; private double price; public Product(String id, String name, double price) { this.id = id; this.name = name; this.price = price; } public String getId() { return id; } public String getName() { return name; } public double getPrice() { return price; } @Override public String toString() { return "Product{id='" + id + "', name='" + name + "', price=" + price + "}"; }}public class RepositoryExample { public static void main(String[] args) { // User repository with Integer ID Repository<User, Integer> userRepo = new InMemoryRepository<>(); // Save users userRepo.save(new User(1, "Alice", "alice@example.com")); userRepo.save(new User(2, "Bob", "bob@example.com")); userRepo.save(new User(3, "Charlie", "charlie@example.com")); // Find by ID Optional<User> user = userRepo.findById(1); user.ifPresent(u -> System.out.println("Found: " + u)); // Find all List<User> allUsers = userRepo.findAll(); System.out.println("All users: " + allUsers.size()); // Check existence boolean exists = userRepo.existsById(2); System.out.println("User 2 exists: " + exists); // Delete userRepo.deleteById(3); System.out.println("After delete: " + userRepo.findAll().size()); // Product repository with String ID Repository<Product, String> productRepo = new InMemoryRepository<>(); productRepo.save(new Product("P001", "Laptop", 999.99)); productRepo.save(new Product("P002", "Mouse", 29.99)); productRepo.save(new Product("P003", "Keyboard", 79.99)); Optional<Product> product = productRepo.findById("P001"); product.ifPresent(p -> System.out.println("\nFound: " + p)); List<Product> allProducts = productRepo.findAll(); System.out.println("All products: " + allProducts.size()); // Same repository interface works for both User and Product! // This is the power of generics! }}Key Concepts
Type Safety
Generics catch type errors at compile time instead of runtime, making your code safer!
Code Reusability
Write one generic class and use it with any type - no need to duplicate code for each type!
No Casting Needed
With generics, you don't need to cast objects. The compiler knows the type!
Type Parameter
T, E, K, V are common type parameter names. T=Type, E=Element, K=Key, V=Value.
Best Practices
- ✓Use meaningful type parameter names (T for Type, E for Element, K for Key, V for Value)
- ✓Make classes generic when they logically work with multiple types
- ✓Don't use raw types - always specify the type parameter
- ✓Use <> diamond operator for cleaner instantiation
- ✓Consider thread safety when creating generic container classes
- ✓Document what types are expected and any constraints
Common Mistakes
✗ Using raw types without type parameters
Why it's wrong: Raw types bypass generics safety. Always specify the type: Box<String>, not just Box!
✗ Creating generic arrays
Why it's wrong: You cannot create arrays of generic types due to type erasure. Use ArrayList instead!
✗ Using primitive types as type parameters
Why it's wrong: Use wrapper classes (Integer, Double) instead of primitives (int, double).
✗ Confusing generic class definition with usage
Why it's wrong: Define with <T>, use with <ActualType>. Box<T> is definition, Box<String> is usage.
Interview Tips
- 💡Explain that generics provide compile-time type safety
- 💡Know how to create a generic class with type parameters
- 💡Understand the difference between generic class definition and usage
- 💡Be able to create classes with multiple type parameters
- 💡Explain the benefits: type safety, code reuse, no casting
- 💡Know that you can't use primitives - must use wrapper classes