W3docs

Interfaces Genéricas en Java

Aprende a diseñar interfaces genéricas en Java que parametrizan sus firmas de métodos sobre un tipo.

Una interfaz genérica es una interfaz cuya declaración toma uno o más parámetros de tipo, igual que una clase genérica. Es el tercer lugar donde pueden vivir los parámetros de tipo en Java, junto con las clases genéricas y los métodos genéricos, y es el más importante — porque casi todo contrato reutilizable en la biblioteca estándar es una interfaz genérica. List<E>, Map<K, V>, Comparator<T>, Function<T, R>, Supplier<T>, Iterable<T>, Iterator<T> — son la columna vertebral del Java moderno.

La sintaxis

La lista de parámetros de tipo se ubica entre el nombre de la interfaz y el cuerpo:

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

Léelo como "un Container parametrizado sobre algún tipo de elemento T." Dentro de la interfaz, T puede aparecer en los parámetros de los métodos, en los tipos de retorno y en cualquier otra posición donde pueda ir un tipo. Los métodos por defecto (Java 8+) y los métodos privados de interfaz (Java 9+) también pueden usar T.

Cuando implementas la interfaz, tienes que tomar una decisión — y esa decisión es toda la decisión arquitectónica:

// 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(); }
}

Ambas son válidas. La primera es "un Container que contiene Strings, específicamente." La segunda es "un Container parametrizado sobre el mismo E que el llamador elige." La mayoría de los contenedores reutilizables son la segunda forma; los especializados (un JsonObject es un "contenedor de JsonValues, nada más") son la primera.

Múltiples parámetros de tipo

La forma se generaliza directamente a dos o más parámetros. Mira 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();
  ...
}

La declaración Map<K, V> dice "dos parámetros: K para las claves, V para los valores." Las implementaciones los fijan o los pasan a través:

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

El get(Object key) en la firma de Map es una decisión deliberada de diseño de API — acepta cualquier objeto como clave de búsqueda por razones históricas. Volveremos a eso en la parte de Colecciones; no es una regla de los genéricos, sino un compromiso específico de Map.

Las interfaces funcionales son interfaces genéricas

Las interfaces en java.util.functionFunction, Predicate, Consumer, Supplier, BiFunction, y demás — son todas interfaces genéricas con un único método abstracto, lo que las convierte en objetivos para las 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);
}

Cuando escribes s -> s.length(), el compilador infiere un Function<String, Integer> del contexto. Los dos parámetros de tipo de Function<T, R> se rellenan con el código circundante — generalmente una operación de stream o un parámetro de método:

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

Eso es una interfaz genérica y un método genérico (Stream.map) cooperando. La firma del método es aproximadamente <R> Stream<R> map(Function<? super T, ? extends R> mapper) — comodines que veremos en Wildcards, y un parámetro de tipo que elige R basándose en la función que pasaste.

Interfaces autorreferentes — Comparable<T>

Uno de los patrones más útiles en la biblioteca estándar es la interfaz genérica autorreferente, donde el argumento de tipo es la propia clase que la implementa:

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);
  }
}

Lee class Money implements Comparable<Money> como "Money sabe cómo compararse con otros Money." Esto es lo que hace que Collections.sort(List<Money> list) funcione sin un Comparator — cada elemento ya tiene un compareTo(Money) que heredó del contrato de la interfaz, y el sistema de tipos garantiza que el argumento tenga el mismo tipo que el receptor.

Comparable<T> es el ejemplo canónico de esta forma — cada tipo de valor en el JDK que tiene un orden natural lo implementa: Integer implements Comparable<Integer>, String implements Comparable<String>, LocalDate implements Comparable<LocalDate>, y así sucesivamente.

Herencia de una interfaz genérica

Las mismas tres opciones aparecen para la herencia entre interfaces — extends en lugar de implements, pero las reglas son las mismas:

// 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); }

La misma idea que para las clases — el parámetro del padre debe ser suministrado (con un tipo real o uno reenviado), y el hijo puede agregar parámetros propios encima.

Los métodos por defecto pueden usar el parámetro de tipo

Java 8 añadió métodos por defecto a las interfaces. Pueden usar el parámetro de tipo de la interfaz exactamente igual que cualquier método abstracto:

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);
  }
}

El método por defecto addAll funciona para todo implementador, independientemente del T que hayan elegido. Así es como Collection<E> proporciona forEach, removeIf, stream y similares — un cuerpo por defecto, y cada implementación lo obtiene.

Un ejemplo completo: una interfaz genérica Repository

Una pequeña abstracción de repositorio — interfaz más dos implementaciones. La primera implementación fija el tipo de entidad (UserRepo solo contiene usuarios); la segunda permanece genérica (InMemoryRepo<E> contiene lo que el llamador solicite). Ambas satisfacen el mismo contrato desde el lado del llamador.

java— editable, runs on the server

InMemoryRepo<E> es la forma reutilizable — el parámetro de tipo se reenvía desde la interfaz a la clase, por lo que el mismo cuerpo funciona para User, String o cualquier otra cosa. UserRepo es la forma especializada — fija E a User y luego añade métodos que solo tienen sentido para usuarios. Ambas respetan el mismo contrato Repository<E>, y ambas heredan isEmpty() de forma gratuita desde el método por defecto.

Qué sigue

Hasta ahora, cada parámetro de tipo ha sido completamente sin restricciones — T podía ser cualquier cosa. En la práctica, a menudo querrás decir "T debe ser un Number" o "T debe implementar Comparable," para poder llamar métodos sobre él dentro del cuerpo. Para eso sirven los parámetros de tipo acotados, y son el siguiente capítulo. Continúa en Parámetros de Tipo Acotados en Java.

Práctica

Práctica
Tienes `interface Repository<E> { E find(int id); }` y `class UserRepo implements Repository<User>`. Un llamador escribe `Repository r = new UserRepo();` (sin argumento de tipo) y luego `User u = r.find(1);`. ¿Cuál es el problema?
Tienes `interface Repository<E> { E find(int id); }` y `class UserRepo implements Repository<User>`. Un llamador escribe `Repository r = new UserRepo();` (sin argumento de tipo) y luego `User u = r.find(1);`. ¿Cuál es el problema?
Was this page helpful?