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
// ── 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 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
// ── 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 5 — for-each works
r.forEach(n -> System.out.print(n * 2 + " ")); // 2 4 6 8 10 — default method works