Los genéricos en Java representan el concepto de "tipos parametrizados". Esta capacidad permite que las clases, interfaces y métodos operen sobre tipos de datos que se definen como parámetros en el momento de su declaración, de manera análoga a cómo un método recibe argumentos variables.
El objetivo primordial de los genéricos es proporcionar una capa de seguridad de tipos (type safety) en tiempo de compilación, eliminando la necesidad de realizar conversiones explícitas (castings) manuales y reduciendo el riesgo de errores de tipo en tiempo de ejecución.
Borrado de tipos (Type Erasure)
Es fundamental entender que los genéricos son procesados principalmente por el compilador de Java (javac). El compilador utiliza la información de los genéricos para validar la integridad de los datos, pero una vez terminada la validación, dicha información se elimina durante el proceso de compilación para mantener la compatibilidad con versiones anteriores de la JVM. Este fenómeno se conoce como "borrado de tipos".
package com.ejemplo.tecnico;
import java.util.LinkedList;
public class Envoltorio<E> {
private E elemento;
public static void inicializar() {
LinkedList<Double> precios = new LinkedList<>();
precios.add(19.99);
}
public E obtenerContenido() {
return elemento;
}
}
Aunque en el archivo .class el tipo específico desaparece del código ejecutable, la metadata del archivo de clase conserva ciertas firmas para permitir que las herramientas de desarrollo y de reflexión puedan identificar las restricciones originales. Por esta razón, algunos descompiladores modernos son capaces de reconstruir los tipos genéricos a partir de los atributos de firma del bytecode.
Comportamiento de la clase en tiempo de ejecución
Dado que existe el borrado de tipos, todas las instancias de una clase genérica comparten la misma clase en tiempo de ejecución, independientemente de los argumentos de tipo utilizados.
LinkedList<Integer> listaEnteros = new LinkedList<>();
LinkedList<String> listaCadenas = new LinkedList<>();
// Ambas expresiones devuelven 'true'
System.out.println(listaEnteros.getClass() == listaCadenas.getClass());
System.out.println(listaEnteros.getClass() == LinkedList.class);
Uso de comodines (Wildcards)
Los comodines permiten flexibilizar el uso de tipos parametrizados mediante límites superiorse e inferiores.
Límite superior (Upper Bound)
Utiliza ? extends T para restringir el tipo a una clase específica o cualquiera de sus subclases.
// Solo acepta Number o sus hijos como Integer, Double, etc.
List<? extends Number> miLista;
Límite inferior (Lower Bound)
Utiliza ? super T para restringir el tipo a una clase específica o cualquiera de sus superclases.
// Solo acepta Integer o sus ancestros como Number u Object
List<? super Integer> miLista;
Compatibilidad y tipos crudos (Raw Types)
Java permite la interacción entre tipos parametrizados y tipos "crudos" (sin parámetros) para asegurar la retrocompatibilidad. Sin embargo, esto traslada la responsabilidad de la seguridad de tipos directamente al desarrollador.
// Asignar una instancia genérica a una referencia cruda
List listaCruda = new ArrayList<Integer>();
listaCruda.add("Texto"); // Compila, pero es peligroso
// Asignar una instancia cruda a una referencia parametrizada
List<Double> listaPrecios = new ArrayList();
listaPrecios.add(10.5);
// listaPrecios.add("Error"); // Error de compilación detectado por la referencia
Interfaces genéricas
Al implementar una interfaz genérica, la clase implementadora debe definir el tipo concreto o mantener la genericidad.
interface Servicio<T> {
void procesar(T entrada);
}
// Implementación definiendo el tipo concreto
class ServicioCadenas implements Servicio<String> {
@Override
public void procesar(String entrada) {
System.out.println(entrada.toLowerCase());
}
}
Clases genéricas
Los tipos genéricos se definen en la declaración de la clase. Es importante notar que no se pueden utilizar parámetros de tipo en contextos estáticos (campos o métodos estáticos), ya que los tipos se determinan al instanciar la clase y el contexto estático es compartido.
class ContenedorDinamico<T> {
private T valor;
public void asignar(T valor) {
this.valor = valor;
}
public T recuperar() {
return valor;
}
public static void main(String[] args) {
ContenedorDinamico<Integer> instancia = new ContenedorDinamico<>();
instancia.asignar(100);
}
}
Métodos genéricos
Un método puede declarar sus propios parámetros de tipo independientemente de la clase. El parámetro de tipo se coloca antes del tipo de retorno.
public <V> void imprimirDato(V dato) {
System.out.println("Tipo: " + dato.getClass().getName());
System.out.println("Valor: " + dato);
}