home / skills / thebushidocollective / han / java-generics

This skill helps you write type-safe Java code using generics, wildcards, and bounds to improve reuse and compile-time safety.

npx playbooks add skill thebushidocollective/han --skill java-generics

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
15.9 KB
---
name: java-generics
user-invocable: false
description: Use when Java generics including type parameters, wildcards, and type bounds. Use when writing type-safe reusable code.
allowed-tools:
  - Bash
  - Read
  - Write
  - Edit
---

# Java Generics

Master Java's generics system for writing type-safe, reusable code with
compile-time type checking, generic classes, methods, wildcards, and
type bounds.

## Introduction to Generics

Generics enable types to be parameters when defining classes, interfaces,
and methods, providing compile-time type safety.

**Basic generic class:**

```java
public class Box<T> {
    private T content;

    public void set(T content) {
        this.content = content;
    }

    public T get() {
        return content;
    }

    public static void main(String[] args) {
        // Type-safe box for String
        Box<String> stringBox = new Box<>();
        stringBox.set("Hello");
        String value = stringBox.get(); // No casting needed

        // Type-safe box for Integer
        Box<Integer> intBox = new Box<>();
        intBox.set(42);
        Integer number = intBox.get();
    }
}
```

**Generic with multiple type parameters:**

```java
public class Pair<K, V> {
    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 static void main(String[] args) {
        Pair<String, Integer> pair = new Pair<>("age", 30);
        String key = pair.getKey();
        Integer value = pair.getValue();
    }
}
```

## Generic Methods

Generic methods can be defined independently of generic classes.

**Basic generic method:**

```java
public class GenericMethods {
    // Generic method
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }

    // Generic method with return type
    public static <T> T getFirst(T[] array) {
        if (array.length > 0) {
            return array[0];
        }
        return null;
    }

    public static void main(String[] args) {
        String[] strings = {"a", "b", "c"};
        Integer[] numbers = {1, 2, 3};

        printArray(strings);  // T inferred as String
        printArray(numbers);  // T inferred as Integer

        String first = getFirst(strings);
        Integer firstNum = getFirst(numbers);
    }
}
```

**Generic method with multiple type parameters:**

```java
public class MultiplTypeParams {
    public static <K, V> Map<K, V> createMap(K key, V value) {
        Map<K, V> map = new HashMap<>();
        map.put(key, value);
        return map;
    }

    public static <T, R> R transform(T input, Function<T, R> transformer) {
        return transformer.apply(input);
    }

    public static void main(String[] args) {
        Map<String, Integer> map = createMap("count", 10);

        String result = transform(42, num -> "Number: " + num);
        // Result: "Number: 42"
    }
}
```

## Bounded Type Parameters

Type bounds restrict the types that can be used as type arguments.

**Upper bounded type parameters:**

```java
public class UpperBound {
    // T must be Number or subclass of Number
    public static <T extends Number> double sum(List<T> numbers) {
        double total = 0;
        for (T num : numbers) {
            total += num.doubleValue();
        }
        return total;
    }

    // Multiple bounds
    public static <T extends Comparable<T> & Serializable> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }

    public static void main(String[] args) {
        List<Integer> integers = List.of(1, 2, 3, 4, 5);
        double sum = sum(integers); // 15.0

        List<Double> doubles = List.of(1.5, 2.5, 3.5);
        double doubleSum = sum(doubles); // 7.5

        String maxStr = max("apple", "banana"); // "banana"
    }
}
```

**Class with bounded type parameter:**

```java
public class NumberBox<T extends Number> {
    private T number;

    public NumberBox(T number) {
        this.number = number;
    }

    public double doubleValue() {
        return number.doubleValue();
    }

    public boolean isZero() {
        return number.doubleValue() == 0.0;
    }

    public static void main(String[] args) {
        NumberBox<Integer> intBox = new NumberBox<>(42);
        NumberBox<Double> doubleBox = new NumberBox<>(3.14);

        // Compile error: String is not a Number
        // NumberBox<String> stringBox = new NumberBox<>("fail");
    }
}
```

## Wildcards

Wildcards provide flexibility when working with generic types.

**Unbounded wildcard:**

```java
public class UnboundedWildcard {
    // Accept any List
    public static void printList(List<?> list) {
        for (Object elem : list) {
            System.out.println(elem);
        }
    }

    public static int size(List<?> list) {
        return list.size();
    }

    public static void main(String[] args) {
        List<String> strings = List.of("a", "b", "c");
        List<Integer> integers = List.of(1, 2, 3);

        printList(strings);
        printList(integers);

        System.out.println(size(strings));  // 3
        System.out.println(size(integers)); // 3
    }
}
```

**Upper bounded wildcard:**

```java
public class UpperBoundedWildcard {
    // Accept List of Number or any subclass
    public static double sum(List<? extends Number> numbers) {
        double total = 0;
        for (Number num : numbers) {
            total += num.doubleValue();
        }
        return total;
    }

    public static void main(String[] args) {
        List<Integer> integers = List.of(1, 2, 3);
        List<Double> doubles = List.of(1.5, 2.5);
        List<Number> numbers = List.of(1, 2.5, 3);

        System.out.println(sum(integers)); // 6.0
        System.out.println(sum(doubles));  // 4.0
        System.out.println(sum(numbers));  // 6.5
    }
}
```

**Lower bounded wildcard:**

```java
public class LowerBoundedWildcard {
    // Accept List of Integer or any superclass
    public static void addIntegers(List<? super Integer> list) {
        for (int i = 1; i <= 5; i++) {
            list.add(i);
        }
    }

    public static void main(String[] args) {
        List<Integer> integers = new ArrayList<>();
        addIntegers(integers);
        System.out.println(integers); // [1, 2, 3, 4, 5]

        List<Number> numbers = new ArrayList<>();
        addIntegers(numbers);
        System.out.println(numbers); // [1, 2, 3, 4, 5]

        List<Object> objects = new ArrayList<>();
        addIntegers(objects);
        System.out.println(objects); // [1, 2, 3, 4, 5]
    }
}
```

## PECS Principle

Producer Extends, Consumer Super - guideline for using wildcards.

**PECS in action:**

```java
public class PECSExample {
    // Producer - reading from source (extends)
    public static <T> void copy(
        List<? extends T> source,
        List<? super T> destination
    ) {
        for (T item : source) {
            destination.add(item);
        }
    }

    // Producer - extends for reading
    public static double sumNumbers(List<? extends Number> numbers) {
        double sum = 0;
        for (Number num : numbers) { // Reading (producing values)
            sum += num.doubleValue();
        }
        return sum;
    }

    // Consumer - super for writing
    public static void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 3; i++) {
            list.add(i); // Writing (consuming values)
        }
    }

    public static void main(String[] args) {
        List<Integer> source = List.of(1, 2, 3);
        List<Number> destination = new ArrayList<>();

        copy(source, destination);
        System.out.println(destination); // [1, 2, 3]
    }
}
```

## Generic Interfaces

Interfaces can be generic, providing contracts for generic types.

**Generic interface:**

```java
public interface Repository<T, ID> {
    T findById(ID id);
    List<T> findAll();
    void save(T entity);
    void delete(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 void save(User user) {
        storage.put(user.getId(), user);
    }

    @Override
    public void delete(Long id) {
        storage.remove(id);
    }
}

class User {
    private Long id;
    private String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
}
```

**Comparable and Comparator:**

```java
public class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Person other) {
        return this.name.compareTo(other.name);
    }

    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));

        // Natural ordering (by name)
        Collections.sort(people);

        // Custom comparator (by age)
        Comparator<Person> ageComparator =
            Comparator.comparingInt(p -> p.age);
        people.sort(ageComparator);
    }
}
```

## Type Erasure

Java generics use type erasure - generic type information is removed at
runtime.

**Understanding type erasure:**

```java
public class TypeErasure {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        List<Integer> integers = new ArrayList<>();

        // At runtime, both are just List
        System.out.println(strings.getClass() == integers.getClass());
        // true

        // Cannot check generic type at runtime
        // if (list instanceof List<String>) {} // Compile error

        // Can only check raw type
        if (strings instanceof List) {
            System.out.println("Is a List");
        }
    }
}
```

**Consequences of type erasure:**

```java
public class ErasureConsequences<T> {
    // Cannot create instance of type parameter
    // T instance = new T(); // Compile error

    // Cannot create array of parameterized type
    // T[] array = new T[10]; // Compile error

    // Cannot use instanceof with type parameter
    public boolean isInstance(Object obj) {
        // if (obj instanceof T) {} // Compile error
        return true;
    }

    // Workaround: pass Class<T>
    private Class<T> type;

    public ErasureConsequences(Class<T> type) {
        this.type = type;
    }

    public T createInstance() throws Exception {
        return type.getDeclaredConstructor().newInstance();
    }

    @SuppressWarnings("unchecked")
    public T[] createArray(int size) {
        return (T[]) Array.newInstance(type, size);
    }
}
```

## Generic Builders

Builder pattern with generics for fluent APIs.

**Generic builder:**

```java
public class Query<T> {
    private final Class<T> type;
    private String where;
    private String orderBy;
    private int limit;

    private Query(Class<T> type) {
        this.type = type;
    }

    public static <T> Query<T> from(Class<T> type) {
        return new Query<>(type);
    }

    public Query<T> where(String condition) {
        this.where = condition;
        return this;
    }

    public Query<T> orderBy(String field) {
        this.orderBy = field;
        return this;
    }

    public Query<T> limit(int count) {
        this.limit = count;
        return this;
    }

    public List<T> execute() {
        // Execute query and return results
        return new ArrayList<>();
    }

    public static void main(String[] args) {
        List<User> users = Query.from(User.class)
            .where("age > 18")
            .orderBy("name")
            .limit(10)
            .execute();
    }
}
```

## Recursive Type Bounds

Type bounds can reference the type parameter itself.

**Enum with recursive bound:**

```java
public class RecursiveBound {
    // Enum trick
    public static <E extends Enum<E>> void printEnum(Class<E> enumClass) {
        for (E constant : enumClass.getEnumConstants()) {
            System.out.println(constant);
        }
    }

    // Comparable with recursive bound
    public static <T extends Comparable<T>> T max(List<T> list) {
        if (list.isEmpty()) {
            throw new IllegalArgumentException("Empty list");
        }

        T max = list.get(0);
        for (T item : list) {
            if (item.compareTo(max) > 0) {
                max = item;
            }
        }
        return max;
    }

    enum Color { RED, GREEN, BLUE }

    public static void main(String[] args) {
        printEnum(Color.class);

        List<String> words = List.of("apple", "banana", "cherry");
        String maxWord = max(words); // "cherry"

        List<Integer> numbers = List.of(1, 5, 3, 9, 2);
        Integer maxNum = max(numbers); // 9
    }
}
```

**Builder with recursive bound:**

```java
public abstract class Builder<T, B extends Builder<T, B>> {
    protected abstract B self();

    public abstract T build();
}

public class Person {
    private final String name;
    private final int age;

    protected Person(PersonBuilder<?> builder) {
        this.name = builder.name;
        this.age = builder.age;
    }

    public static PersonBuilder<?> builder() {
        return new PersonBuilder<>();
    }

    public static class PersonBuilder<B extends PersonBuilder<B>>
            extends Builder<Person, B> {
        private String name;
        private int age;

        public B name(String name) {
            this.name = name;
            return self();
        }

        public B age(int age) {
            this.age = age;
            return self();
        }

        @Override
        @SuppressWarnings("unchecked")
        protected B self() {
            return (B) this;
        }

        @Override
        public Person build() {
            return new Person(this);
        }
    }
}
```

## When to Use This Skill

Use java-generics when you need to:

- Write reusable code that works with multiple types
- Enforce compile-time type safety
- Eliminate casting and type errors at runtime
- Create generic collections, algorithms, or utilities
- Build type-safe APIs and frameworks
- Implement generic design patterns
- Work with Java Collections Framework
- Define flexible method signatures with type parameters
- Create bounded type hierarchies
- Implement builder or factory patterns with type safety

## Best Practices

- Use meaningful type parameter names (T, E, K, V)
- Prefer bounded type parameters over raw types
- Use wildcards for flexibility in method parameters
- Apply PECS principle (Producer Extends, Consumer Super)
- Avoid raw types in new code
- Use @SuppressWarnings("unchecked") sparingly
- Document generic type constraints clearly
- Prefer generic methods over generic classes when possible
- Use bounded wildcards for maximum API flexibility
- Consider type erasure implications

## Common Pitfalls

- Using raw types instead of parameterized types
- Confusing extends and super wildcards
- Trying to create arrays of generic types
- Not understanding type erasure limitations
- Overusing wildcards making code unreadable
- Incorrect variance with wildcards
- Forgetting that generics are compile-time only
- Not handling unchecked warnings properly
- Creating unnecessarily complex generic hierarchies
- Misusing instanceof with generic types

## Resources

- [Java Generics Tutorial](<https://docs.oracle.com/javase/tutorial/java/generics/>)
- [Effective Java: Generics](<https://www.oreilly.com/library/view/effective-java/9780134686097/>)
- [Java Generics FAQ](<http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html>)
- [Oracle Generics Documentation](<https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/package-summary.html>)

Overview

This skill teaches practical use of Java generics to build type-safe, reusable code using type parameters, wildcards, and bounds. It focuses on patterns that reduce runtime errors, improve API clarity, and enable fluent generic APIs. Use it to design generic classes, methods, interfaces, and builders that work cleanly across types.

How this skill works

The skill inspects common generic constructs and explains when to use type parameters, bounded parameters, and wildcards. It demonstrates generic classes and methods, PECS (Producer Extends, Consumer Super), recursive bounds, and workarounds for type erasure. Examples show compile-time guarantees, patterns for builders and repositories, and safe usage for collections.

When to use it

  • When designing reusable classes or methods that should operate over multiple types (e.g., containers, utilities).
  • When creating APIs that must accept different collection element types without losing type safety.
  • When implementing data access layers or repositories with generic entity and ID types.
  • When writing fluent builders or factories that need to preserve concrete types in chained calls.
  • When you need to apply constraints (e.g., numeric operations or comparability) with bounded type parameters.

Best practices

  • Prefer type parameters on classes or methods instead of raw types to preserve compile-time checks.
  • Use <? extends T> for read-only/producer APIs and <? super T> for write/consumer APIs (PECS).
  • Declare multiple bounds with & to combine interfaces (e.g., Comparable<T> & Serializable).
  • Avoid creating arrays of generic types; use Class<T> tokens or collections instead.
  • Keep generic signatures simple and document intent; prefer clear names (T, K, V, E) and add Javadoc where needed.

Example use cases

  • A generic Box<T> or Pair<K,V> to hold arbitrary payloads with no casts required.
  • Generic repository interfaces Repository<T, ID> to reuse persistence logic across entities.
  • Utility methods like sum(List<? extends Number>) and addIntegers(List<? super Integer>) following PECS.
  • Fluent Query<T> or Builder<T,B> APIs that preserve the concrete type across chained calls.
  • Algorithms using recursive bounds (T extends Comparable<T>) to implement type-safe max/min functions.

FAQ

Why do I get class cast exceptions with generics at runtime?

Java uses type erasure: generic type parameters are removed at runtime. Correct static typing prevents casts at compile time, but runtime checks use raw types, so ensure proper generic declarations and avoid unchecked casts.

When should I use extends vs super in wildcards?

Use extends when you only need to read (produce) values of type T or its subtypes; use super when you need to write (consume) T values into the structure. Remember: PECS — Producer Extends, Consumer Super.