foreach-ui logo
codeLanguages
account_treeDSA

Quick Actions

quizlock Random Quiz
trending_uplock Progress
  • 1
  • 2
  • 3
  • 4
  • quiz
Java
  • Create and use generic classes with type parameters
  • Implement generic methods in both generic and non-generic classes
  • Master bounded type parameters and multiple constraints
  • Apply generics in real-world scenarios like factories and utilities

Generic Classes and Methods in Java

You've seen how generics work. Now let's build your own generic classes and methods—the kind of flexible, reusable code that powers libraries like the Collections Framework.

Designing Generic Classes

When to Make a Class Generic

Make a class generic when:

  • It operates on data without needing to know the specific type
  • The same logic applies to many different types
  • You want type safety without code duplication

Containers, wrappers, data structures, utility classes—all good candidates.

Single Type Parameter Classes

The simplest generic class has one type parameter:

public class Container<T> {
    private T item;
    
    public Container() {
        this.item = null;
    }
    
    public Container(T item) {
        this.item = item;
    }
    
    public T getItem() {
        return item;
    }
    
    public void setItem(T item) {
        this.item = item;
    }
    
    public boolean hasItem() {
        return item != null;
    }
    
    public void clear() {
        this.item = null;
    }
}

Key insight: Inside the class, T acts as a regular type. You can declare fields, parameters, and return types with it.

Multiple Type Parameters

When your class works with multiple types, use multiple parameters:

public class Pair<K, V> {
    private final K first;
    private final V second;
    
    public Pair(K first, V second) {
        this.first = first;
        this.second = second;
    }
    
    public K getFirst() { return first; }
    public V getSecond() { return second; }
    
    @Override
    public String toString() {
        return "(" + first + ", " + second + ")";
    }
    
    // Static factory method (note the type parameters)
    public static <K, V> Pair<K, V> of(K first, V second) {
        return new Pair<>(first, second);
    }
}

Usage demonstrates the flexibility:

Pair<String, Integer> nameAge = new Pair<>("Alice", 30);
Pair<Integer, Integer> coordinates = new Pair<>(10, 20);
Pair<String, List<String>> personHobbies = new Pair<>("Bob", Arrays.asList("reading", "gaming"));

// Using static factory
Pair<String, Double> product = Pair.of("Coffee", 2.99);

Self-Referential Type Parameters

Sometimes a class needs to refer to itself in its type parameter. This pattern is common in builder classes and comparable implementations:

public abstract class Builder<T extends Builder<T>> {
    protected String name;
    protected int value;
    
    @SuppressWarnings("unchecked")
    public T withName(String name) {
        this.name = name;
        return (T) this;  // Return the actual subtype
    }
    
    @SuppressWarnings("unchecked")
    public T withValue(int value) {
        this.value = value;
        return (T) this;
    }
    
    public abstract Object build();
}

// Concrete builder
public class ProductBuilder extends Builder<ProductBuilder> {
    private String category;
    
    public ProductBuilder withCategory(String category) {
        this.category = category;
        return this;  // Returns ProductBuilder, not Builder
    }
    
    @Override
    public Product build() {
        return new Product(name, value, category);
    }
}

// Fluent usage
Product product = new ProductBuilder()
    .withName("Laptop")
    .withValue(999)
    .withCategory("Electronics")
    .build();

Generic Methods

Generic methods have their own type parameters, independent of any class-level parameters.

Why Generic Methods?

Use generic methods when:

  • The type varies per method call, not per instance
  • The method is in a non-generic class (like utility classes)
  • You need a different type parameter than the class provides

Syntax and Declaration

The type parameter comes before the return type:

public class ArrayUtils {
    
    // Generic method to find an element
    public static <T> int indexOf(T[] array, T target) {
        for (int i = 0; i < array.length; i++) {
            if (array[i].equals(target)) {
                return i;
            }
        }
        return -1;
    }
    
    // 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;
    }
    
    // Generic method with multiple type parameters
    public static <T, R> R[] transform(T[] source, Function<T, R> transformer, 
                                        IntFunction<R[]> arrayFactory) {
        R[] result = arrayFactory.apply(source.length);
        for (int i = 0; i < source.length; i++) {
            result[i] = transformer.apply(source[i]);
        }
        return result;
    }
}

Type Inference

Usually, you don't need to specify the type explicitly - the compiler infers it:

String[] names = {"Alice", "Bob", "Charlie"};
int index = ArrayUtils.indexOf(names, "Bob");  // T inferred as String

Integer[] numbers = {1, 2, 3};
ArrayUtils.swap(numbers, 0, 2);  // T inferred as Integer

