1. El problema de las colecciones «crudas» (raw types)
Un poco de historia. Antes de Java 5 todas las colecciones eran «omnívoras». Almacenaban objetos de tipo Object, y el compilador no controlaba qué metías en ellas. ¿Quieres meter una cadena? Adelante. ¿Un número? ¿Por qué no? ¿Un gato? También se podía.
// Ejemplo de colecciones "crudas" (raw types), Java anterior a la versión 5
List list = new ArrayList();
list.add("Hola");
list.add(42);
list.add(new Object());
El problema aparecía al «sacar» y usar el valor:
String s = (String) list.get(0); // OK, es una cadena
String s2 = (String) list.get(1); // ¡BOOM! ClassCastException
El compilador guarda silencio, y en tiempo de ejecución obtienes ClassCastException. Es como una caja con la etiqueta «manzanas» en la que hay una taza, un plátano y un erizo.
¿Por qué es malo?
- Los errores se manifiestan solo en tiempo de ejecución.
- Se mezclan los tipos: hay que convertir (cast) los objetos manualmente al tipo necesario.
- El código es menos legible y más peligroso.
La solución: genéricos
Los genéricos (generics) son un mecanismo que permite crear clases, interfaces y métodos con parámetros de tipo. Es decir, le dices a la colección: «Guarda solo cadenas», y el compilador lo hace cumplir estrictamente.
List<String> words = new ArrayList<>();
words.add("Hola");
words.add("Mundo");
// words.add(42); // Error de compilación: no se puede añadir un int a List<String>
Ahora el compilador no permitirá meter en la lista nada salvo cadenas. El error se detecta antes de ejecutar el programa.
La idea principal de los genéricos:
Proporcionar seguridad de tipos para colecciones (y no solo) para que los errores se capturen en la compilación, no en tiempo de ejecución.
2. Sintaxis de genéricos: cómo se ve en el código
Especificar el tipo entre los signos de menor y mayor
Cuando crees una colección, especifica el tipo de los elementos entre <>:
List<String> names = new ArrayList<>();
names.add("Alicia");
names.add("Bob");
// names.add(123); // Error: no se puede añadir un número a una lista de cadenas
String first = names.get(0); // ¡No hace falta casting!
Clásicos:
- List<String> — lista de cadenas
- List<Integer> — lista de números enteros
- Set<Double> — conjunto de números de punto flotante
- Map<String, Integer> — clave String, valor Integer
¿Por qué no escribir simplemente List?
Se puede, pero pierdes todas las ventajas de los genéricos, y el compilador te avisará:
List list = new ArrayList(); // raw type - no recomendado
list.add("Hello");
list.add(7.5);
String s = (String) list.get(1); // ¡Hola, ClassCastException!
El código moderno en Java siempre usa genéricos.
Operador diamante <>
Desde Java 7 se puede omitir el tipo a la derecha si se entiende por el contexto:
List<String> list = new ArrayList<>(); // El compilador infiere que es <String>
3. Genéricos para distintas colecciones
Ejemplos para List, Set, Map
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
Set<String> uniqueNames = new HashSet<>();
uniqueNames.add("Alicia");
uniqueNames.add("Bob");
Map<String, Integer> ages = new HashMap<>();
ages.put("Alicia", 23);
ages.put("Bob", 31);
Ejemplo con una clase propia
class Student {
String name;
int age;
// ...
}
List<Student> students = new ArrayList<>();
students.add(new Student());
4. Matices útiles
Ventajas de los genéricos
Seguridad de tipos. El compilador se asegura de que a la colección solo entren elementos del tipo adecuado.
No hay necesidad de conversiones de tipo. Antes: String s = (String) list.get(0);. Ahora: String s = list.get(0);.
Código más legible y fiable. Menos sorpresas en tiempo de ejecución.
Limitaciones de los genéricos
No se pueden usar tipos primitivos. Los genéricos funcionan solo con objetos, no con primitivos (int, double, boolean). Usa clases envoltorio: Integer, Double, Boolean.
List<Integer> numbers = new ArrayList<>();
numbers.add(10); // int se convierte automáticamente en Integer (autoboxing)
Breve sobre el borrado de tipos (type erasure)
En Java los genéricos se implementan mediante borrado de tipos: tras la compilación, la información sobre los parámetros de tipo se borra y, en tiempo de ejecución, la JVM no sabe que era un List<String> y no simplemente un List. Se hizo por compatibilidad hacia atrás.
Consecuencia: no se puede comprobar el parámetro de tipo con instanceof usando un argumento de tipo concreto.
List<String> list = new ArrayList<>();
// if (list instanceof List<String>) { ... } // Error de compilación
Intentar agregar un elemento de otro tipo: error de compilación
List<String> words = new ArrayList<>();
words.add("Hello");
// words.add(123); // Error de compilación: incompatible types: int cannot be converted to String
Map<String, Integer> map = new HashMap<>();
map.put("Gato", 5);
// map.put(3, "Elefante"); // Error: la clave debe ser String y el valor, Integer
Y eso es fantástico: los errores se detectan en la compilación.
No solo colecciones
También puedes usar genéricos en clases y métodos propios. Por ejemplo, una «Caja» genérica:
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("Hola");
System.out.println(stringBox.get());
Box<Integer> intBox = new Box<>();
intBox.set(42);
System.out.println(intBox.get());
En colecciones los genéricos son el estándar, pero también los verás en otros lugares, por ejemplo, en Stream API y Optional.
5. Errores típicos al trabajar con genéricos
Error n.º 1: Uso de colecciones «crudas». La forma List list = new ArrayList(); elimina la seguridad de tipos. Indica siempre los parámetros de tipo, por ejemplo List<String>.
Error n.º 2: Intentar usar primitivos. No puedes escribir List<int>; usa List<Integer>.
Error n.º 3: Hacer cast manual al leer de la colección. Si usas genéricos, el cast (String) list.get(i) no es necesario. Si hay que hacerlo, en algún punto se han roto los tipos.
Error n.º 4: Esperar que los parámetros de tipo estén disponibles en tiempo de ejecución. Debido al borrado de tipos, no se pueden comprobar con instanceof al estilo de List<String>.
Error n.º 5: Mezclar tipos distintos en una misma colección. Si se ha declarado List<String>, no intentes añadir Integer: el compilador no lo permitirá, y eso es bueno.
GO TO FULL VERSION