W3docs

Java Generic Interfaces

Design generic interfaces in Java that parameterize their method signatures over a type.

Java Generic Interfaces

A generic interface is an interface whose declaration takes one or more type parameters, just like a generic class. It's the third place type parameters can live in Java, alongside generic classes and generic methods, and it's the most consequential one — because almost every reusable contract in the standard library is a generic interface. List<E>, Map<K, V>, Comparator<T>, Function<T, R>, Supplier<T>, Iterable<T>, Iterator<T> — they're the spine of modern Java.

The syntax

The type parameter list sits between the interface name and the body:

public interface Container<T> {
  void add(T item);
  T   get(int index);
  int size();
}

Read it as "a Container parameterised over some element type T." Inside the interface, T can appear in method parameters, return types, and any other position a type can go. Default methods (Java 8+) and private interface methods (Java 9+) can use T too.

When you implement the interface, you have to make a choice — and the choice is the whole architectural decision:

// 1. Pick a concrete type — the implementation is specialised.
public class StringContainer implements Container<String> {
  private final List<String> items = new ArrayList<>();
  public void   add(String s)  { items.add(s); }
  public String get(int i)     { return items.get(i); }
  public int    size()         { return items.size(); }
}

// 2. Stay generic — pass the parameter through to the class.
public class ListContainer<E> implements Container<E> {
  private final List<E> items = new ArrayList<>();
  public void add(E e)    { items.add(e); }
  public E    get(int i)  { return items.get(i); }
  public int  size()      { return items.size(); }
}

Both are valid. The first is "a Container that holds Strings, specifically." The second is "a Container parameterised over the same E the caller picks." Most reusable containers are the second form; specialised ones (a JsonObject is a "container of JsonValues, nothing else") are the first.

Multiple type parameters

The shape generalises directly to two or more parameters. Look at java.util.Map:

public interface Map<K, V> {
  V    put(K key, V value);
  V    get(Object key);          // Object on purpose — see below
  Set<K> keySet();
  Collection<V> values();
  ...
}

The Map<K, V> declaration says "two parameters: K for keys, V for values." Implementations pin them down or pass them through:

public class StringIntMap implements Map<String, Integer> { ... }   // pinned
public class HashMap<K, V> implements Map<K, V>           { ... }   // passed through

The get(Object key) in Map's signature is a deliberate piece of API design — it accepts any object as a lookup key for historical reasons. We'll come back to that in the Collections part; it isn't a generics rule, just a Map-specific compromise.

Functional interfaces are generic interfaces

The interfaces in java.util.functionFunction, Predicate, Consumer, Supplier, BiFunction, and so on — are all generic interfaces with a single abstract method, which makes them targets for lambdas:

public interface Function<T, R> {
  R apply(T t);
}

public interface Predicate<T> {
  boolean test(T t);
}

public interface Comparator<T> {
  int compare(T a, T b);
}

When you write s -> s.length(), the compiler infers a Function<String, Integer> from context. The two type parameters of Function<T, R> are filled in by the surrounding code — usually a stream operation or a method parameter:

List<String> names = List.of("Ada", "Grace", "Linus");
List<Integer> lengths = names.stream()
    .map(s -> s.length())          // Function<String, Integer> — both inferred
    .toList();

That's a generic interface and a generic method (Stream.map) cooperating. The method's signature is roughly <R> Stream<R> map(Function<? super T, ? extends R> mapper) — wildcards we'll meet in Wildcards, and a type parameter that picks R based on whichever function you handed in.

Self-referential interfaces — Comparable<T>

One of the most useful patterns in the standard library is the self-referential generic interface, where the type argument is the implementing class itself:

public interface Comparable<T> {
  int compareTo(T other);
}

public class Money implements Comparable<Money> {
  private final long cents;
  // ...
  @Override public int compareTo(Money other) {
    return Long.compare(this.cents, other.cents);
  }
}

Read class Money implements Comparable<Money> as "Money knows how to compare itself to other Moneys." This is what makes Collections.sort(List<Money> list) work without a Comparator — every element already carries a compareTo(Money) it inherited from the interface contract, and the type system enforces that the argument has the same type as the receiver.

Comparable<T> is the canonical example of this shape — every value type in the JDK that has a natural order implements it: Integer implements Comparable<Integer>, String implements Comparable<String>, LocalDate implements Comparable<LocalDate>, and so on.

Inheriting from a generic interface

The same three choices show up for interface-to-interface inheritance — extends instead of implements, but the rules are the same:

// Pin the parameter.
public interface StringList extends List<String> { ... }

// Pass it through.
public interface MyList<E> extends List<E> { ... }

// Add new ones.
public interface IndexedList<E, I> extends List<E> { I indexOf(E e); }

Same idea as for classes — the parent's parameter must be supplied (with a real type or a forwarded one), and the child can add parameters of its own on top.

Default methods can use the type parameter

Java 8 added default methods to interfaces. They can use the interface's type parameter exactly like any abstract method can:

public interface Container<T> {
  void add(T item);
  T    get(int index);
  int  size();

  default boolean isEmpty()        { return size() == 0; }
  default void addAll(Iterable<T> items) {
    for (T item : items) add(item);
  }
}

The default method addAll works for every implementer, regardless of what T they picked. That's how Collection<E> provides forEach, removeIf, stream, and friends — one default body, every implementation gets it.

A worked example: a generic Repository interface

A small repository abstraction — interface + two implementations. The first implementation pins the entity type (UserRepo holds users only); the second stays generic (InMemoryRepo<E> holds whatever the caller asks for). Both satisfy the same contract from the caller's side.

java— editable, runs on the server

InMemoryRepo<E> is the reusable shape — the type parameter is forwarded from the interface to the class, so the same body works for User, String, or anything else. UserRepo is the specialised shape — it pins E to User and then adds methods that only make sense for users. Both honour the same Repository<E> contract, and both inherit isEmpty() for free from the default method.

What's next

So far every type parameter has been completely unrestricted — T could be anything. In practice you often want to say "T must be a Number" or "T must implement Comparable," so you can actually call methods on it inside the body. That's what bounded type parameters are for, and they're the next chapter. Continue to Java Bounded Type Parameters.

Practice

Practice

You have `interface Repository<E> { E find(int id); }` and `class UserRepo implements Repository<User>`. A caller writes `Repository r = new UserRepo();` (no type argument) and then `User u = r.find(1);`. What's the problem?