Al cerrar el módulo 9 quedó una deuda declarada con todas las letras: ClienteCatalogo, ClienteMetadatos y EnriquecedorCatalogo repiten la misma estructura —hacer la petición, comprobar el código, analizar el cuerpo, traducir el error— cambiando únicamente el tipo del resultado. Y no tenías forma de escribir "esto es un cliente de algo que devuelve cosas de tipo T" sin duplicar la clase entera.
También quedó una deuda más vieja. Desde el módulo 5 llevas escribiendo List<Material>, Map<String, Libro>, Optional<Ficha>, CompletableFuture<HttpResponse<String>>, y cada vez que aparecían esos <...> te dijimos lo mismo: "es el tipo de los elementos, la teoría está en 10-01". Esta es 10-01.
Los genéricos son la respuesta de Java a una pregunta muy concreta: ¿cómo escribo código que funcione con cualquier tipo, sin renunciar a que el compilador compruebe los tipos por mí? Antes de Java 5 la respuesta era "usa Object y haz casts", y el precio era que los errores de tipo aparecían en ejecución, en producción, con un ClassCastException que nadie vio venir. Con genéricos, esos mismos errores aparecen en compilación, en tu pantalla, antes de que nadie los sufra.
Esta lección explica qué significa exactamente ese <T>, cómo escribir tus propias clases e interfaces genéricas, cómo acotar los tipos aceptados, cómo funcionan los comodines y el principio PECS, y —muy importante— qué NO puedes hacer con genéricos y por qué, porque el borrado de tipos deja un conjunto de limitaciones que sorprenden a todo el mundo la primera vez.
Al terminar, BiblioTech tendrá un Repositorio<T extends Identificable> que generaliza de un plumazo CatalogoConcurrente y RegistroPrestamosSeguro, y un Resultado<T> genérico que sustituye al Resultado del módulo 6.
Contenido
- El problema: contenedores de
ObjectyClassCastException - La misma escena con genéricos
- Clases genéricas propias:
Caja<T> - Convenciones de nombres para los parámetros de tipo
- Instanciación y el operador diamante
<> - Interfaces genéricas propias
- Métodos genéricos e inferencia
- Límites:
<T extends Comparable<T>> - Límites múltiples con
& - Invariancia: por qué
List<String>no es unList<Object> - El contraste con los arrays covariantes
- Comodines:
<?>,<? extends X>y<? super X> - PECS: Producer Extends, Consumer Super
- Borrado de tipos: qué hace realmente el compilador
- Las consecuencias prácticas del borrado
- Métodos puente (bridge methods)
- Tipos crudos (raw types) y por qué existen
@SuppressWarnings("unchecked")y cuándo es legítimo- El token de tipo:
Class<T> - Genéricos y excepciones
- BiblioTech:
Repositorio<T extends Identificable> - BiblioTech:
Resultado<T>genérico - Errores Comunes y Consejos
- Ejercicios
- El problema: contenedores de
Object y ClassCastException
Object y ClassCastExceptionImagina que estás en Java 1.4 y quieres una lista de libros. La única lista que existe guarda Object:
import java.util.ArrayList;
import java.util.List;
public class CatalogoAntiguo {
public static void main(String[] args) {
List catalogo = new ArrayList(); // sin genericos: guarda Object
catalogo.add(new Libro("Java Efectivo", "978-0000000001"));
catalogo.add(new Libro("Patrones de Diseño", "978-0000000002"));
catalogo.add("Refactorización"); // ¡ERROR! pero el compilador calla
for (int i = 0; i < catalogo.size(); i++) {
Libro libro = (Libro) catalogo.get(i); // cast obligatorio
System.out.println(libro.getTitulo());
}
}
}Este código compila sin un solo error. Y al ejecutarlo:
Java Efectivo
Patrones de Diseño
Exception in thread "main" java.lang.ClassCastException:
class java.lang.String cannot be cast to class Libro
at CatalogoAntiguo.main(CatalogoAntiguo.java:14)Fíjate en los tres males, porque son exactamente los tres que resuelven los genéricos:
| Mal | Descripción | Coste |
|---|---|---|
| No hay comprobación | catalogo.add("Refactorización") es válido para el compilador |
El error viaja hasta producción |
| Casts por todas partes | Cada get() obliga a un (Libro) |
Ruido y oportunidad de equivocarse |
| El fallo está lejos de la causa | El add erróneo está en la línea 10; la excepción salta en la 14 |
Depuración larga |
Y hay un cuarto mal, más sutil: la API no documenta nada. Si ves un método List cargarCatalogo(), no sabes qué contiene la lista. Tienes que leer la implementación o confiar en el nombre.
- La misma escena con genéricos
import java.util.ArrayList;
import java.util.List;
public class CatalogoModerno {
public static void main(String[] args) {
List<Libro> catalogo = new ArrayList<>();
catalogo.add(new Libro("Java Efectivo", "978-0000000001"));
catalogo.add(new Libro("Patrones de Diseño", "978-0000000002"));
// catalogo.add("Refactorización"); // NO COMPILA
for (Libro libro : catalogo) { // sin cast
System.out.println(libro.getTitulo());
}
}
}Si descomentas la línea del String, el compilador dice:
error: no suitable method found for add(String)
catalogo.add("Refactorización");
^
method Collection.add(Libro) is not applicable
(argument mismatch; String cannot be converted to Libro)El error se ha movido de las 3 de la madrugada en producción a las 10 de la mañana en tu editor. Ese es todo el valor de los genéricos, y es enorme.
Con la lista genérica, además:
- Desaparecen los casts. El compilador sabe que
get(i)devuelve unLibro. - La API se documenta sola.
List<Libro> cargarCatalogo()no admite dudas. - El IDE te autocompleta. Al escribir
libro.te ofrece los métodos deLibro, no los deObject.
La idea central en una frase: un genérico es un parámetro que no es un valor, sino un tipo. Igual que un método recibe parámetros de valor (
int dias), una clase genérica recibe parámetros de tipo (<T>), y el compilador comprueba la coherencia de todo el uso.
- Clases genéricas propias:
Caja<T>
Caja<T>La sintaxis es directa: se declara el parámetro de tipo entre <> justo después del nombre de la clase, y a partir de ahí ese nombre se usa como si fuera un tipo real dentro de todo el cuerpo.
package com.nexussoftware.bibliotech.dominio;
/**
* Contenedor de un unico valor de tipo T.
* T es un PARAMETRO DE TIPO: no existe hasta que alguien
* escribe Caja<Libro> o Caja<String>.
*/
public class Caja<T> {
private T contenido; // campo del tipo parametrizado
public Caja(T contenido) { // parametro del tipo parametrizado
this.contenido = contenido;
}
public T obtener() { // retorno del tipo parametrizado
return contenido;
}
public void guardar(T contenido) {
this.contenido = contenido;
}
public boolean estaVacia() {
return contenido == null;
}
@Override
public String toString() {
return "Caja[" + contenido + "]";
}
}Uso:
Caja<Libro> cajaLibro = new Caja<>(new Libro("Java Efectivo", "978-0000000001"));
Libro l = cajaLibro.obtener(); // sin cast: el compilador sabe que es Libro
Caja<String> cajaTexto = new Caja<>("Sala 3");
String s = cajaTexto.obtener();
// cajaLibro.guardar("esto no"); // NO COMPILACaja<Libro> y Caja<String> son tipos distintos para el compilador. Un Caja<Libro> no se puede asignar a una variable Caja<String> ni al revés. A esto se le llama que la clase está parametrizada, y a Caja<Libro> se le llama tipo parametrizado; a Caja a secas, tipo crudo (apartado 17).
Una clase puede tener varios parámetros de tipo, separados por comas:
/**
* Par de valores de tipos posiblemente distintos.
* Es, en esencia, lo que hace Map.Entry<K, V>.
*/
public class Par<A, B> {
private final A primero;
private final B segundo;
public Par(A primero, B segundo) {
this.primero = primero;
this.segundo = segundo;
}
public A primero() { return primero; }
public B segundo() { return segundo; }
/** Devuelve el par con los elementos intercambiados. */
public Par<B, A> invertir() {
return new Par<>(segundo, primero);
}
@Override
public String toString() {
return "(" + primero + ", " + segundo + ")";
}
}Par<String, Integer> prestamosPorEmpleado = new Par<>("Marta Ruiz", 4);
Par<Integer, String> invertido = prestamosPorEmpleado.invertir();
System.out.println(prestamosPorEmpleado); // (Marta Ruiz, 4)
System.out.println(invertido); // (4, Marta Ruiz)Observa invertir(): devuelve Par<B, A>, es decir, los parámetros de tipo se pueden combinar y reordenar libremente. El compilador lo sigue todo.
Nota sobre
record. Unrecord(04-07) también puede ser genérico:public record Par<A, B>(A primero, B segundo) { }hace lo mismo en una línea. Lo verás combinado consealeden 10-06.
- Convenciones de nombres para los parámetros de tipo
Los parámetros de tipo pueden llamarse como quieras (Caja<TipoDelContenido> compila), pero existe una convención universal de una sola letra mayúscula que conviene respetar porque hace el código inmediatamente legible:
| Letra | Significado | Ejemplo del JDK |
|---|---|---|
T |
Type: un tipo cualquiera, el caso general | Optional<T>, Class<T> |
E |
Element: elemento de una colección | List<E>, Set<E> |
K |
Key: clave de un mapa | Map<K, V> |
V |
Value: valor de un mapa | Map<K, V> |
R |
Result: tipo del resultado de una función | Function<T, R> |
S, U, V |
Tipos adicionales cuando ya usaste T |
BiFunction<T, U, R> |
N |
Number: tipo numérico | Menos común |
Recuerda que en 04-06 usaste Function<T, R>, Predicate<T>, Consumer<T> y Supplier<T> sin preguntarte de dónde salían esas letras. Ahora ya lo sabes: Function<T, R> significa literalmente "función que recibe algo de tipo T y devuelve algo de tipo R", y por eso Function<Libro, String> es la función que extrae el título.
Un parámetro de tipo no es una variable. No se puede asignar, no ocupa memoria, no existe en ejecución (apartado 14). Es una instrucción para el compilador.
- Instanciación y el operador diamante
<>
<>Antes de Java 7 había que repetir el tipo a ambos lados:
Java 7 introdujo el operador diamante <>, que le dice al compilador "infiere aquí lo mismo que hay a la izquierda":
Con var (Java 10, que verás en 10-06) el diamante vacío no sirve, porque no hay nada a la izquierda de donde inferir:
var catalogo = new ArrayList<Libro>(); // BIEN: el tipo va a la derecha
// var lista = new ArrayList<>(); // infiere ArrayList<Object>: casi nunca es lo que quieresEl diamante también funciona en clases anónimas desde Java 9:
Comparator<Libro> porTitulo = new Comparator<>() { // Java 9+
@Override
public int compare(Libro a, Libro b) {
return a.getTitulo().compareTo(b.getTitulo());
}
};
- Interfaces genéricas propias
Las interfaces se parametrizan igual que las clases. BiblioTech necesita una que marque "esto tiene un identificador de tipo T":
package com.nexussoftware.bibliotech.dominio;
/**
* Todo lo que BiblioTech puede almacenar en un repositorio
* sabe decir cual es su identificador.
*/
public interface Identificable {
String getId();
}Y una interfaz genérica de verdad, con parámetro de tipo:
package com.nexussoftware.bibliotech.servicio;
/**
* Convierte objetos de tipo T en una linea de texto y viceversa.
* Sustituye a los metodos aTexto/desdeTexto repetidos en el modulo 7.
*/
public interface Serializador<T> {
String serializar(T objeto);
T deserializar(String linea);
}Al implementarla hay dos formas de hacerlo, y la diferencia es fundamental:
// FORMA 1: fijar el tipo. La implementacion ya NO es generica.
public class SerializadorLibro implements Serializador<Libro> {
@Override
public String serializar(Libro libro) {
return libro.getIsbn() + ";" + libro.getTitulo();
}
@Override
public Libro deserializar(String linea) {
String[] partes = linea.split(";", 2);
return new Libro(partes[1], partes[0]);
}
}// FORMA 2: propagar el parametro. La implementacion sigue siendo generica.
public class SerializadorConRegistro<T> implements Serializador<T> {
private final Serializador<T> delegado;
public SerializadorConRegistro(Serializador<T> delegado) {
this.delegado = delegado;
}
@Override
public String serializar(T objeto) {
String resultado = delegado.serializar(objeto);
System.out.println("[SER] " + objeto.getClass().getSimpleName());
return resultado;
}
@Override
public T deserializar(String linea) {
return delegado.deserializar(linea);
}
}La forma 2 es un decorador (el patrón que viste aplicado a los flujos en 07-03 y que formalizarás en 12-02) y funciona con cualquier T: new SerializadorConRegistro<>(new SerializadorLibro()) es un Serializador<Libro>.
Las interfaces funcionales del módulo 4 son exactamente esto: Function<T, R> es una interfaz genérica con un único método abstracto. Nada mágico.
- Métodos genéricos e inferencia
Un método puede declarar sus propios parámetros de tipo, independientes de los de la clase. Se ponen entre los modificadores y el tipo de retorno:
public class Utilidades {
/**
* Intercambia dos posiciones de una lista de cualquier tipo.
* modificadores <T> retorno nombre
*/
public static <T> void intercambiar(List<T> lista, int i, int j) {
T temporal = lista.get(i);
lista.set(i, lista.get(j));
lista.set(j, temporal);
}
/** Devuelve el primer elemento o null si la lista esta vacia. */
public static <T> T primero(List<T> lista) {
return lista.isEmpty() ? null : lista.get(0);
}
/** Construye una lista a partir de elementos sueltos. */
@SafeVarargs
public static <T> List<T> listaDe(T... elementos) {
return new ArrayList<>(Arrays.asList(elementos));
}
/** Cuenta cuantos elementos de la lista cumplen el predicado. */
public static <T> long contar(List<T> lista, Predicate<T> criterio) {
long total = 0;
for (T elemento : lista) {
if (criterio.test(elemento)) {
total++;
}
}
return total;
}
}La inferencia hace que casi nunca tengas que escribir el tipo:
List<Libro> catalogo = Utilidades.listaDe(
new Libro("Java Efectivo", "978-0000000001"),
new Libro("Refactorización", "978-0000000003"));
Utilidades.intercambiar(catalogo, 0, 1); // T se infiere: Libro
Libro primero = Utilidades.primero(catalogo); // T se infiere: Libro
long prestados = Utilidades.contar(catalogo, Libro::estaPrestado);Cuando la inferencia no basta (o quieres ser explícito), la sintaxis para indicar el tipo es incómoda pero existe: va antes del nombre del método, tras el punto.
¿Método genérico o clase genérica? La regla práctica:
| Situación | Elección |
|---|---|
| El tipo aparece en varios miembros y define el estado del objeto | Clase genérica (Caja<T>, Repositorio<T>) |
| El tipo solo se usa dentro de un método, en su firma | Método genérico (static <T> void intercambiar) |
| Es un método estático de utilidad | Siempre método genérico (un static no puede usar el T de la clase) |
Ese último punto sorprende. Esto no compila:
public class Caja<T> {
// ERROR: non-static type variable T cannot be referenced from a static context
public static T crearVacia() { return null; }
}El T de la clase pertenece a una instancia concreta (Caja<Libro>), y un método estático no tiene instancia. La solución es declarar el método genérico con su propio parámetro:
- Límites:
<T extends Comparable<T>>
<T extends Comparable<T>>Un T sin restricciones solo garantiza que es un Object: puedes llamar a toString(), equals() y hashCode(), y nada más. Si necesitas más, hay que acotar el parámetro con extends:
/**
* Devuelve el mayor elemento de la lista.
* T DEBE saber compararse consigo mismo.
*/
public static <T extends Comparable<T>> T maximo(List<T> lista) {
if (lista.isEmpty()) {
throw new IllegalArgumentException("lista vacía");
}
T mayor = lista.get(0);
for (T elemento : lista) {
if (elemento.compareTo(mayor) > 0) { // legal SOLO gracias al limite
mayor = elemento;
}
}
return mayor;
}Sin el extends Comparable<T>, la línea elemento.compareTo(mayor) no compilaría: Object no tiene compareTo.
Dos matices importantes:
extends significa aquí "es un subtipo de", no "hereda de". Se usa la misma palabra para clases y para interfaces. <T extends Runnable> acepta cualquier clase que implemente Runnable.
El límite superior tiene un efecto secundario útil: dentro del método, T se comporta como su límite. Si escribes <T extends Material>, dentro puedes llamar a elemento.getTitulo() sin cast.
/** Concatena los titulos de cualquier coleccion de materiales. */
public static <T extends Material> String titulos(List<T> materiales) {
StringBuilder sb = new StringBuilder();
for (T m : materiales) {
sb.append(m.getTitulo()).append("; "); // getTitulo() es de Material
}
return sb.toString();
}
- Límites múltiples con
&
&Un parámetro puede exigir varias condiciones a la vez, unidas con &:
/**
* Ordena e imprime elementos que ademas de comparables
* saben serializarse.
*/
public static <T extends Comparable<T> & Serializable> void archivarOrdenado(List<T> items) {
Collections.sort(items);
for (T item : items) {
System.out.println("Archivando: " + item);
}
}Dos reglas de sintaxis que el compilador impone:
- Como máximo una clase, y el resto interfaces (Java no tiene herencia múltiple de clases).
- Si hay una clase, va la primera.
<T extends Material & Comparable<T>>es correcto;<T extends Comparable<T> & Material>no compila.
En BiblioTech esto tiene un uso muy natural: "cualquier cosa que sea un material y que además se pueda comparar".
public static <T extends Material & Comparable<T>> List<T> ordenarCatalogo(List<T> materiales) {
List<T> copia = new ArrayList<>(materiales);
Collections.sort(copia);
return copia;
}
- Invariancia: por qué
List<String> no es un List<Object>
List<String> no es un List<Object>Esta es la parte que más cuesta, y merece calma.
String es un subtipo de Object. Parece razonable esperar que List<String> sea un subtipo de List<Object>. No lo es. Los genéricos en Java son invariantes: Caja<A> y Caja<B> no tienen ninguna relación de herencia, por mucha que tengan A y B.
graph TD
O["Object"] --> S["String"]
LO["List de Object"] -.->|"SIN relacion"| LS["List de String"]
LS -.->|"SIN relacion"| LO
¿Por qué esta aparente rigidez? Porque si se permitiera, el sistema de tipos se rompería. Mira lo que pasaría si compilara:
List<String> textos = new ArrayList<>();
List<Object> objetos = textos; // supongamos que esto compilara
objetos.add(Integer.valueOf(42)); // legal: un Integer ES un Object
String s = textos.get(0); // BOOM: ClassCastException en ejecucionLa lista es la misma; solo hemos cambiado la etiqueta con la que la miramos. A través de la etiqueta List<Object> metemos un Integer, y a través de la etiqueta List<String> lo sacamos como String. Habríamos vuelto exactamente al problema del apartado 1, que era lo que los genéricos venían a resolver.
La invariancia es el precio de la garantía. El compilador prohíbe la asignación porque es la única forma de asegurar que un List<String> solo contendrá String.
- El contraste con los arrays covariantes
Lo interesante es que los arrays sí son covariantes, porque se diseñaron antes de que existieran los genéricos y había que poder escribir Arrays.sort(Object[]). Y el resultado es exactamente el desastre que los genéricos evitan:
public class CovarianciaDeArrays {
public static void main(String[] args) {
String[] textos = { "Java Efectivo", "Refactorización" };
Object[] objetos = textos; // COMPILA: los arrays son covariantes
objetos[0] = Integer.valueOf(42); // compila... y explota en ejecucion
System.out.println(textos[0]);
}
}Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
at CovarianciaDeArrays.main(CovarianciaDeArrays.java:8)La JVM comprueba en ejecución el tipo real de cada elemento que se guarda en un array, y lanza ArrayStoreException. Es decir: los arrays pagan una comprobación en cada escritura y aun así el error llega tarde.
| Aspecto | Arrays | Genéricos |
|---|---|---|
| Varianza | Covariantes (String[] es un Object[]) |
Invariantes (List<String> no es un List<Object>) |
| Comprobación de tipos | En ejecución (ArrayStoreException) |
En compilación |
| Información en ejecución | Reificados: el array sabe su tipo | Borrados: no la conservan |
| Elementos permitidos | Solo tipos reificables | Cualquier tipo de referencia |
Esta tabla explica también por qué arrays y genéricos no se mezclan bien (apartado 15): son dos mecanismos con filosofías opuestas. La recomendación clásica es preferir List<T> a T[].
- Comodines:
<?>, <? extends X> y <? super X>
<?>, <? extends X> y <? super X>La invariancia es correcta pero incómoda. Si escribes este método:
public static void imprimirTitulos(List<Material> materiales) {
for (Material m : materiales) {
System.out.println(m.getTitulo());
}
}...no puedes pasarle un List<Libro>, aunque Libro extienda Material. Y eso es absurdo: el método solo lee.
Los comodines (?) recuperan la flexibilidad sin perder la seguridad. Hay tres formas.
12.1. El comodín sin límite: <?>
List<?> significa "una lista de algún tipo desconocido pero concreto".
/** Sirve para CUALQUIER lista: solo usa metodos que no dependen de T. */
public static void informarTamano(List<?> lista) {
System.out.println("La lista tiene " + lista.size() + " elementos");
for (Object o : lista) { // lo unico seguro es leer como Object
System.out.println(" - " + o);
}
}informarTamano(List.of("a", "b")); // List<String>
informarTamano(List.of(new Libro("Java Efectivo", "978-0000000001"))); // List<Libro>Lo que NO se puede hacer con un List<?>: añadir nada (salvo null).
public static void intentarAnadir(List<?> lista) {
// lista.add("hola"); // NO COMPILA
// lista.add(new Object()); // NO COMPILA
lista.add(null); // unico permitido
}Y es lógico: si no sabes de qué tipo es la lista, no puedes saber qué es seguro meter. Podría ser un List<Libro> y estarías metiendo un String.
List<?> no es lo mismo que List<Object>. List<Object> es una lista que acepta cualquier cosa (y a la que sí puedes añadir); List<?> es una lista de un tipo específico que tú desconoces.
12.2. Comodín con límite superior: <? extends X>
List<? extends Material> significa "una lista de Material o de cualquier subtipo suyo". Ahora sí:
public static void imprimirTitulos(List<? extends Material> materiales) {
for (Material m : materiales) { // leer como Material: SEGURO
System.out.println(m.getTitulo());
}
// materiales.add(new Libro(...)); // NO COMPILA
}List<Libro> libros = new ArrayList<>();
List<Revista> revistas = new ArrayList<>();
List<Material> mezcla = new ArrayList<>();
imprimirTitulos(libros); // OK
imprimirTitulos(revistas); // OK
imprimirTitulos(mezcla); // OKPuedes leer, no puedes escribir. ¿Por qué no puedes añadir un Libro? Porque List<? extends Material> podría ser en realidad un List<Revista>, y meterle un Libro lo corrompería. El compilador no sabe cuál de los subtipos es, así que prohíbe todas las escrituras.
12.3. Comodín con límite inferior: <? super X>
List<? super Libro> significa "una lista de Libro o de cualquier supertipo suyo": podría ser List<Libro>, List<Material> o List<Object>.
public static void anadirLibrosDePrueba(List<? super Libro> destino) {
destino.add(new Libro("Java Efectivo", "978-0000000001")); // SEGURO
destino.add(new Libro("Refactorización", "978-0000000003"));
// Libro l = destino.get(0); // NO COMPILA
Object o = destino.get(0); // lo unico seguro es Object
}List<Libro> soloLibros = new ArrayList<>();
List<Material> materiales = new ArrayList<>();
List<Object> cualquiera = new ArrayList<>();
anadirLibrosDePrueba(soloLibros); // OK
anadirLibrosDePrueba(materiales); // OK
anadirLibrosDePrueba(cualquiera); // OKPuedes escribir, casi no puedes leer. Añadir un Libro siempre es seguro: sea la lista de Libro, de Material o de Object, un Libro cabe en todas. Pero al leer, lo único que el compilador puede garantizar es Object, porque la lista podría ser un List<Object> lleno de String.
- PECS: Producer Extends, Consumer Super
La regla que resume los dos apartados anteriores se conoce como PECS, acuñada por Joshua Bloch:
Producer Extends, Consumer Super. Si el parámetro produce valores que tú lees, usa
? extends. Si el parámetro consume valores que tú le das, usa? super. Si hace las dos cosas, no uses comodín: usa el tipo exacto.
La forma de decidir es preguntarse: ¿de dónde salen los datos y hacia dónde van?
graph LR
A["Coleccion origen<br/>PRODUCE datos<br/>? extends T"] -->|"lees de aqui"| M["Tu metodo"]
M -->|"escribes aqui"| B["Coleccion destino<br/>CONSUME datos<br/>? super T"]
El ejemplo canónico es un método de copia:
/**
* Copia elementos de origen a destino.
* origen PRODUCE elementos -> ? extends T
* destino CONSUME elementos -> ? super T
*/
public static <T> void copiar(List<? extends T> origen, List<? super T> destino) {
for (T elemento : origen) {
destino.add(elemento);
}
}Con esa firma, esto funciona:
List<Libro> libros = List.of(
new Libro("Java Efectivo", "978-0000000001"),
new Libro("Patrones de Diseño", "978-0000000002"));
List<Material> almacen = new ArrayList<>();
List<Object> registro = new ArrayList<>();
Utilidades.<Material>copiar(libros, almacen); // Libro produce, Material consume
Utilidades.<Libro>copiar(libros, registro); // Libro produce, Object consumeEjemplos que fallan al elegir mal
Error 1: usar extends donde hace falta super.
// MAL: destino declarado como productor
public static <T> void copiarMal(List<? extends T> origen, List<? extends T> destino) {
for (T elemento : origen) {
destino.add(elemento); // NO COMPILA
}
}error: no suitable method found for add(T)
destino.add(elemento);
^
method Collection.add(CAP#1) is not applicable
(argument mismatch; T cannot be converted to CAP#1)Ese CAP#1 es el tipo capturado: el compilador dice "no sé qué tipo concreto es el comodín, así que no puedo garantizar que tu T encaje".
Error 2: usar super donde hace falta extends.
// MAL: origen declarado como consumidor
public static <T> void copiarMal2(List<? super T> origen, List<? super T> destino) {
for (T elemento : origen) { // NO COMPILA
destino.add(elemento);
}
}Leer de un ? super T da Object, no T.
PECS en el JDK
Una vez entendido, empiezas a ver PECS por todas partes. Estas firmas del JDK dejan de parecer ruido:
// Collections: origen produce, destino consume
public static <T> void copy(List<? super T> dest, List<? extends T> src)
// El comparador CONSUME elementos para compararlos
public static <T> void sort(List<T> list, Comparator<? super T> c)
// Stream.map: la funcion CONSUME T y PRODUCE R
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
// Optional.ifPresent: el consumidor CONSUME T
public void ifPresent(Consumer<? super T> action)El caso de Comparator<? super T> es el más útil de entender. Significa: "para ordenar una lista de Libro, me vale un Comparator<Libro>, pero también un Comparator<Material>". Y es evidente: si sabes comparar materiales cualesquiera, sabes comparar libros.
Comparator<Material> porTitulo = Comparator.comparing(Material::getTitulo);
List<Libro> libros = new ArrayList<>(...);
libros.sort(porTitulo); // funciona gracias al ? super TSin ese ? super, esta línea no compilaría y habría que duplicar comparadores por cada subtipo.
Consejo práctico: aplica PECS a las APIs que escribes para otros (métodos públicos, bibliotecas). Dentro de tu código de aplicación, con tipos concretos, no lo necesitas y añade ruido.
Excepción a la regla: nunca uses un comodín en el tipo de retorno de un método público. Obliga a quien lo llama a lidiar con comodines sin ganar nada.
- Borrado de tipos: qué hace realmente el compilador
Aquí está la clave para entender todas las limitaciones que vienen después.
Cuando Java 5 introdujo los genéricos, había millones de líneas de código escritas contra List sin parámetros, y billones de bytes de clases ya compiladas. La decisión de diseño fue: los genéricos existen solo en tiempo de compilación. El compilador los usa para comprobar tu código, y después los borra. A esto se le llama borrado de tipos (type erasure).
En concreto, el compilador:
- Sustituye cada parámetro de tipo por su límite (o por
Objectsi no tiene límite). - Inserta los casts necesarios donde hacen falta.
- Genera métodos puente cuando la herencia lo requiere (apartado 16).
Lo que tú escribes:
public class Caja<T> {
private T contenido;
public T obtener() { return contenido; }
public void guardar(T c) { this.contenido = c; }
}Lo que queda en el .class (visto conceptualmente):
public class Caja {
private Object contenido;
public Object obtener() { return contenido; }
public void guardar(Object c) { this.contenido = c; }
}Y en el punto de uso, tú escribes:
y el compilador genera:
Los casts no desaparecieron: los escribe el compilador por ti, y solo después de haber comprobado que son correctos. Esa es la diferencia con el código de 1999.
Con límite, el borrado usa el límite en lugar de Object:
public static <T extends Material> String titulos(List<T> lista)
// se borra a
public static String titulos(List lista) // y dentro, T se trata como MaterialPuedes verlo tú mismo con javap:
public class Caja<T> {
private T contenido;
public Caja(T);
public T obtener();
public void guardar(T);
}javap muestra la firma genérica porque esa información sí se guarda en los metadatos de la clase (el atributo Signature), para que el compilador pueda comprobar el código que use esta clase. Pero la JVM no la usa en ejecución: el bytecode real opera sobre Object. Con javap -c verías las instrucciones sobre Object y los checkcast.
Matiz importante para 10-03: el borrado no elimina toda la información genérica del fichero
.class. Las firmas de clases, campos y métodos sí conservan sus tipos genéricos declarados; lo que se pierde es el tipo de una instancia concreta en ejecución. Un objetoArrayListno sabe si nació comoArrayList<Libro>. La reflexión (10-03) puede leer lo primero congetGenericType(), no lo segundo.
- Las consecuencias prácticas del borrado
Estas cinco limitaciones son directas del borrado, y conviene reconocerlas al instante cuando el compilador se queja.
15.1. No se puede hacer new T()
public class Repositorio<T> {
public T crearVacio() {
// return new T(); // ERROR: type parameter T cannot be instantiated directly
return null;
}
}En ejecución T no existe: no hay ninguna clase de la que construir un objeto. La solución es pasar un Supplier<T> (el del módulo 4) o un token de tipo Class<T> (apartado 19):
public class Repositorio<T> {
private final Supplier<T> fabrica;
public Repositorio(Supplier<T> fabrica) {
this.fabrica = fabrica;
}
public T crearVacio() {
return fabrica.get(); // la fabrica SI sabe construir un T
}
}
// uso
Repositorio<Libro> repo = new Repositorio<>(Libro::new);15.2. No se puede hacer instanceof List<String>
if (objeto instanceof List<String>) { } // ERROR: illegal generic type for instanceof
if (objeto instanceof List<?>) { } // OK: solo el tipo crudo es comprobableEn ejecución no hay forma de distinguir un List<String> de un List<Libro>: los dos son un ArrayList a secas. Solo puedes comprobar List<?> (o List, con aviso).
15.3. No se pueden sobrecargar métodos que solo difieren en el genérico
public class Informes {
// ERROR: name clash: ambos se borran a procesar(List)
public void procesar(List<Libro> libros) { }
public void procesar(List<Revista> revistas) { }
}Tras el borrado, ambas firmas son procesar(List). La solución es dar nombres distintos (procesarLibros, procesarRevistas), que además se lee mejor.
15.4. No se pueden crear arrays genéricos
Los arrays son reificados (apartado 11): necesitan saber su tipo en ejecución para lanzar ArrayStoreException. Un T[] no puede saberlo. Las dos salidas:
// SOLUCION 1 (recomendada): usar una List
private final List<T> items = new ArrayList<>();
// SOLUCION 2: array de Object con cast y aviso suprimido (lo que hace ArrayList por dentro)
@SuppressWarnings("unchecked")
private T[] items = (T[]) new Object[10];La segunda es la que usa el propio ArrayList del JDK (transient Object[] elementData), y es segura solo porque el array es privado y nunca se devuelve al exterior como T[]. Si lo devolvieras, quien lo recibiera obtendría un ClassCastException al asignarlo.
15.5. Los tipos parametrizados comparten la misma clase
List<Libro> libros = new ArrayList<>();
List<String> textos = new ArrayList<>();
System.out.println(libros.getClass()); // class java.util.ArrayList
System.out.println(libros.getClass() == textos.getClass()); // trueAmbos son el mismo Class. Lo mismo ocurre con los miembros static: una clase genérica tiene un único juego de miembros estáticos, compartido por todas las parametrizaciones. Caja<Libro> y Caja<String> comparten el mismo contador estático.
- Métodos puente (bridge methods)
Una nota técnica que explica un detalle raro que puedes encontrarte en trazas de pila y en la reflexión.
Cuando una clase implementa una interfaz genérica con un tipo concreto, el borrado crea un desajuste de firmas. Considera:
public class ComparadorLibros implements Comparator<Libro> {
@Override
public int compare(Libro a, Libro b) {
return a.getTitulo().compareTo(b.getTitulo());
}
}Tras el borrado, Comparator declara int compare(Object, Object), pero tu clase define int compare(Libro, Libro). Son firmas distintas: el polimorfismo no funcionaría. El compilador resuelve esto generando un método adicional invisible para ti:
// generado por el compilador, marcado como sintetico y puente
public int compare(Object a, Object b) {
return compare((Libro) a, (Libro) b); // delega, con el cast
}Consecuencias prácticas:
- Si en 10-03 llamas a
getDeclaredMethods()sobreComparadorLibros, verás dos métodoscompare. Se filtran conmetodo.isBridge()ometodo.isSynthetic(). - Si alguien pasa un objeto del tipo equivocado por una vía sin comprobar (un tipo crudo), el
ClassCastExceptionsalta dentro del método puente, en una línea que tú no escribiste. Vercompare(Unknown Source)en una traza suele significar esto.
- Tipos crudos (raw types) y por qué existen
Un tipo crudo es un tipo genérico usado sin sus parámetros: List en lugar de List<Libro>.
Compila, con un aviso:
Note: Ejemplo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.Existen únicamente por compatibilidad hacia atrás. Cuando Java 5 introdujo los genéricos, todo el código existente usaba List sin parámetros. Prohibirlo habría roto el ecosistema entero de un día para otro. Así que se permitió mezclar código antiguo y nuevo.
Lo peligroso es que un tipo crudo desactiva la comprobación genérica de todo lo que toca, incluso hacia arriba:
public static void envenenar(List crudo) { // tipo crudo
crudo.add("no soy un Libro");
}
public static void main(String[] args) {
List<Libro> catalogo = new ArrayList<>();
envenenar(catalogo); // compila, solo con aviso
for (Libro l : catalogo) { // ClassCastException aqui
System.out.println(l.getTitulo());
}
}| Escritura | ¿Compila? | Comprobación | Cuándo usarla |
|---|---|---|---|
List<Libro> |
Sí | Completa | Siempre |
List<?> |
Sí | Segura: solo lectura como Object |
Cuando el tipo da igual |
List<Object> |
Sí | Completa (lista de cualquier cosa) | Rara vez |
List (crudo) |
Sí, con aviso | Ninguna | Nunca en código nuevo |
Regla: compila con -Xlint:unchecked y trata los avisos como errores. Si en tu código aparece un tipo crudo, es un bug esperando su turno.
@SuppressWarnings("unchecked") y cuándo es legítimo
@SuppressWarnings("unchecked") y cuándo es legítimo@SuppressWarnings (que verás formalmente en 10-02) silencia un aviso del compilador. Es una herramienta necesaria y, mal usada, un desastre.
Las tres reglas de oro:
- Ámbito mínimo. Nunca en la clase entera; idealmente sobre una variable local.
- Comentario obligatorio explicando por qué la operación es segura pese al aviso.
- Solo cuando puedes demostrarlo, no cuando quieres que el compilador se calle.
Mal:
@SuppressWarnings("unchecked") // sobre toda la clase: silencia bugs futuros
public class Repositorio<T> { ... }Bien:
public <T> T[] copiarEn(T[] destino) {
if (destino.length < tamano) {
// SEGURO: Arrays.copyOf con el tipo real del array de destino
// devuelve un array del mismo tipo que 'destino', que es T[].
@SuppressWarnings("unchecked")
T[] nuevo = (T[]) Arrays.copyOf(items, tamano, destino.getClass());
return nuevo;
}
System.arraycopy(items, 0, destino, 0, tamano);
return destino;
}Fíjate en el truco: se declara una variable local solo para poder poner ahí la anotación con el ámbito más pequeño posible. Es idiomático y aparece en el propio JDK.
- El token de tipo:
Class<T>
Class<T>Si T no existe en ejecución, ¿cómo se las arreglan Jackson, Hibernate o Spring para construir objetos del tipo correcto? Con un token de tipo: se pasa el Class<T> como argumento normal, y ese objeto sí existe en ejecución.
package com.nexussoftware.bibliotech.servicio;
import java.lang.reflect.InvocationTargetException;
/**
* Fabrica generica que SI puede crear instancias de T,
* porque recibe el Class<T> como token de tipo.
*/
public class FabricaEntidades<T> {
private final Class<T> tipo;
public FabricaEntidades(Class<T> tipo) {
this.tipo = tipo;
}
/** Crea una instancia usando el constructor sin argumentos. */
public T crear() {
try {
return tipo.getDeclaredConstructor().newInstance();
} catch (NoSuchMethodException | InstantiationException
| IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(
"No se pudo instanciar " + tipo.getSimpleName(), e);
}
}
/**
* Convierte con seguridad: cast comprobado en EJECUCION.
* Si el objeto no es del tipo esperado lanza ClassCastException
* aqui mismo, no tres capas mas abajo.
*/
public T convertir(Object candidato) {
return tipo.cast(candidato);
}
public String nombreTipo() {
return tipo.getSimpleName();
}
}FabricaEntidades<Libro> fabrica = new FabricaEntidades<>(Libro.class);
Libro nuevo = fabrica.crear();
System.out.println(fabrica.nombreTipo()); // LibroClass<T> es genérico él mismo: Libro.class tiene tipo Class<Libro>, y por eso tipo.cast(x) devuelve un T sin necesidad de cast manual ni de @SuppressWarnings. Es la forma limpia de recuperar en ejecución lo que el borrado se llevó.
Este patrón se llama contenedor heterogéneo con seguridad de tipos, y así se implementan cosas como los atributos de una petición web:
public class ContextoTipado {
private final Map<Class<?>, Object> valores = new HashMap<>();
public <T> void poner(Class<T> tipo, T valor) {
valores.put(Objects.requireNonNull(tipo), valor);
}
public <T> T obtener(Class<T> tipo) {
return tipo.cast(valores.get(tipo)); // cast seguro
}
}ContextoTipado ctx = new ContextoTipado();
ctx.poner(Empleado.class, new Empleado("Marta Ruiz"));
ctx.poner(String.class, "sesión-4711");
Empleado quien = ctx.obtener(Empleado.class); // sin cast, sin avisoLimitación de los tokens de tipo: no pueden representar tipos parametrizados. No existe List<Libro>.class. Las bibliotecas resuelven esto con la técnica del super type token (una clase anónima que sí conserva el tipo genérico en su firma, recuperable por reflexión); es lo que hay detrás del TypeReference de Jackson, que verás en 11-07. La mecánica de por qué funciona la explicarás en 10-03.
- Genéricos y excepciones
Dos limitaciones concretas que conviene conocer.
No se puede capturar un tipo genérico:
public <E extends Exception> void ejecutar(Runnable tarea, Class<E> tipo) {
try {
tarea.run();
} catch (E e) { // ERROR: cannot use the type variable E in a catch clause
// ...
}
}En ejecución no hay E, y el mecanismo de captura de la JVM compara tipos reales. La alternativa es capturar el supertipo y comprobar con el token:
public <E extends Exception> void ejecutar(Runnable tarea, Class<E> tipo) throws E {
try {
tarea.run();
} catch (Exception e) {
if (tipo.isInstance(e)) {
throw tipo.cast(e); // relanzamos ya tipado
}
throw new BiblioTechException("Fallo inesperado en la tarea", e);
}
}Una clase genérica no puede extender Throwable:
// ERROR: a generic class may not extend java.lang.Throwable
public class ErrorTipado<T> extends BiblioTechException { }Por la misma razón: el catch no podría distinguir ErrorTipado<Libro> de ErrorTipado<Prestamo>.
Sí se puede usar un tipo genérico en la cláusula throws, y es un truco útil:
/** Ejecuta la tarea propagando su excepcion tipada. */
public static <T, E extends Exception> T intentar(SupplierConError<T, E> tarea) throws E {
return tarea.obtener();
}
@FunctionalInterface
public interface SupplierConError<T, E extends Exception> {
T obtener() throws E;
}
- BiblioTech:
Repositorio<T extends Identificable>
Repositorio<T extends Identificable>Vamos a saldar la deuda. CatalogoConcurrente y RegistroPrestamosSeguro hacen lo mismo con tipos distintos: guardar por identificador, buscar, listar, eliminar. Un único repositorio genérico los sustituye.
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.Identificable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
/**
* Repositorio generico y seguro entre hilos para cualquier entidad
* que sepa dar su identificador.
*
* El limite <T extends Identificable> permite llamar a getId()
* dentro de la clase sin ningun cast.
*/
public class Repositorio<T extends Identificable> {
private final Map<String, T> porId = new ConcurrentHashMap<>();
private final String nombre;
public Repositorio(String nombre) {
this.nombre = Objects.requireNonNull(nombre, "nombre");
}
/** Guarda o reemplaza. Devuelve el valor anterior, si lo habia. */
public Optional<T> guardar(T entidad) {
Objects.requireNonNull(entidad, "entidad");
return Optional.ofNullable(porId.put(entidad.getId(), entidad));
}
/** Guarda solo si no existia. true si se insertó. */
public boolean guardarSiAusente(T entidad) {
return porId.putIfAbsent(entidad.getId(), entidad) == null;
}
public Optional<T> buscarPorId(String id) {
return Optional.ofNullable(porId.get(id));
}
public Optional<T> eliminar(String id) {
return Optional.ofNullable(porId.remove(id));
}
public boolean contiene(String id) {
return porId.containsKey(id);
}
public int tamano() {
return porId.size();
}
/** Vista inmodificable: nadie puede alterar el repositorio por la puerta de atras. */
public List<T> listarTodos() {
return List.copyOf(porId.values());
}
/**
* Filtra con un predicado.
* Predicate<? super T> por PECS: el predicado CONSUME elementos,
* asi que un Predicate<Identificable> tambien vale.
*/
public List<T> buscar(Predicate<? super T> criterio) {
List<T> resultado = new ArrayList<>();
for (T entidad : porId.values()) {
if (criterio.test(entidad)) {
resultado.add(entidad);
}
}
return resultado;
}
/**
* Ordena una copia.
* Comparator<? super T> por PECS: un Comparator<Identificable> vale
* para ordenar cualquier subtipo.
*/
public List<T> listarOrdenado(Comparator<? super T> orden) {
List<T> copia = new ArrayList<>(porId.values());
copia.sort(orden);
return copia;
}
/**
* Vuelca todas las entidades en el destino.
* ? super T por PECS: el destino CONSUME.
*/
public void volcarEn(Collection<? super T> destino) {
destino.addAll(porId.values());
}
@Override
public String toString() {
return "Repositorio[" + nombre + ", " + porId.size() + " entidades]";
}
}Ahora las entidades implementan Identificable:
public abstract class Material implements Identificable {
private final String isbn;
private final String titulo;
// ... resto del modulo 3
@Override
public String getId() {
return isbn;
}
}public class Prestamo implements Identificable {
private final String referencia; // p. ej. "PR-2026-0041"
@Override
public String getId() {
return referencia;
}
}Y el uso, con dos tipos distintos y una sola clase de repositorio:
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.dominio.*;
import com.nexussoftware.bibliotech.servicio.Repositorio;
import java.util.Comparator;
import java.util.List;
public class BiblioTechApp {
public static void main(String[] args) {
Repositorio<Material> catalogo = new Repositorio<>("catálogo");
Repositorio<Prestamo> prestamos = new Repositorio<>("préstamos");
catalogo.guardar(new Libro("Java Efectivo", "978-0000000001"));
catalogo.guardar(new Libro("Patrones de Diseño", "978-0000000002"));
catalogo.guardar(new Libro("Refactorización", "978-0000000003"));
prestamos.guardar(new Prestamo("PR-2026-0041", "978-0000000001", "Marta Ruiz"));
prestamos.guardar(new Prestamo("PR-2026-0042", "978-0000000003", "Diego Alonso"));
// Ni un solo cast en todo lo que sigue.
catalogo.buscarPorId("978-0000000001")
.ifPresent(m -> System.out.println("Encontrado: " + m.getTitulo()));
List<Material> ordenado =
catalogo.listarOrdenado(Comparator.comparing(Material::getTitulo));
ordenado.forEach(m -> System.out.println(" " + m.getTitulo()));
List<Prestamo> deMarta = prestamos.buscar(p -> p.getEmpleado().equals("Marta Ruiz"));
System.out.println("Préstamos de Marta: " + deMarta.size());
System.out.println(catalogo);
System.out.println(prestamos);
}
}Encontrado: Java Efectivo
Java Efectivo
Patrones de Diseño
Refactorización
Préstamos de Marta: 1
Repositorio[catálogo, 3 entidades]
Repositorio[préstamos, 2 entidades]Qué se ha ganado, en concreto:
| Antes | Ahora |
|---|---|
CatalogoConcurrente (≈120 líneas) |
Repositorio<Material> (1 línea de declaración) |
RegistroPrestamosSeguro (≈130 líneas) |
Repositorio<Prestamo> (1 línea) |
| Dos implementaciones que corregir en paralelo cuando hay un bug | Una sola |
Un Repositorio de Object obligaría a castear en cada buscarPorId |
Sin casts |
Y añadir un repositorio de SalaReuniones o de Reserva es ahora una línea, no una clase.
- BiblioTech:
Resultado<T> genérico
Resultado<T> genéricoEn 06-07 creaste Resultado como "objeto resultado": una alternativa a lanzar excepciones para errores esperables. Pero era un Resultado que solo sabía llevar un String, o que llevaba Object y obligaba a castear. Genéricos al rescate.
package com.nexussoftware.bibliotech.servicio;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Function;
/**
* Resultado de una operacion que puede fallar de forma esperable.
* O contiene un valor de tipo T, o contiene un mensaje de error.
* Nunca las dos cosas, nunca ninguna.
*
* Constructor privado + fabricas estaticas: es imposible construir
* un Resultado incoherente.
*/
public final class Resultado<T> {
private final T valor;
private final String error;
private Resultado(T valor, String error) {
this.valor = valor;
this.error = error;
}
/**
* Metodo generico estatico: declara su propio <U>, porque un
* metodo static NO puede usar el T de la clase (apartado 7).
*/
public static <U> Resultado<U> exito(U valor) {
return new Resultado<>(Objects.requireNonNull(valor, "valor"), null);
}
public static <U> Resultado<U> fallo(String error) {
return new Resultado<>(null, Objects.requireNonNull(error, "error"));
}
public boolean esExito() { return error == null; }
public boolean esFallo() { return error != null; }
public T valor() {
if (esFallo()) {
throw new NoSuchElementException("El resultado es un fallo: " + error);
}
return valor;
}
public String error() {
if (esExito()) {
throw new NoSuchElementException("El resultado es un éxito");
}
return error;
}
public T oPorDefecto(T alternativa) {
return esExito() ? valor : alternativa;
}
/**
* Transforma el valor si hay exito; propaga el error si no.
* Function<? super T, ? extends R> por PECS:
* la funcion CONSUME T -> ? super T
* la funcion PRODUCE R -> ? extends R
*/
public <R> Resultado<R> map(Function<? super T, ? extends R> transformacion) {
if (esFallo()) {
return Resultado.fallo(error); // el error viaja intacto
}
return Resultado.exito(transformacion.apply(valor));
}
/** Encadena operaciones que a su vez pueden fallar, sin anidar Resultado<Resultado<R>>. */
public <R> Resultado<R> flatMap(Function<? super T, Resultado<R>> siguiente) {
return esFallo() ? Resultado.fallo(error) : siguiente.apply(valor);
}
@Override
public String toString() {
return esExito() ? "Éxito[" + valor + "]" : "Fallo[" + error + "]";
}
}Uso en el servicio de préstamos:
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.*;
public class GestorPrestamos {
private final Repositorio<Material> catalogo;
private final Repositorio<Prestamo> prestamos;
public GestorPrestamos(Repositorio<Material> catalogo, Repositorio<Prestamo> prestamos) {
this.catalogo = catalogo;
this.prestamos = prestamos;
}
/** El tipo de retorno DICE que esto puede fallar, y con que valor sale bien. */
public Resultado<Prestamo> prestar(String isbn, String empleado) {
Material material = catalogo.buscarPorId(isbn).orElse(null);
if (material == null) {
return Resultado.fallo("No existe ningún material con ISBN " + isbn);
}
if (material.estaPrestado()) {
return Resultado.fallo("El material '" + material.getTitulo() + "' ya está prestado");
}
material.marcarPrestado(empleado);
Prestamo prestamo = new Prestamo(siguienteReferencia(), isbn, empleado);
prestamos.guardar(prestamo);
return Resultado.exito(prestamo);
}
private String siguienteReferencia() {
return String.format("PR-2026-%04d", prestamos.tamano() + 1);
}
}Y el punto clave, en la presentación:
Resultado<Prestamo> resultado = gestor.prestar("978-0000000001", "Nuria Vidal");
if (resultado.esExito()) {
System.out.println("Préstamo registrado: " + resultado.valor().getId());
} else {
System.out.println("No se pudo prestar: " + resultado.error());
}
// Encadenamiento sin comprobaciones intermedias
String recibo = gestor.prestar("978-0000000002", "Diego Alonso")
.map(Prestamo::getId)
.map(id -> "Recibo nº " + id)
.oPorDefecto("Sin recibo: el préstamo no se pudo registrar");
System.out.println(recibo);El tipo de retorno documenta el contrato. Resultado<Prestamo> dice, sin leer una línea de la implementación: esto puede fallar de forma esperable, y cuando va bien te da un Prestamo. Compáralo con Prestamo prestar(...) throws BiblioTechException (que obliga a try/catch para un caso normal) o con Prestamo prestar(...) devolviendo null (que no dice nada y produce NullPointerException).
En 10-04 verás Optional<T>, que es el caso particular de este patrón cuando el error no lleva información: solo hay valor o no lo hay.
Errores Comunes y Consejos
1. Confundir List<Object> con List<?>. El primero acepta cualquier cosa y permite añadir; el segundo es de un tipo desconocido y solo permite leer como Object y añadir null. Si tu método solo lee, usa List<?> o List<? extends X>, nunca List<Object>, porque este último no acepta un List<String>.
2. Usar tipos crudos "para que compile". Cada tipo crudo apaga la comprobación genérica en una zona del código. Compila siempre con -Xlint:unchecked -Xlint:rawtypes y arregla los avisos.
3. Esperar que los genéricos existan en ejecución. Ni new T(), ni instanceof List<String>, ni new T[10], ni sobrecargas que solo difieren en el parámetro. Cuando necesites el tipo real, pasa un Class<T> o un Supplier<T>.
4. Creer que List<Libro> es un List<Material>. No lo es. Si tu método solo lee de la lista, declara List<? extends Material> y funcionará con las dos.
5. Poner comodines en el tipo de retorno. public List<? extends Material> listar() obliga a quien lo usa a arrastrar comodines por todo su código. Devuelve List<Material>.
6. Silenciar avisos con @SuppressWarnings en la clase entera. Ámbito mínimo, siempre con un comentario que justifique por qué la operación es segura. Si no sabes justificarla, el aviso es un bug.
7. Olvidar PECS al escribir APIs públicas. Un Comparator<T> en lugar de Comparator<? super T> obliga a duplicar comparadores. Un Consumer<T> en lugar de Consumer<? super T> impide reutilizar un consumidor genérico.
8. Mezclar arrays y genéricos. List<Libro>[] no es creable directamente y la covarianza de los arrays lucha contra la invariancia de los genéricos. Usa List<List<Libro>>.
Consejo 1: empieza concreto y generaliza después. Escribe Repositorio de Material primero, haz que funcione, y solo cuando necesites el segundo tipo conviértelo en Repositorio<T>. Generalizar antes de tener dos casos reales suele producir abstracciones equivocadas.
Consejo 2: pon el límite más flojo que te sirva. <T extends Identificable> en lugar de <T extends Material> si solo llamas a getId(). Cada restricción de más cierra puertas a los usuarios de tu API.
Consejo 3: lee las firmas del JDK. <R> Stream<R> map(Function<? super T, ? extends R> mapper) ya no es ruido: es "transforma cada T en un R con una función que acepta T o cualquier supertipo y produce R o cualquier subtipo". Entenderlas es el 80 % de dominar genéricos.
Consejo 4: cuando el compilador diga CAP#1, piensa en comodines. Ese identificador es el tipo capturado de un ?. Casi siempre significa que has elegido mal entre extends y super.
Ejercicios
Ejercicio 1: Cache<K, V> genérica con límite de tamaño
Escribe una clase Cache<K, V> para BiblioTech que:
- Guarde pares clave-valor con capacidad máxima fija.
- Al superarse la capacidad, elimine la entrada menos recientemente usada (LRU).
- Ofrezca
Optional<V> obtener(K clave),void poner(K clave, V valor),int tamano()yList<K> clavesPorAntiguedad(). - Tenga un método
V obtenerOCalcular(K clave, Function<? super K, ? extends V> calculo)que devuelva el valor cacheado o lo calcule y lo guarde. - Lleve estadísticas de aciertos y fallos.
Pista: LinkedHashMap con el constructor de tres argumentos y removeEldestEntry sobrescrito hace el LRU por ti.
Ejercicio 2: PECS en un agregador de estadísticas
Escribe una clase de utilidad Agregador con estos métodos estáticos, eligiendo correctamente los comodines:
sumar(Collection<? ...> numeros)→ devuelvedoublecon la suma de una colección de cualquier tipo numérico.copiarFiltrando(Collection<? ...> origen, Collection<? ...> destino, Predicate<? ...> criterio)→ copia del origen al destino los elementos que cumplan el criterio.maximoSegun(Collection<? ...> elementos, Comparator<? ...> orden)→ devuelve el máximo, oOptional.empty()si está vacía.
Después escribe un main que demuestre, con las clases de BiblioTech, que se puede pasar un List<Libro> como origen y un List<Material> como destino, y un Comparator<Material> para ordenar libros.
Ejercicio 3: ValidadorEncadenable<T> con Resultado<T>
Construye un validador genérico para BiblioTech:
ValidadorEncadenable<T>acumula reglas: cada regla es unPredicate<? super T>con su mensaje de error.- Método
conRegla(Predicate<? super T> regla, String mensaje)que devuelve el propio validador (encadenable). - Método
Resultado<T> validar(T objeto)que devuelve éxito con el objeto si pasa todas las reglas, o fallo con todos los mensajes de las reglas incumplidas, separados por;. - Un método estático
de(Class<T> tipo)que cree el validador (usa el token de tipo para el mensaje).
Úsalo para validar un Libro: ISBN no nulo y de 17 caracteres, título no vacío, título de menos de 200 caracteres.
Soluciones
Solución 1
package com.nexussoftware.bibliotech.servicio;
import java.util.*;
import java.util.function.Function;
/**
* Cache LRU generica con capacidad maxima.
*
* K y V son independientes: se puede tener una Cache<String, Ficha>
* o una Cache<Integer, List<Prestamo>> con la misma clase.
*/
public class Cache<K, V> {
private final int capacidad;
private final LinkedHashMap<K, V> mapa;
private long aciertos = 0;
private long fallos = 0;
public Cache(int capacidad) {
if (capacidad <= 0) {
throw new IllegalArgumentException("La capacidad debe ser positiva: " + capacidad);
}
this.capacidad = capacidad;
// Tercer argumento true = orden de ACCESO (no de insercion): eso es el LRU.
// 0.75f es el factor de carga habitual (retoma 05-05).
this.mapa = new LinkedHashMap<>(capacidad, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> masAntigua) {
// Cuando devuelve true, LinkedHashMap elimina la entrada mas antigua.
return size() > Cache.this.capacidad;
}
};
}
/** Devuelve Optional en vez de null: la ausencia es explicita (ver 10-04). */
public Optional<V> obtener(K clave) {
V valor = mapa.get(clave); // get() reordena por ser orden de acceso
if (valor != null) {
aciertos++;
} else {
fallos++;
}
return Optional.ofNullable(valor);
}
public void poner(K clave, V valor) {
Objects.requireNonNull(clave, "clave");
Objects.requireNonNull(valor, "valor");
mapa.put(clave, valor);
}
/**
* PECS aplicado: la funcion CONSUME la clave (? super K)
* y PRODUCE el valor (? extends V).
* Asi acepta una Function<Object, Ficha> para una Cache<String, Ficha>.
*/
public V obtenerOCalcular(K clave, Function<? super K, ? extends V> calculo) {
V existente = mapa.get(clave);
if (existente != null) {
aciertos++;
return existente;
}
fallos++;
V calculado = calculo.apply(clave);
mapa.put(clave, calculado);
return calculado;
}
public int tamano() {
return mapa.size();
}
/** De la menos usada recientemente a la mas reciente. */
public List<K> clavesPorAntiguedad() {
return new ArrayList<>(mapa.keySet());
}
public double tasaAciertos() {
long total = aciertos + fallos;
return total == 0 ? 0.0 : (double) aciertos / total;
}
@Override
public String toString() {
return String.format("Cache[%d/%d entradas, %d aciertos, %d fallos, %.0f%% acierto]",
mapa.size(), capacidad, aciertos, fallos, tasaAciertos() * 100);
}
}Prueba:
public class PruebaCache {
public static void main(String[] args) {
Cache<String, Ficha> fichas = new Cache<>(3);
// El calculo simula una consulta cara (leer del CSV, o pedir por red)
Function<String, Ficha> consultaCara = isbn -> {
System.out.println(" [CALCULANDO ficha de " + isbn + "]");
return new Ficha(isbn, "Título de " + isbn);
};
System.out.println("1) " + fichas.obtenerOCalcular("978-0000000001", consultaCara));
System.out.println("2) " + fichas.obtenerOCalcular("978-0000000002", consultaCara));
System.out.println("3) " + fichas.obtenerOCalcular("978-0000000001", consultaCara)); // acierto
System.out.println("4) " + fichas.obtenerOCalcular("978-0000000003", consultaCara));
System.out.println("Claves antes de desbordar: " + fichas.clavesPorAntiguedad());
// Esta cuarta clave expulsa a la menos usada recientemente,
// que es ...0002 (la ...0001 se refresco en el paso 3).
System.out.println("5) " + fichas.obtenerOCalcular("978-0000000004", consultaCara));
System.out.println("Claves después: " + fichas.clavesPorAntiguedad());
System.out.println(fichas);
}
} [CALCULANDO ficha de 978-0000000001]
1) Ficha[978-0000000001, Título de 978-0000000001]
[CALCULANDO ficha de 978-0000000002]
2) Ficha[978-0000000002, Título de 978-0000000002]
3) Ficha[978-0000000001, Título de 978-0000000001]
[CALCULANDO ficha de 978-0000000003]
4) Ficha[978-0000000003, Título de 978-0000000003]
Claves antes de desbordar: [978-0000000002, 978-0000000001, 978-0000000003]
[CALCULANDO ficha de 978-0000000004]
5) Ficha[978-0000000004, Título de 978-0000000004]
Claves después: [978-0000000001, 978-0000000003, 978-0000000004]
Cache[3/3 entradas, 1 aciertos, 4 fallos, 20% acierto]Comentarios. Tres cosas que este ejercicio deja claras.
La clase anónima que extiende LinkedHashMap usa K y V de la clase externa. Esto funciona porque es una clase interna (no estática), y las clases internas sí pueden referirse a los parámetros de tipo de la que las contiene (04-03). Fíjate también en Cache.this.capacidad: dentro de la clase anónima, capacidad a secas no existiría.
El orden de acceso es lo que hace el LRU. Con accessOrder = true, cada get() mueve la entrada al final. Por eso al desbordar se expulsa ...0002 y no ...0001, aunque ...0001 se insertara antes: se accedió a ella en el paso 3.
obtenerOCalcular es el patrón de caché correcto. Sin él, quien usa la caché escribe if (cache.obtener(k).isEmpty()) { ... }, con dos búsquedas y una condición de carrera si hay varios hilos. Compáralo con Map.computeIfAbsent del módulo 5: es la misma idea.
Solución 2
package com.nexussoftware.bibliotech.servicio;
import java.util.*;
import java.util.function.Predicate;
public final class Agregador {
private Agregador() { } // clase de utilidad: no se instancia
/**
* PRODUCE numeros que leemos -> ? extends Number.
* Asi acepta List<Integer>, List<Double>, List<Long>...
* Con Collection<Number> NO aceptaria un List<Integer> (invariancia).
*/
public static double sumar(Collection<? extends Number> numeros) {
double total = 0;
for (Number n : numeros) {
total += n.doubleValue();
}
return total;
}
/**
* origen PRODUCE -> ? extends T
* destino CONSUME -> ? super T
* criterio CONSUME -> Predicate<? super T>
*/
public static <T> int copiarFiltrando(Collection<? extends T> origen,
Collection<? super T> destino,
Predicate<? super T> criterio) {
int copiados = 0;
for (T elemento : origen) {
if (criterio.test(elemento)) {
destino.add(elemento);
copiados++;
}
}
return copiados;
}
/**
* elementos PRODUCE -> ? extends T
* orden CONSUME -> Comparator<? super T>
* El retorno NO lleva comodin: Optional<T> limpio.
*/
public static <T> Optional<T> maximoSegun(Collection<? extends T> elementos,
Comparator<? super T> orden) {
Iterator<? extends T> it = elementos.iterator();
if (!it.hasNext()) {
return Optional.empty();
}
T mayor = it.next();
while (it.hasNext()) {
T candidato = it.next();
if (orden.compare(candidato, mayor) > 0) {
mayor = candidato;
}
}
return Optional.of(mayor);
}
}Demostración:
public class PruebaAgregador {
public static void main(String[] args) {
// 1) sumar acepta cualquier coleccion de subtipos de Number
List<Integer> paginas = List.of(412, 395, 448);
List<Double> multas = List.of(1.50, 0.75, 3.20);
System.out.printf("Páginas totales: %.0f%n", Agregador.sumar(paginas));
System.out.printf("Multas totales: %.2f €%n", Agregador.sumar(multas));
// 2) origen List<Libro>, destino List<Material>: dos tipos DISTINTOS
List<Libro> libros = new ArrayList<>(List.of(
new Libro("Java Efectivo", "978-0000000001", 412, true),
new Libro("Patrones de Diseño", "978-0000000002", 395, false),
new Libro("Refactorización", "978-0000000003", 448, true)));
List<Material> prestados = new ArrayList<>();
int copiados = Agregador.copiarFiltrando(libros, prestados, Libro::estaPrestado);
System.out.println("Copiados a la lista de materiales: " + copiados);
prestados.forEach(m -> System.out.println(" " + m.getTitulo()));
// 3) Comparator<Material> ordenando una List<Libro>: gracias al ? super T
Comparator<Material> porTitulo = Comparator.comparing(Material::getTitulo);
Agregador.maximoSegun(libros, porTitulo)
.ifPresent(l -> System.out.println("Último alfabéticamente: " + l.getTitulo()));
// Y el maximo devuelve Libro, no Material: el tipo se conserva
Optional<Libro> ultimo = Agregador.maximoSegun(libros, porTitulo);
System.out.println("El tipo estático es Libro: " + ultimo.map(Libro::getIsbn).orElse("-"));
}
}Páginas totales: 1255
Multas totales: 5,45 €
Copiados a la lista de materiales: 2
Java Efectivo
Refactorización
Último alfabéticamente: Refactorización
El tipo estático es Libro: 978-0000000003Comentarios.
Sin ? extends Number, sumar(paginas) no compilaría. Un List<Integer> no es un List<Number> por la invariancia del apartado 10. Este es el ejemplo más corto de por qué existen los comodines.
El destino puede ser de un supertipo. copiarFiltrando(libros, prestados, ...) copia Libro en un List<Material>, que es exactamente lo que uno espera poder hacer y que sin ? super T sería imposible.
El retorno conserva el tipo más específico. maximoSegun(libros, porTitulo) devuelve Optional<Libro>, no Optional<Material>, porque T se infiere de la colección. Esto es lo que se pierde si pones el comodín en el retorno.
Solución 3
package com.nexussoftware.bibliotech.servicio;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
/**
* Validador generico y encadenable.
* Acumula reglas y devuelve un Resultado<T> con TODOS los fallos,
* no solo el primero: quien rellena un formulario agradece verlos juntos.
*/
public final class ValidadorEncadenable<T> {
/** Par regla-mensaje. Un record generico (04-07 + este apartado 3). */
private record Regla<T>(Predicate<? super T> condicion, String mensaje) { }
private final String nombreTipo;
private final List<Regla<T>> reglas = new ArrayList<>();
private ValidadorEncadenable(String nombreTipo) {
this.nombreTipo = nombreTipo;
}
/**
* Token de tipo (apartado 19): el Class<T> aporta el nombre legible
* y ademas fija T sin necesidad de escribirlo.
*/
public static <T> ValidadorEncadenable<T> de(Class<T> tipo) {
return new ValidadorEncadenable<>(tipo.getSimpleName());
}
/**
* Devuelve this para encadenar.
* Predicate<? super T> por PECS: la regla CONSUME el objeto,
* asi que un Predicate<Object> o un Predicate<Material> tambien valen.
*/
public ValidadorEncadenable<T> conRegla(Predicate<? super T> condicion, String mensaje) {
reglas.add(new Regla<>(Objects.requireNonNull(condicion, "condición"),
Objects.requireNonNull(mensaje, "mensaje")));
return this;
}
public Resultado<T> validar(T objeto) {
if (objeto == null) {
return Resultado.fallo("El " + nombreTipo + " es nulo");
}
List<String> fallos = new ArrayList<>();
for (Regla<T> regla : reglas) {
if (!regla.condicion().test(objeto)) {
fallos.add(regla.mensaje());
}
}
if (fallos.isEmpty()) {
return Resultado.exito(objeto);
}
return Resultado.fallo(nombreTipo + " no válido: " + String.join("; ", fallos));
}
public int numeroDeReglas() {
return reglas.size();
}
}Uso:
public class PruebaValidador {
public static void main(String[] args) {
ValidadorEncadenable<Libro> validador = ValidadorEncadenable.de(Libro.class)
.conRegla(l -> l.getIsbn() != null, "el ISBN es obligatorio")
.conRegla(l -> l.getIsbn() != null
&& l.getIsbn().length() == 17, "el ISBN debe tener 17 caracteres")
.conRegla(l -> l.getTitulo() != null
&& !l.getTitulo().isBlank(), "el título no puede estar vacío")
.conRegla(l -> l.getTitulo() == null
|| l.getTitulo().length() < 200, "el título excede los 200 caracteres");
System.out.println("Reglas configuradas: " + validador.numeroDeReglas());
List<Libro> candidatos = List.of(
new Libro("Java Efectivo", "978-0000000001"),
new Libro("Patrones de Diseño", "978-1"),
new Libro("", "978-0000000003"));
for (Libro candidato : candidatos) {
Resultado<Libro> resultado = validador.validar(candidato);
// Encadenamiento con map: solo se ejecuta si hubo exito
String linea = resultado
.map(Libro::getTitulo)
.map(t -> "ACEPTADO: " + t)
.oPorDefecto("RECHAZADO: " + (resultado.esFallo() ? resultado.error() : ""));
System.out.println(linea);
}
// El validador es reutilizable con cualquier Predicate<Material>
Predicate<Material> tieneTitulo = m -> m.getTitulo() != null;
ValidadorEncadenable<Libro> otro = ValidadorEncadenable.de(Libro.class)
.conRegla(tieneTitulo, "necesita título"); // Predicate<Material> en un validador de Libro
System.out.println(otro.validar(new Libro("Refactorización", "978-0000000003")));
}
}Reglas configuradas: 4
ACEPTADO: Java Efectivo
RECHAZADO: Libro no válido: el ISBN debe tener 17 caracteres
RECHAZADO: Libro no válido: el título no puede estar vacío
Éxito[Libro[Refactorización, 978-0000000003]]Comentarios.
El record Regla<T> privado y genérico demuestra que los records también se parametrizan, y que puedes tener tipos genéricos anidados. Como es privado y estático (los records lo son implícitamente), tiene que declarar su propio <T>: no puede usar el de la clase externa.
Predicate<? super T> no es decorativo. La última línea del main pasa un Predicate<Material> a un ValidadorEncadenable<Libro>. Con Predicate<T> a secas eso no compilaría, y tendrías que duplicar los predicados comunes por cada subtipo de Material.
Acumular todos los fallos en lugar de cortar en el primero es una decisión de diseño, no un detalle. Un validador que corta obliga al usuario a corregir un error, reenviar, descubrir el siguiente, corregir, reenviar. En 11-02 verás que la validación de Spring hace exactamente esto y por la misma razón.
Y fíjate en que el Resultado<T> genérico ha aparecido aquí sin ningún esfuerzo. El validador no sabe nada de préstamos ni de libros: devuelve Resultado<T> de lo que sea que valide. Esa es la ganancia de haber generalizado.
Conclusión
Has saldado la deuda más antigua del curso: ya sabes exactamente qué significa ese <T>.
Sabes qué problema resuelven: antes de Java 5, un contenedor era de Object, cada lectura exigía un cast y cada error de tipo llegaba a producción como ClassCastException. Con genéricos el mismo error llega a tu editor en compilación, desaparecen los casts, la API se documenta sola y el IDE puede ayudarte.
Escribes clases genéricas propias (Caja<T>, Par<A, B>), conoces las convenciones (T, E, K, V, R), usas el diamante <> y sabes que en un método static hay que declarar un parámetro de tipo nuevo porque el de la clase pertenece a la instancia. Escribes interfaces genéricas y sabes que al implementarlas puedes fijar el tipo (implements Serializador<Libro>) o propagarlo (class Decorador<T> implements Serializador<T>). Escribes métodos genéricos con static <T> void ... y dejas que la inferencia haga el trabajo.
Acotas con límites: <T extends Comparable<T>> para poder llamar a compareTo, <T extends Material> para poder llamar a getTitulo, y límites múltiples con & con la clase siempre primero.
Y entiendes la parte difícil. Los genéricos son invariantes —List<String> no es un List<Object>— y sabes exactamente por qué: permitirlo dejaría meter un Integer en una lista de String y devolvería el problema de 1999. Has visto el contraste con los arrays covariantes, que sí lo permiten y pagan por ello con ArrayStoreException en ejecución. Los comodines recuperan la flexibilidad: <?> para lo que da igual, <? extends X> para leer, <? super X> para escribir. Y PECS —Producer Extends, Consumer Super— es la regla que decide cuál, con los errores de compilación y el CAP#1 que aparecen al equivocarse. Ahora las firmas del JDK (Comparator<? super T>, Function<? super T, ? extends R>) se leen solas.
Sabes qué hace realmente el compilador: el borrado de tipos. Sustituye cada parámetro por su límite, inserta los casts y genera métodos puente. Y de ahí salen todas las limitaciones que ahora reconoces al instante: no hay new T(), no hay instanceof List<String>, no hay sobrecargas que solo difieran en el genérico, no hay arrays genéricos, y List<Libro>.class no existe. Los tipos crudos solo están ahí por compatibilidad y desactivan la comprobación de todo lo que tocan; @SuppressWarnings("unchecked") es legítimo con ámbito mínimo y una justificación escrita. Y el token de tipo Class<T> es la vía limpia para recuperar en ejecución lo que el borrado se llevó, con tipo.cast() haciendo el cast comprobado sin avisos.
BiblioTech ha cambiado de forma visible. CatalogoConcurrente y RegistroPrestamosSeguro —doscientas cincuenta líneas casi idénticas— han desaparecido en favor de un único Repositorio<T extends Identificable> que se instancia en una línea para materiales, préstamos, salas o reservas, con PECS bien aplicado en buscar, listarOrdenado y volcarEn. Y el Resultado del módulo 6 es ahora un Resultado<T> genérico con map y flatMap, cuyo tipo de retorno documenta el contrato: esto puede fallar, y cuando va bien te da un Prestamo.
Pero hay algo que los genéricos no pueden hacer. Los genéricos le dicen cosas al compilador, y solo al compilador: qué tipos encajan dónde. No sirven para decir "este campo debe exportarse al CSV en la tercera columna", ni "esta operación hay que registrarla en la auditoría", ni "este método está obsoleto desde la versión 3". Esa clase de información —metadatos sobre el código, dirigidos no solo al compilador sino también a las herramientas de construcción y a los frameworks que se ejecutan— necesita otro mecanismo.
Ese mecanismo son las anotaciones, y es la siguiente lección. Verás las estándar del JDK y qué hace realmente cada una, aprenderás a crear las tuyas con @interface, y sobre todo entenderás las meta-anotaciones —empezando por @Retention, que decide si tu anotación llegará viva a la ejecución o morirá en el compilador—. BiblioTech definirá @CampoCsv para marcar qué campos se exportan y @Auditable para marcar qué operaciones se registran. Y entonces quedará una pregunta abierta que resolverá 10-03: una anotación no hace nada por sí sola; alguien tiene que leerla. Ese alguien es la reflexión.
Curso de Programación en Java
Módulo 1: Introducción a Java
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
