☕ Java

Generic Interface

A generic interface in Java is an interface declaration that has one or more type parameters, allowing implementors and callers to bind specific types to those parameters. Generic interfaces enable type-safe polymorphism across unrelated type hierarchies: Comparable<T>, Iterable<T>, Function<T,R>, and Predicate<T> are all generic interfaces that are implemented by classes that supply concrete types for those parameters. A class that implements a generic interface must either supply concrete type arguments (class Integer implements Comparable<Integer>), propagate the type parameter (class ArrayList<E> implements List<E>), or leave the interface raw (class Legacy implements List). This entry covers all forms of generic interface declaration and implementation, multiple interface bounds and their interaction with erasure, functional interfaces as a specific and important category of generic interface, implementing the same generic interface twice with different arguments (and why Java prohibits it), covariant return types in generic interface implementations, and the patterns used throughout the standard library.

Declaring and Implementing Generic Interfaces

A generic interface is declared with angle brackets after the interface name, listing one or more type parameters: interface Container<E> or interface Transformer<T, R> or interface Keyed<K extends Comparable<K>, V>. The type parameters can have upper bounds, and the bounds can themselves be generic (including self-referential bounds like <T extends Comparable<T>>). There is no syntax for lower-bounded type parameters on type declarations; lower bounds appear only on wildcards. When a non-generic class implements a generic interface, it must supply concrete type arguments: class StringList implements Iterable<String>. All occurrences of E in the interface's methods are replaced by String in the implementing class, and the class must provide implementations whose signatures match those concrete types. A non-generic implementor that leaves the interface raw (class Raw implements Iterable without a type argument) compiles with an unchecked warning and loses type safety for all methods inherited from that interface — all type parameters are treated as Object. When a generic class implements a generic interface, it can supply some, all, or none of its own type parameters as arguments to the interface. ArrayList<E> supplies its own E to List<E>: as ArrayList's type parameter E ranges over types, its List bound follows. A class could also partially apply: class StringKeyedMap<V> implements Map<String, V> supplies String for K and its own V for the map's V. A class can also supply a concrete type plus its own parameter: class Pair<A, B> implements Map.Entry<A, B> where both are forwarded. Interfaces can extend generic interfaces, subject to the same rules: interface SortedList<E extends Comparable<E>> extends List<E> adds a bound not present in List. An interface can extend multiple generic interfaces: interface SearchableMap<K, V> extends Map<K, V>, Iterable<Map.Entry<K, V>>. The type parameters of the extending interface must satisfy all bounds imposed by every extended interface.
Java
// ── Simple generic interface declaration ─────────────────────────────
interface Repository<T, ID> {
    T findById(ID id);
    List<T> findAll();
    void save(T entity);
    void delete(ID id);
}

// Non-generic implementor — supplies concrete types:
class UserRepository implements Repository<User, Long> {
    @Override public User findById(Long id) { ... }
    @Override public List<User> findAll() { ... }
    @Override public void save(User entity) { ... }
    @Override public void delete(Long id) { ... }
}

// Generic implementor — propagates its own type parameters:
class InMemoryRepository<T, ID> implements Repository<T, ID> {
    private final Map<ID, T> store = new HashMap<>();
    @Override public T findById(ID id) { return store.get(id); }
    @Override public List<T> findAll() { return new ArrayList<>(store.values()); }
    @Override public void save(T entity) { /* ... */ }
    @Override public void delete(ID id) { store.remove(id); }
}

// Partial application — one type fixed, one propagated:
class StringKeyedRepository<T> implements Repository<T, String> {
    @Override public T findById(String id) { ... }
    // String is fixed; T is still a type parameter of the class
}

// ── Bounded type parameters on generic interfaces ─────────────────────
interface Sorter<T extends Comparable<T>> {
    List<T> sort(List<T> input);
    T min(List<T> input);
    T max(List<T> input);
}

// Implementor must satisfy the bound — T still requires Comparable<T>:
class NaturalSorter<T extends Comparable<T>> implements Sorter<T> {
    @Override public List<T> sort(List<T> input) {
        List<T> copy = new ArrayList<>(input);
        Collections.sort(copy);
        return copy;
    }
    @Override public T min(List<T> input) { return Collections.min(input); }
    @Override public T max(List<T> input) { return Collections.max(input); }
}

// Can also fix T to a specific Comparable type:
class IntegerSorter implements Sorter<Integer> {
    @Override public List<Integer> sort(List<Integer> input) { ... }
    @Override public Integer min(List<Integer> input) { return Collections.min(input); }
    @Override public Integer max(List<Integer> input) { return Collections.max(input); }
}

// ── Interface extending multiple generic interfaces ────────────────────
interface SearchableContainer<E extends Comparable<E>>
        extends Iterable<E>, Comparable<SearchableContainer<E>> {
    boolean contains(E element);
    E min();
    E max();
    // Inherits: Iterator<E> iterator() from Iterable<E>
    // Inherits: int compareTo(SearchableContainer<E> other) from Comparable<...>
}

// ── Raw implementation — legal but loses type safety ──────────────────
@SuppressWarnings("rawtypes")
class LegacyContainer implements Iterable {   // raw — compiles with warning
    private List items = new ArrayList();
    @Override public Iterator iterator() { return items.iterator(); }  // raw Iterator
}

LegacyContainer legacy = new LegacyContainer();
// All generic type info is gone: items come out as Object
for (Object item : legacy) { System.out.println(item); }

Functional Interfaces, Default Methods, and the Prohibition on Implementing the Same Generic Interface Twice

Functional interfaces are a specific category of generic interface: any interface with exactly one abstract method is a functional interface, and if that interface is generic, lambdas and method references that implement it are type-checked against the concrete type argument supplied at the use site. Function<T, R>, Predicate<T>, Consumer<T>, Supplier<T>, and BiFunction<T, U, R> are all functional generic interfaces from java.util.function. A lambda is an anonymous implementation of the interface, with the type arguments inferred from context. The same lambda expression can implement different functional interfaces depending on what type is expected: n -> n > 0 implements Predicate<Integer>, Function<Integer, Boolean>, or any other single-abstract-method interface whose abstract method takes Integer and returns Boolean or boolean. Default methods in generic interfaces are particularly powerful: they can be defined in terms of the interface's type parameters and provide implementation that all implementors inherit. Iterable<T>'s default forEach(Consumer<? super T> action) is implemented once in the interface in terms of T; every implementor of Iterable<T> automatically gets forEach without implementing it themselves. Interface static methods in generic interfaces can also be generic, but they cannot use the interface's type parameters — they must declare their own. Java prohibits a class from implementing the same generic interface twice with different type arguments: class C implements Comparable<String>, Comparable<Integer> is a compile-time error. The reason is erasure: both Comparable<String> and Comparable<Integer> erase to Comparable, so there would be two bridge methods with the same erased signature compareTo(Object), which is impossible for the JVM to disambiguate. The restriction applies even through the class hierarchy: if a superclass already implements Comparable<String>, a subclass cannot implement Comparable<Integer>. This constraint is one of the most confusing consequences of erasure for API designers working with multiple heterogeneous orderings or transformations. Covariant return types interact with generic interface implementations. An implementing class may override an interface method with a more specific (covariant) return type. When the interface method is generic, the implementing class can supply a narrower bound. This does not violate substitutability because any caller that only knows about the interface gets the declared return type (or a subtype), while callers with static knowledge of the implementation class get the narrower type directly.
Java
// ── Functional generic interface and lambda type inference ───────────
Function<String, Integer> length   = String::length;        // T=String, R=Integer
Function<String, String>  upper    = String::toUpperCase;   // T=String, R=String
Predicate<String>         notEmpty = s -> !s.isEmpty();     // T=String
Consumer<String>          print    = System.out::println;   // T=String

// The same lambda may satisfy different functional interface types:
// n -> n * 2  can be:
Function<Integer, Integer> doubleIt  = n -> n * 2;  // Function<Integer,Integer>
UnaryOperator<Integer>     doubleOp  = n -> n * 2;  // UnaryOperator<Integer> (extends Function)
IntUnaryOperator           doubleInt = n -> n * 2;  // primitive specialization

// ── Generic interface with default method ─────────────────────────────
interface Pipeline<T> {
    T process(T input);

    // Default method defined in terms of T — no implementation cost for implementors:
    default Pipeline<T> andThen(Pipeline<T> next) {
        return input -> next.process(this.process(input));
    }

    default T processAll(List<T> inputs, T identity) {
        T result = identity;
        for (T input : inputs) result = process(result);
        return result;
    }
}

Pipeline<String> trim        = String::trim;
Pipeline<String> upper       = String::toUpperCase;
Pipeline<String> trimAndUpper = trim.andThen(upper);  // composed pipeline

System.out.println(trimAndUpper.process("  hello  "));  // HELLO

// ── Static method in generic interface — cannot use interface's T ─────
interface Factory<T> {
    T create();

    // Static methods declare their own type parameters — cannot refer to T:
    static <E> Factory<List<E>> listFactory() {
        return ArrayList::new;
    }

    // This would be a compile error:
    // static Factory<T> defaultFactory() { ... }  // T is not in scope for static method
}

Factory<List<String>> listFac = Factory.listFactory();
List<String> newList = listFac.create();  // fresh ArrayList<String>

// ── Prohibition on implementing the same interface twice ──────────────
// COMPILE ERROR — both erase to Comparable:
// class Ambiguous implements Comparable<String>, Comparable<Integer> { }

// The problem through inheritance:
class Base implements Comparable<String> {
    @Override public int compareTo(String o) { return 0; }
}

// COMPILE ERROR — Base already provides Comparable<String>:
// class Child extends Base implements Comparable<Integer> { }

// ── Covariant return type in generic interface implementation ─────────
interface Transformer<T, R> {
    R transform(T input);
    List<R> transformAll(List<T> inputs);
}

// Implementor narrows return type from List<String> to ArrayList<String>:
class StringTransformer implements Transformer<Integer, String> {
    @Override
    public String transform(Integer input) { return input.toString(); }

    @Override
    public ArrayList<String> transformAll(List<Integer> inputs) {  // covariant return: ArrayList<String> ⊆ List<String>
        ArrayList<String> result = new ArrayList<>();
        for (Integer i : inputs) result.add(transform(i));
        return result;
    }
}

Transformer<Integer, String> t = new StringTransformer();
List<String> result = t.transformAll(List.of(1, 2, 3));  // caller sees List<String>

StringTransformer direct = new StringTransformer();
ArrayList<String> exact = direct.transformAll(List.of(1, 2, 3));  // caller sees ArrayList<String>

Generic Interface Patterns in the Standard Library and Self-Referential Bounds

The Java standard library is built on a small number of generic interface patterns that recur throughout. Understanding these patterns is both a study in generic interface design and a practical guide to using the APIs correctly. The self-referential bound pattern, also called the recursive generic or curiously recurring template pattern (CRTP), appears in Comparable<T extends Comparable<T>>, Enum<E extends Enum<E>>, and many builder APIs. For Comparable, the declaration says: T is comparable to things of type T — an Integer compares to other Integers, not to arbitrary objects. The self-referential bound enforces that compareTo implementations are homogeneous. The full declaration on Comparable is interface Comparable<T> — the bound on T is at the use site, not in the interface declaration itself: the idiom <T extends Comparable<T>> appears on the method or class that requires comparable elements, not in the Comparable interface. The producer/consumer interface split in java.util.function maps directly to the PECS principle. Supplier<T> produces T values (its only method is T get()) — it is used with ? extends T when a supplier of subtypes is acceptable. Consumer<T> consumes T values (its only method is void accept(T t)) — it is used with ? super T when a consumer of supertypes is acceptable. Function<T, R> both consumes T and produces R. The andThen and compose default methods on Function use ? super T and ? extends R in their signatures (Function<? super R, ? extends V>) to maximize flexibility in composition chains. The Iterable<T> and Iterator<T> pair demonstrate how generic interfaces compose: Iterable<T> has a single abstract method Iterator<T> iterator(), and implementing it brings forEach (via default method) and full for-each loop support. The for-each loop desugars to iterator() plus hasNext()/next()/remove() calls, with the type of element inferred from the type argument to Iterable. This is the standard library's primary mechanism for type-safe collection traversal. Builder interfaces commonly use the CRTP to chain method calls while returning the correct concrete subtype: interface Builder<B extends Builder<B>> { B withName(String name); } allows each method to return B (the concrete builder type) rather than Builder, enabling fluent chains without casting. The subclass supplies itself as the type argument: class PersonBuilder extends Builder<PersonBuilder>.
Java
// ── Self-referential bound (CRTP) — Comparable pattern ──────────────
// The Comparable interface itself has no bound on T:
// public interface Comparable<T> { int compareTo(T o); }

// The self-referential constraint appears on USERS of Comparable:
public static <T extends Comparable<T>> T max(List<T> list) {
    return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}

// Works for any type that is Comparable to itself:
System.out.println(max(List.of(3, 1, 4, 1, 5)));    // 5
System.out.println(max(List.of("apple", "cherry", "banana")));  // cherry

// ── Builder interface with CRTP — fluent API without casting ──────────
interface Builder<B extends Builder<B>> {
    B withName(String name);
    B withDescription(String description);
}

class PersonBuilder implements Builder<PersonBuilder> {
    private String name;
    private String description;

    @Override public PersonBuilder withName(String name) {
        this.name = name;
        return this;
    }
    @Override public PersonBuilder withDescription(String description) {
        this.description = description;
        return this;
    }
    public Person build() { return new Person(name, description); }
}

// Without CRTP, withName() would return Builder<PersonBuilder>, requiring a cast to chain:
Person p = new PersonBuilder()
    .withName("Alice")
    .withDescription("Engineer")
    .build();   // no cast needed — withName() returns PersonBuilder

// ── Function composition and producer/consumer interfaces ─────────────
Function<String, Integer> length   = String::length;
Function<Integer, Boolean> isEven  = n -> n % 2 == 0;