// Transform strings to their lengths
String[] words = {"hello", "world"};
Integer[] lengths = ArrayUtils.transform(
    words, 
    String::length, 
    Integer[]::new
);  // T=String, R=Integer inferred

When inference fails, you can specify types explicitly:

// Explicit type argument (rarely needed)
ArrayUtils.<String>swap(names, 0, 1);

Generic Methods in Generic Classes

A generic class can have generic methods with additional type parameters:

public class Box<T> {
    private T content;
    
    public void setContent(T content) {
        this.content = content;
    }
    
    public T getContent() {
        return content;
    }
    
    // Generic method with its own type parameter U
    public <U> Pair<T, U> pairWith(U other) {
        return new Pair<>(content, other);
    }
    
    // Another generic method
    public <R> R transform(Function<T, R> transformer) {
        return transformer.apply(content);
    }
}

// Usage
Box<String> box = new Box<>();
box.setContent("Hello");

Pair<String, Integer> pair = box.pairWith(42);  // U is Integer
Integer length = box.transform(String::length);  // R is Integer

Design Patterns with Generics

Generic Factory Pattern

public interface Factory<T> {
    T create();
}

public class StringFactory implements Factory<String> {
    @Override
    public String create() {
        return new String();
    }
}

public class ListFactory<E> implements Factory<List<E>> {
    @Override
    public List<E> create() {
        return new ArrayList<>();
    }
}

// Using factories
Factory<String> stringFactory = new StringFactory();
Factory<List<Integer>> intListFactory = new ListFactory<>();

String str = stringFactory.create();
List<Integer> numbers = intListFactory.create();

Generic Repository Pattern

public interface Repository<T, ID> {
    T findById(ID id);
    List<T> findAll();
    T save(T entity);
    void delete(ID id);
    boolean exists(ID id);
}

public class UserRepository implements Repository<User, Long> {
    private Map<Long, User> storage = new HashMap<>();
    
    @Override
    public User findById(Long id) {
        return storage.get(id);
    }
    
    @Override
    public List<User> findAll() {
        return new ArrayList<>(storage.values());
    }
    
    @Override
    public User save(User entity) {
        storage.put(entity.getId(), entity);
        return entity;
    }
    
    @Override
    public void delete(Long id) {
        storage.remove(id);
    }
    
    @Override
    public boolean exists(Long id) {
        return storage.containsKey(id);
    }
}

Generic Event System

public interface Event { }

public interface EventListener<E extends Event> {
    void onEvent(E event);
}

public class EventBus {
    private Map<Class<?>, List<EventListener<?>>> listeners = new HashMap<>();
    
    public <E extends Event> void subscribe(Class<E> eventType, EventListener<E> listener) {
        listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener);
    }
    
    @SuppressWarnings("unchecked")
    public <E extends Event> void publish(E event) {
        List<EventListener<?>> eventListeners = listeners.get(event.getClass());
        if (eventListeners != null) {
            for (EventListener<?> listener : eventListeners) {
                ((EventListener<E>) listener).onEvent(event);
            }
        }
    }
}

// Usage
class UserCreatedEvent implements Event {
    private final String username;
    public UserCreatedEvent(String username) { this.username = username; }
    public String getUsername() { return username; }
}

EventBus bus = new EventBus();
bus.subscribe(UserCreatedEvent.class, event -> 
    System.out.println("User created: " + event.getUsername()));
bus.publish(new UserCreatedEvent("alice"));

Best Practices

1. Choose Meaningful Type Parameter Names

// Poor naming
class Thing<X, Y, Z> { }

// Good naming
class Cache<K, V> { }  // Key and Value
class Converter<S, T> { }  // Source and Target

2. Prefer Generic Methods to Raw Types

// Avoid
public Object getFirst(List list) {
    return list.get(0);
}

// Prefer
public <T> T getFirst(List<T> list) {
    return list.get(0);
}

3. Use Bounded Types When Appropriate

When you need specific capabilities from the type:

// Can call compareTo() because T must implement Comparable
public <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}

4. Document Type Parameters

/**
 * A generic cache implementation.
 * @param <K> the type of keys maintained by this cache
 * @param <V> the type of cached values
 */
public class Cache<K, V> { }

Generic classes and methods let you write once, use everywhere—with type safety. Declare type parameters after the class name (class Box<T>) or before the return type for methods (<T> T method(T arg)). The compiler infers types from context. Document your type parameters so people know what they're for.

© 2026 forEach. All rights reserved.

Privacy Policy•Terms of Service