W3docs

Clases Genéricas en Java

Aprende a escribir clases genéricas en Java con parámetros de tipo, múltiples parámetros, el operador diamante y un ejemplo funcional de Stack tipado.

Una clase genérica es una clase cuya declaración lleva uno o más parámetros de tipo — marcadores de posición que el llamador rellena al crear una instancia. El mismo cuerpo de clase describe entonces toda una familia de tipos: Box<String>, Box<Integer>, Box<User> son tipos distintos en tiempo de compilación que comparten un único código fuente. Esta es la forma más común que toman los genéricos, y es como están escritas todas las colecciones de java.util, cada Optional, cada Future y cada CompletableFuture.

La sintaxis

La lista de parámetros de tipo va entre el nombre de la clase y el cuerpo, entre corchetes angulares:

public class Box<T> {
  private T value;

  public Box(T value) { this.value = value; }

  public T get()              { return value; }
  public void set(T value)    { this.value = value; }
}

Lee la declaración como "un Box parametrizado sobre algún tipo T." Dentro de la clase, T se comporta como cualquier otro tipo — puedes declarar campos de tipo T, métodos que devuelven T, parámetros de tipo T. El compilador lo trata como un tipo real y desconocido hasta que el llamador elige uno.

En el punto de llamada, se indica el tipo concreto:

Box<String>  greeting = new Box<>("hello");
Box<Integer> answer   = new Box<>(42);

String s = greeting.get();   // already a String — no cast
int i    = answer.get();     // auto-unboxed from Integer

El <> de la derecha es el operador diamante — el compilador infiere el argumento de tipo a partir de la declaración del lado izquierdo. Puedes escribir new Box<String>("hello") de forma explícita, pero casi nunca es necesario.

Múltiples parámetros de tipo

Una clase puede declarar más de un parámetro de tipo. El ejemplo clásico es un par clave/valor:

public class Entry<K, V> {
  private final K key;
  private final V value;

  public Entry(K key, V value) {
    this.key   = key;
    this.value = value;
  }

  public K key()   { return key; }
  public V value() { return value; }
}

Entry<String, Integer> score = new Entry<>("Ada", 100);
String name = score.key();
int    n    = score.value();

La convención es usar nombres de una sola letra — K para clave, V para valor, E para elemento, R para retorno, T para "tipo genérico." Cuando se necesita más claridad (algo raro), se permiten nombres más largos: Map<KeyType, ValueType> es válido, aunque poco habitual.

Acotar el parámetro de tipo

Por defecto, un parámetro de tipo representa "cualquier tipo," por lo que dentro de la clase solo puedes llamar a los métodos que tiene todo objeto (equals, toString, hashCode). Si tu clase necesita hacer algo con los valores — compararlos, sumarlos, leer una propiedad — debes restringir T con un límite superior usando extends:

// T can be any type that is (or extends) Number, so .doubleValue() is callable.
public class NumberBox<T extends Number> {
  private final T value;

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

  public double asDouble() { return value.doubleValue(); }
}

NumberBox<Integer> n = new NumberBox<>(42);   // fine — Integer is a Number
// NumberBox<String> bad = ...;               // ❌ String is not a Number

extends aquí significa "es un subtipo de," y funciona tanto para clases como para interfaces. Incluso puedes requerir varios límites a la vez — <T extends Number & Comparable<T>> — con el límite de clase (si existe) listado primero. El límite es también lo que hace que el tipo sea utilizable: sin extends Number, value.doubleValue() no compilaría.

Constructores genéricos

El parámetro de tipo está fijado por la instancia, por lo que cada constructor de una clase genérica ya tiene acceso a T:

public class Pair<T> {
  private final T first;
  private final T second;

  public Pair(T first, T second) { this.first = first; this.second = second; }
  public Pair(T both)            { this(both, both); }
}

Los constructores en sí mismos también pueden ser genéricos con parámetros de tipo adicionales independientes de los de la clase — pero eso es poco común y corresponde al siguiente capítulo sobre métodos genéricos.

Las clases genéricas pueden heredar entre sí

Una subclase puede heredar de una clase genérica de tres formas. Cada una significa algo diferente:

// 1. Lock the parent's type parameter — concrete subclass for one element type.
public class StringList extends ArrayList<String> { ... }

// 2. Pass the type parameter through — the subclass is still generic.
public class MyList<E> extends ArrayList<E> { ... }

// 3. Add new type parameters of your own.
public class TaggedList<E, Tag> extends ArrayList<E> { ... }

La forma intermedia es la más común — se propaga el parámetro del padre a los propios llamadores. La primera forma es la que se usa cuando la subclase es especializada: un árbol de nodos solo de strings.

Campos y el parámetro de tipo

Cada instancia de Box<...> lleva su propio T. El bytecode no — en tiempo de ejecución la JVM solo ve Box (esto es el borrado de tipos, tratado más adelante en esta parte). La consecuencia es que el parámetro de tipo pertenece a la instancia, no al objeto de clase:

Box<String>  a = new Box<>("hi");
Box<Integer> b = new Box<>(5);

a.getClass() == b.getClass();   // true — both are class Box

Es un dato útil a tener en cuenta: Box<String> y Box<Integer> son tipos diferentes para el compilador pero la misma clase en tiempo de ejecución. Volveremos a esto en Java Type Erasure.

Los miembros estáticos no pueden ver el parámetro de tipo

Los campos y métodos estáticos pertenecen a la clase, no a ninguna instancia en particular — por lo que no pueden ver el T de la instancia. Esto es ilegal:

public class Box<T> {
  private static T defaultValue;        // ❌ won't compile — no T at the static level
  public  static T empty() { ... }      // ❌ same problem
}

Un método estático que necesita un parámetro de tipo debe declarar el suyo propio, independiente del de la clase. Ese es el tema del próximo capítulo.

Diseñando el tuyo propio: un pequeño stack tipado

Una clase funcional y completa para unirlo todo — un Stack genérico con push, pop, peek y size. Está parametrizado sobre E (elemento), respaldado internamente por un Object[] (debido a las restricciones de arrays genéricos), y el cast sin verificar en pop es el tipo de solución bien contenida que verás en código real.

java— editable, runs on the server

Las anotaciones @SuppressWarnings("unchecked") están en las dos lecturas que deben hacer cast de Object a E. Esos casts son seguros — push solo almacena valores de tipo E — pero el compilador no puede verificarlo, porque el borrado ha eliminado E del bytecode. Suprimir la advertencia localmente, en el ámbito más pequeño posible, es lo correcto.

¿Qué sigue?

Has visto el parámetro a nivel de clase. A veces necesitas que un solo método sea genérico, con su propio parámetro de tipo independiente de la clase — útil para métodos de utilidad, helpers estáticos, y cualquier operación cuya relación de tipos vive solo dentro de ese método. Continúa con Java Generic Methods.

Práctica

Práctica
Escribes `public class Box<T> { private static T value; }`. El compilador lo rechaza. ¿Por qué?
Escribes `public class Box<T> { private static T value; }`. El compilador lo rechaza. ¿Por qué?
Was this page helpful?