// andThen: Function<T,R> + Function<? super R, ? extends V> → Function<T,V>
Function<String, Boolean> lengthIsEven = length.andThen(isEven);
System.out.println(lengthIsEven.apply("hello"));   // false (5 is odd)
System.out.println(lengthIsEven.apply("hi"));      // true  (2 is even)

// compose: applies the argument first, then this function:
Function<String, Boolean> composed = isEven.compose(length);  // same as andThen but reversed
System.out.println(composed.apply("hello"));  // false

// Consumer composition: andThen chains consumers sequentially
Consumer<String> print  = System.out::println;
Consumer<String> log    = s -> System.err.println("LOG: " + s);
Consumer<String> both   = print.andThen(log);
both.accept("event");    // prints to stdout AND stderr

// Predicate composition: and, or, negate
Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> longStr  = s -> s.length() > 5;
Predicate<String> valid    = notEmpty.and(longStr);
System.out.println(valid.test("hello world"));  // true
System.out.println(valid.test("hi"));           // false

// ── Iterable<T> for-each desugaring ──────────────────────────────────
class Range implements Iterable<Integer> {
    private final int start, end;
    Range(int start, int end) { this.start = start; this.end = end; }

    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            int current = start;
            @Override public boolean hasNext() { return current < end; }
            @Override public Integer next() { return current++; }
        };
    }
    // forEach is inherited from Iterable via default method — no override needed
}

Range r = new Range(1, 6);
for (int n : r) System.out.print(n + " ");  // 1 2 3 4 5for-each works
r.forEach(n -> System.out.print(n * 2 + " "));  // 2 4 6 8 10default method works

Related Topics in Generics

Generic Class
A generic class in Java is a class parameterized by one or more type parameters, written as class Box<T> or class Pair<K, V>. Type parameters allow a single class definition to work correctly with many different types while preserving full compile-time type safety — eliminating the need for casts, preventing ClassCastException at runtime, and making the intended type contract explicit in the API. The compiler erases type parameters at bytecode level (type erasure), replacing them with their bounds or Object, but all type checking occurs at compile time before erasure. This entry covers type parameter declaration and naming conventions, bounded type parameters (extends, super), multiple type parameters, raw types and why they are unsafe, type erasure and its consequences, wildcards (?, ? extends T, ? super T), the PECS principle, recursive type bounds, generic interfaces, inheritance with generic classes, and how to design generic classes that work correctly across all parameterizations.
Generic Method
A generic method is a method that declares its own type parameters, independent of any type parameters on its enclosing class. The type parameters appear in angle brackets before the return type: public static <T> T identity(T value). Generic methods can appear in non-generic classes, in generic classes (adding their own type parameters beyond the class's), and as both static and instance methods. The compiler infers the type arguments for generic method calls from the argument types, eliminating the need for explicit type witness syntax in most cases. This entry covers type parameter declaration on methods, type inference rules and when inference fails, bounded type parameters on methods, returning computed type relationships, generic methods that operate on multiple type parameters, static generic utility methods (the standard library pattern), the interaction between method type parameters and class type parameters, explicit type witness syntax, and the difference between using a class type parameter vs a method type parameter.
Type Parameters
Type parameters are the placeholders — written as single uppercase letters like T, E, K, V — that make Java generics work. A type parameter declares a variable that stands in for a concrete type, to be supplied by the caller or inferred by the compiler. They appear on classes (class Box<T>), interfaces (interface Comparable<T>), and methods (public static <T> T identity(T value)), and they are the mechanism by which the Java type system achieves compile-time type safety without duplicating code for each concrete type. This entry covers where and how type parameters are declared, the naming conventions and why they exist, the scope rules for type parameters (where they are in scope and where they are not), how type parameters interact with inheritance, how the compiler resolves a type parameter to a concrete type at each call site, and the distinction between a type parameter (a declaration) and a type argument (a concrete substitution).
Bounded Types
Bounded type parameters constrain which concrete types may be substituted for a type parameter. An upper bound (T extends Number) restricts T to Number and its subtypes, making Number's methods available on values of type T within the generic code. A lower bound cannot be applied to type parameters (only to wildcards), but multiple upper bounds can be combined with & (T extends Number & Comparable<T> & Serializable). Bounds serve two purposes simultaneously: they restrict the valid type arguments that callers can supply, and they expand what the generic code can do with values of type T by exposing the bound's API. Without a bound, only Object's methods are available on T values. This entry covers single and multiple upper bounds, recursive bounds, bounds on class type parameters vs method type parameters, how bounds interact with type erasure, bridge methods generated by the compiler, and practical patterns where bounds enable algorithms impossible with unbounded type parameters.