Skip to content

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:

StringList.java
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;
}
}
IntegerList.java
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;
}
}
CustomerList.java
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:

ObjectList.java
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:

Using ObjectList
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 required
Integer 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:

GenericList.java
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:

Using GenericList
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:

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:

Type instantiation
GenericList<String> stringList = new GenericList<>(); // T = String
GenericList<Integer> intList = new GenericList<>(); // T = Integer
GenericList<Customer> custList = new GenericList<>(); // T = Customer

Each 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:

Compile-time verification
GenericList<String> strings = new GenericList<>();
strings.add("Hello"); // OK: String matches T
strings.add("World"); // OK: String matches T
strings.add(42); // ERROR: Integer doesn't match String
String s = strings.get(0); // OK: returns String, no cast needed
Integer i = strings.get(0); // ERROR: can't assign String to Integer

The compiler catches type mismatches BEFORE your code runs.

Why This Matters

Let me compare the three approaches with real metrics from my project:

MetricDuplicated ClassesObject ApproachGeneric Approach
Lines of code750 (15 x 50)5050
Bugs found3 (in each class)5 (runtime)1
Fixes needed45 fixes5 fixes1 fix
Test classes1511
Runtime errors0Many0
Type safetyYes (per class)NoYes

Maintainability: Fix Once

When I found a bug in my resizing logic:

The bug
public void add(T item) {
// Bug: forgot to resize when array is full!
items[size++] = item; // ArrayIndexOutOfBoundsException!
}

With generics, I fixed it once:

The fix
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:

Consistent API
// 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:

Bounded type parameter
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:

Using bounded types
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.0
NumberUtils.sum(doubles); // OK: returns 4.0
// NumberUtils.sum(strings); // ERROR: String doesn't extend Number

Bounded types let you reuse code with constraints, ensuring type safety and enabling specific operations.

Common Patterns for Code Reuse

Pattern 1: Generic Container Classes

Pair.java - Reusable container
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

Generic utility method
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 elements
ArrayUtils.swap(ints, 0, 1); // Swaps Integer elements
ArrayUtils.swap(customers, 0, 1); // Swaps Customer elements

Pattern 3: Generic Interfaces (Repository Pattern)

This is the pattern I use most in real projects:

Repository.java - Generic interface
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:

Concrete implementations
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:

Stack.java - Generic stack
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:

Using Stack
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

Wrong - Too specific
// 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

Wrong - Missing bounds
// 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:

Right - 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

Wrong - Raw type breaks reuse
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:

  1. Without generics: You either duplicate code for each type OR use Object and accept runtime errors
  2. With generics: One implementation serves all types, with compile-time type safety
  3. The mechanism: Type parameters (<T>) act as placeholders that the compiler fills in with actual types
  4. The benefits: Maintainability (fix once), reduced codebase size, consistency, type safety

The difference is dramatic:

Before GenericsAfter Generics
15 classes, 750 lines1 class, 50 lines
Bug fixed 15 timesBug fixed once
Inconsistent APIsConsistent API
No type safetyFull 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments