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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 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!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// Example 1: Simple generic container
public 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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// Generic class with TWO type parameters
public 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 parameters
public 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 cache
public 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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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 envelope
public 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 pattern
public 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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import java.util.*;
// Generic Repository interface
public 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 implementation
public 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 classes
class 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