How Do Java Generics Enable Code Reuse?
Problem
I was staring at 15 nearly identical Java classes in my codebase. Each one did the same thing - manage a list of items - but for different types:
public class StringList { private String[] items; private int size;
public void add(String item) { // resize array if needed... items[size++] = item; }
public String get(int index) { return items[index]; }
public int size() { return size; }}public class IntegerList { private Integer[] items; private int size;
public void add(Integer item) { // resize array if needed... items[size++] = item; }
public Integer get(int index) { return items[index]; }
public int size() { return size; }}public class CustomerList { private Customer[] items; private int size;
public void add(Customer item) { // resize array if needed... items[size++] = item; }
public Customer get(int index) { return items[index]; }
public int size() { return size; }}I had StringList, IntegerList, CustomerList, OrderList, ProductList… The logic was identical. Only the type changed. When I found a bug in the array resizing logic, I had to fix it in 15 places. This was a maintenance nightmare.
Environment
- Java 17
- Maven 3.9.0
- IntelliJ IDEA 2024.1
- macOS Sonoma
What happened?
I tried two approaches before finding the right solution.
Attempt 1: Code Duplication
My first approach was to just copy-paste. I wrote StringList, then copied it and changed String to Integer, then to Customer, and so on.
The problems quickly became obvious:
- Bug propagation: When I found a bug in the
add()method, I had to fix it in every single class - Inconsistency: Some classes had
remove()methods, others didn’t - Code bloat: My codebase grew to thousands of lines of duplicated logic
- Testing nightmare: I had to write 15 sets of nearly identical tests
Attempt 2: Using Object
I tried to be clever and use Object as the element type:
public class ObjectList { private Object[] items; private int size;
public void add(Object item) { items[size++] = item; }
public Object get(int index) { return items[index]; }}Now I had one class! But new problems appeared:
ObjectList list = new ObjectList();list.add("Hello");list.add(42); // No error - mixed types!list.add(new Customer());
String s = (String) list.get(0); // Manual cast requiredInteger i = (Integer) list.get(1); // What if order changes?String broken = (String) list.get(2); // ClassCastException at runtime!This approach had its own problems:
- No type safety: Any type could be added to any list
- Runtime errors: ClassCastException when I retrieved the wrong type
- Manual casts everywhere: The code was verbose and error-prone
- Lost intent: I couldn’t tell what type of data a list was supposed to hold
Both approaches were wrong. I needed a better way.
How to solve it?
The solution was Java Generics. With generics, I could write ONE implementation that works for ALL types:
public class GenericList<T> { private Object[] items; private int size;
public void add(T item) { items[size++] = item; }
@SuppressWarnings("unchecked") public T get(int index) { return (T) items[index]; }
public int size() { return size; }}Now I use the same class for all types:
GenericList<String> strings = new GenericList<>();strings.add("Hello");// strings.add(42); // Compile error - can't add Integer to String list
GenericList<Integer> integers = new GenericList<>();integers.add(42);// integers.add("Hello"); // Compile error - can't add String to Integer list
GenericList<Customer> customers = new GenericList<>();customers.add(new Customer());One class. All types. Type-safe. No duplication. When I fix a bug, I fix it once.
How Code Reuse Works
Let me break down exactly how generics enable code reuse.
Step 1: Type Parameter Declaration
The <T> in the class declaration is called a type parameter:
public class GenericList<T> { // T is a type parameter // T can be used anywhere inside this class}Think of T as a placeholder. When you use GenericList<String>, the compiler replaces T with String throughout the class.
Step 2: Type Instantiation
When you create an instance, you provide the actual type:
GenericList<String> stringList = new GenericList<>(); // T = StringGenericList<Integer> intList = new GenericList<>(); // T = IntegerGenericList<Customer> custList = new GenericList<>(); // T = CustomerEach instantiation creates a “version” of the class for that specific type. But at runtime, there’s still only ONE class. The compiler handles all the type checking.
Step 3: Compile-Time Verification
The compiler verifies every operation against the type parameter:
GenericList<String> strings = new GenericList<>();
strings.add("Hello"); // OK: String matches Tstrings.add("World"); // OK: String matches Tstrings.add(42); // ERROR: Integer doesn't match String
String s = strings.get(0); // OK: returns String, no cast neededInteger i = strings.get(0); // ERROR: can't assign String to IntegerThe compiler catches type mismatches BEFORE your code runs.
Why This Matters
Let me compare the three approaches with real metrics from my project:
| Metric | Duplicated Classes | Object Approach | Generic Approach |
|---|---|---|---|
| Lines of code | 750 (15 x 50) | 50 | 50 |
| Bugs found | 3 (in each class) | 5 (runtime) | 1 |
| Fixes needed | 45 fixes | 5 fixes | 1 fix |
| Test classes | 15 | 1 | 1 |
| Runtime errors | 0 | Many | 0 |
| Type safety | Yes (per class) | No | Yes |
Maintainability: Fix Once
When I found a bug in my resizing logic:
public void add(T item) { // Bug: forgot to resize when array is full! items[size++] = item; // ArrayIndexOutOfBoundsException!}With generics, I fixed it once:
public void add(T item) { if (size >= items.length) { items = Arrays.copyOf(items, items.length * 2); } items[size++] = item;}One fix. All 15 uses of GenericList now work correctly.
Reduced Codebase Size
My list-related code went from 750 lines to 50 lines - a 93% reduction. Less code means:
- Fewer bugs
- Faster compilation
- Easier code reviews
- Simpler refactoring
Consistency Across Types
Every GenericList behaves the same way, whether it holds Strings, Integers, or Customers. The API is consistent:
// Same methods work for all types:list.add(item);list.get(index);list.size();list.remove(index);Bounded Types: Code Reuse with Constraints
Sometimes you need to reuse code but with type restrictions. Generics support this with bounded type parameters:
public class NumberUtils { // Only works with types that extend Number public static <T extends Number> double sum(List<T> numbers) { double total = 0; for (T num : numbers) { total += num.doubleValue(); // Number has doubleValue() } return total; }}Now the method is reusable across all Number types, but not for non-numbers:
List<Integer> ints = List.of(1, 2, 3);List<Double> doubles = List.of(1.5, 2.5);List<String> strings = List.of("a", "b");
NumberUtils.sum(ints); // OK: returns 6.0NumberUtils.sum(doubles); // OK: returns 4.0// NumberUtils.sum(strings); // ERROR: String doesn't extend NumberBounded types let you reuse code with constraints, ensuring type safety and enabling specific operations.
Common Patterns for Code Reuse
Pattern 1: Generic Container Classes
public class Pair<K, V> { private final K key; private final V value;
public Pair(K key, V value) { this.key = key; this.value = value; }
public K getKey() { return key; } public V getValue() { return value; }}
// Use it for any type combination:Pair<String, Integer> nameAndAge = new Pair<>("Alice", 30);Pair<Integer, String> idAndName = new Pair<>(42, "Bob");Pair<Customer, Order> customerOrder = new Pair<>(customer, order);Pattern 2: Generic Utility Methods
public class ArrayUtils { public static <T> void swap(T[] array, int i, int j) { T temp = array[i]; array[i] = array[j]; array[j] = temp; }}
// Works for any array type:String[] strings = {"a", "b", "c"};Integer[] ints = {1, 2, 3};Customer[] customers = {c1, c2, c3};
ArrayUtils.swap(strings, 0, 1); // Swaps String elementsArrayUtils.swap(ints, 0, 1); // Swaps Integer elementsArrayUtils.swap(customers, 0, 1); // Swaps Customer elementsPattern 3: Generic Interfaces (Repository Pattern)
This is the pattern I use most in real projects:
public interface Repository<T, ID> { Optional<T> findById(ID id); List<T> findAll(); T save(T entity); void delete(ID id);}Multiple implementations, one interface:
public class CustomerRepository implements Repository<Customer, Long> { public Optional<Customer> findById(Long id) { // Customer-specific implementation } // ... other methods}
public class OrderRepository implements Repository<Order, String> { public Optional<Order> findById(String id) { // Order-specific implementation } // ... other methods}
public class ProductRepository implements Repository<Product, UUID> { public Optional<Product> findById(UUID id) { // Product-specific implementation } // ... other methods}The interface is reusable. Each implementation handles type-specific details. The common contract is enforced.
Pattern 4: Generic Stack Implementation
A complete example showing all the benefits:
public class Stack<T> { private static final int DEFAULT_CAPACITY = 10; private Object[] elements; private int size = 0;
public Stack() { elements = new Object[DEFAULT_CAPACITY]; }
public void push(T item) { if (size >= elements.length) { resize(); } elements[size++] = item; }
@SuppressWarnings("unchecked") public T pop() { if (size == 0) { throw new EmptyStackException(); } T item = (T) elements[--size]; elements[size] = null; // Help GC return item; }
public boolean isEmpty() { return size == 0; }
private void resize() { elements = Arrays.copyOf(elements, elements.length * 2); }}One implementation serves all use cases:
Stack<String> stringStack = new Stack<>();stringStack.push("Hello");String s = stringStack.pop();
Stack<Integer> intStack = new Stack<>();intStack.push(42);Integer i = intStack.pop();
Stack<Customer> customerStack = new Stack<>();customerStack.push(new Customer("Alice"));Customer c = customerStack.pop();Common Mistakes That Break Code Reuse
Mistake 1: Over-Specific Implementations
// Don't do this:public class StringStack { private String[] elements; // Only works for Strings...}
public class IntegerStack { private Integer[] elements; // Only works for Integers...}Always ask: “Can this logic work for other types?” If yes, make it generic.
Mistake 2: Ignoring Bounded Types
// Without bounds, you can't use type-specific methods:public <T> double sum(List<T> numbers) { double total = 0; for (T num : numbers) { // total += num.doubleValue(); // ERROR! T doesn't have doubleValue() } return total;}Fix with bounds:
public <T extends Number> double sum(List<T> numbers) { double total = 0; for (T num : numbers) { total += num.doubleValue(); // OK! Number has doubleValue() } return total;}Mistake 3: Using Raw Types
List rawList = new ArrayList(); // Raw type - loses all benefits of generics!rawList.add("string");rawList.add(42);rawList.add(new Customer());Raw types throw away the type information that makes code reuse safe.
Summary
In this post, I showed how Java Generics enable code reuse by allowing a single implementation to work across multiple types.
The key insights:
- Without generics: You either duplicate code for each type OR use Object and accept runtime errors
- With generics: One implementation serves all types, with compile-time type safety
- The mechanism: Type parameters (
<T>) act as placeholders that the compiler fills in with actual types - The benefits: Maintainability (fix once), reduced codebase size, consistency, type safety
The difference is dramatic:
| Before Generics | After Generics |
|---|---|
| 15 classes, 750 lines | 1 class, 50 lines |
| Bug fixed 15 times | Bug fixed once |
| Inconsistent APIs | Consistent API |
| No type safety | Full type safety |
Generics aren’t just about type safety - they’re a powerful tool for eliminating code duplication. When you find yourself copying code and changing only the types, generics are the answer.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Java Generics Tutorial
- 👨💻 Effective Java - Item 29: Prefer generic types
- 👨💻 Oracle: Generics in Java
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments