La lección anterior terminó con una molestia legítima: Catalogo.PorTitulo es una clase completa —declaración, @Override, cuerpo, nombre y un hueco en el espacio de nombres— para una sola línea de lógica que se usa exactamente en un sitio. Cuando el código que quieres escribir cabe en una línea y la ceremonia para envolverlo ocupa cinco, algo sobra.
Las clases anónimas eliminan esa ceremonia. Permiten declarar una implementación e instanciarla en la misma expresión, en el punto exacto donde hace falta y sin darle nombre. Fueron durante quince años el mecanismo estándar de Java para pasar comportamiento a un método: cada Comparator, cada listener de interfaz gráfica, cada Runnable se escribía así. Hoy las lambdas han ocupado buena parte de ese terreno, pero las clases anónimas siguen siendo la única opción en varios escenarios concretos, y sobre todo siguen apareciendo en todo el código Java existente: leerlas con soltura no es opcional. Además, entenderlas a fondo es la mejor preparación posible para la lección 04-05, porque una lambda no es más que una clase anónima a la que se le ha quitado todo lo prescindible.
Contenido
- Qué es una clase anónima
- La sintaxis, diseccionada
- Qué puede y qué no puede hacer
- El bloque de inicialización de instancia
- Métodos propios: existen, pero casi no sirven
- Captura de variables efectivamente finales
thisdentro de una clase anónima- Uso clásico 1: un
Comparatorpara el catálogo - Uso clásico 2: una devolución de llamada
- Uso clásico 3: extender puntualmente una clase
- Cuándo sigue siendo la opción correcta y cuándo usar una lambda
- El "double brace initialization" y por qué evitarlo
- Peso en el bytecode y legibilidad
- Tabla de decisión final
- Errores Comunes y Consejos
- Ejercicios
- Qué es una clase anónima
Una clase anónima es una clase sin nombre que se declara e instancia en una sola expresión. Puede ser:
- Una implementación de una interfaz, o
- Una subclase de una clase (concreta o abstracta).
Compara las dos formas de obtener un comparador por título:
// --- FORMA 1: clase anidada con nombre (04-03) ---
public static class PorTitulo implements Comparator<Material> {
@Override
public int compare(Material a, Material b) {
return a.getTitulo().compareToIgnoreCase(b.getTitulo());
}
}
// ... y en otro sitio:
Arrays.sort(catalogo, new Catalogo.PorTitulo());// --- FORMA 2: clase anonima, aqui mismo ---
Arrays.sort(catalogo, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitulo().compareToIgnoreCase(b.getTitulo());
}
});La segunda forma no declara ninguna clase con nombre, no ocupa un lugar en el espacio de nombres de Catalogo y pone la lógica exactamente donde se usa. Quien lee Arrays.sort(...) ve en el acto por qué criterio se ordena, sin ir a buscar otro fichero.
La expresión new Comparator<Material>() { ... } hace tres cosas a la vez:
- Declara una clase nueva que implementa
Comparator<Material>. - La instancia.
- Devuelve esa instancia como valor de la expresión.
- La sintaxis, diseccionada
Esta es la parte que conviene mirar con lupa, porque tiene una estructura peculiar:
Comparator<Material> porTitulo = new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitulo().compareToIgnoreCase(b.getTitulo());
}
};Pieza por pieza:
| Fragmento | Qué es |
|---|---|
Comparator<Material> porTitulo = |
Declaración normal. El tipo es la interfaz, no la clase anónima (que no tiene nombre) |
new |
Se va a crear un objeto |
Comparator<Material> |
El tipo que se implementa (si es interfaz) o se extiende (si es clase) |
() |
Argumentos del constructor. Vacíos si es una interfaz; si extiendes una clase, aquí van los suyos |
{ ... } |
Cuerpo de la clase anónima: sus métodos y campos |
; |
Punto y coma final. El más olvidado de Java |
Ese punto y coma final es la fuente de error número uno con clases anónimas. La razón es que toda la construcción es una expresión dentro de una sentencia, no una declaración de clase. Piénsalo así: si escribes int x = 5;, el ; es evidente. Aquí ocurre lo mismo; lo que despista es que en medio hay una llave de cierre, y el ojo la lee como el final de una clase.
// Con el punto y coma final (correcto):
Comparator<Material> c = new Comparator<Material>() { ... };
// Sin el (error de compilacion):
Comparator<Material> c = new Comparator<Material>() { ... }Cuando la anónima va como argumento de un método, el paréntesis de cierre aparece después de la llave, algo que resulta chocante al principio:
Arrays.sort(catalogo, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitulo().compareToIgnoreCase(b.getTitulo());
}
}); // llave, parentesis, punto y comaEse }); es la firma visual de una clase anónima en código Java. Aprende a reconocerlo.
- Qué puede y qué no puede hacer
| Puede | No puede |
|---|---|
| Implementar una interfaz | Implementar varias interfaces |
| Extender una clase (concreta o abstracta) | Extender una clase e implementar interfaces a la vez |
| Tener campos propios | Tener un constructor propio |
| Tener un bloque de inicialización de instancia | Ser abstract, static o final |
| Declarar métodos nuevos | Que esos métodos se llamen desde fuera (apartado 5) |
| Acceder a los miembros de la clase externa | Ser reutilizada: es de un solo uso |
| Capturar variables efectivamente finales | Modificar esas variables |
La limitación central es no tener constructor. Y la razón es sencilla: un constructor lleva el nombre de su clase, y esta clase no tiene nombre. No hay forma de escribirlo.
Comparator<Material> c = new Comparator<Material>() {
// Comparator() { } // IMPOSIBLE: la clase no tiene nombre
// AnonimaSinNombre() { } // IMPOSIBLE: no se llama asi
@Override public int compare(Material a, Material b) { return 0; }
};¿Y si extiendes una clase que sí tiene constructor? Entonces puedes pasarle argumentos en los paréntesis del new, que se reenvían al constructor de la superclase:
// Extiende Empleado usando su constructor de dos argumentos
Empleado becario = new Empleado("Nuria Vidal", "EMP-003") {
@Override
public boolean puedeTomarPrestado() {
return false; // los becarios en practicas no toman prestados materiales
}
};Aquí ("Nuria Vidal", "EMP-003") no es el constructor de la anónima: es la invocación del constructor de Empleado. La anónima recibe un constructor implícito que se limita a reenviarlos.
- El bloque de inicialización de instancia
Si no hay constructor, ¿cómo inicializas un campo con lógica? Con un bloque de inicialización de instancia: un bloque { ... } suelto en el cuerpo de la clase, que se ejecuta al construir el objeto, justo después del constructor de la superclase.
Comparator<Material> porTituloConLog = new Comparator<Material>() {
private int comparaciones; // campo propio
private final String etiqueta; // campo final
{ // BLOQUE DE INICIALIZACION DE INSTANCIA (hace de constructor)
etiqueta = "cmp-" + System.nanoTime();
comparaciones = 0;
System.out.println("Comparador creado: " + etiqueta);
}
@Override
public int compare(Material a, Material b) {
comparaciones++;
return a.getTitulo().compareToIgnoreCase(b.getTitulo());
}
};Este mecanismo no es exclusivo de las anónimas —cualquier clase puede tenerlo—, pero aquí es la única vía para ejecutar código de inicialización. El orden completo, ampliando lo que viste en 03-04, es:
flowchart TD
A["new Anonima(...)"] --> B["constructor de la superclase"]
B --> C["inicializadores de campos, en orden de escritura"]
C --> D["bloque de inicializacion de instancia"]
D --> E["cuerpo del constructor implicito (vacio)"]
E --> F["objeto listo"]
En la práctica, si tu clase anónima necesita un bloque de inicialización complejo, considéralo una señal: probablemente merece ser una clase con nombre.
- Métodos propios: existen, pero casi no sirven
Una clase anónima puede declarar métodos que no están en la interfaz o la clase base. Compilan. Pero hay una trampa:
Comparator<Material> comparador = new Comparator<Material>() {
private int comparaciones;
@Override
public int compare(Material a, Material b) {
comparaciones++;
return a.getTitulo().compareToIgnoreCase(b.getTitulo());
}
/** Metodo propio: NO esta en Comparator. */
public int getComparaciones() {
return comparaciones;
}
};
Arrays.sort(catalogo, comparador);
// comparador.getComparaciones(); // NO COMPILAerror: cannot find symbol symbol: method getComparaciones() location: variable comparador of type Comparator<Material>
El motivo es la regla de polimorfismo de 03-06: el tipo declarado decide qué puedes llamar. La variable es de tipo Comparator<Material>, y Comparator no tiene getComparaciones(). Y no puedes declararla del tipo real, porque el tipo real no tiene nombre.
Hay un resquicio, que sirve para entender el mecanismo aunque no lo uses: invocar el método sobre la propia expresión, antes de que el tipo se pierda.
System.out.println(
new Object() {
int contarLetras(String s) { return s.length(); }
}.contarLetras("Java Efectivo")
); // 13Funciona porque el compilador conoce el tipo anónimo mientras dura la expresión. En cuanto lo asignas a una variable, el tipo se generaliza y el método se vuelve inalcanzable.
Conclusión práctica: los métodos propios de una anónima solo son útiles dentro de ella misma, como auxiliares privados del método que sí forma parte del contrato. Si necesitas exponer métodos nuevos, necesitas una clase con nombre.
- Captura de variables efectivamente finales
Se aplica exactamente la misma regla que a las clases locales (04-03): una clase anónima puede leer variables locales y parámetros del método que la rodea, siempre que sean final o efectivamente finales.
public Material[] ordenarPorMulta(Material[] materiales, int diasTranscurridos) {
Material[] copia = Arrays.copyOf(materiales, materiales.length);
Arrays.sort(copia, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
// 'diasTranscurridos' es un parametro capturado: no se reasigna
return Double.compare(b.calcularMulta(diasTranscurridos),
a.calcularMulta(diasTranscurridos));
}
});
return copia;
}Si en cualquier punto del método escribieras diasTranscurridos = 30;, la variable dejaría de ser efectivamente final y el error aparecería dentro de la clase anónima:
La razón es la que estudiaste en 04-03: la variable local vive en la pila y muere con el método; el objeto anónimo vive en el montón y puede sobrevivirle. Java copia el valor dentro del objeto, y permitir la modificación produciría dos copias descoordinadas.
Y una nota importante que evita confusiones: la restricción afecta a la variable, no al objeto. Puedes capturar una referencia y modificar el objeto al que apunta:
Empleado marta = new Empleado("Marta Ruiz", "EMP-001");
Runnable tarea = new Runnable() {
@Override
public void run() {
marta.registrarPrestamo(); // PERMITIDO: se modifica el OBJETO
// marta = otroEmpleado; // PROHIBIDO: se reasignaria la VARIABLE
}
};Runnable es la interfaz de las tareas ejecutables; su uso real con hilos es el módulo 8.
this dentro de una clase anónima
this dentro de una clase anónimaEste es el punto que provoca más errores sutiles del módulo, y merece atención completa.
Dentro de una clase anónima, this se refiere a la propia clase anónima, no a la clase que la rodea.
package com.nexussoftware.bibliotech.servicio;
public class GestorPrestamos {
private final String nombreGestor = "Gestor central";
public void demostrarThis() {
Runnable tarea = new Runnable() {
private final String nombreGestor = "Tarea anonima";
@Override
public void run() {
System.out.println("this.nombreGestor = " + this.nombreGestor);
System.out.println("GestorPrestamos.this.nombre = "
+ GestorPrestamos.this.nombreGestor);
System.out.println("this.getClass() = " + this.getClass().getName());
System.out.println("GestorPrestamos.this.class = "
+ GestorPrestamos.this.getClass().getName());
}
};
tarea.run();
}
}this.nombreGestor = Tarea anonima GestorPrestamos.this.nombre = Gestor central this.getClass() = com.nexussoftware.bibliotech.servicio.GestorPrestamos$1 GestorPrestamos.this.class = com.nexussoftware.bibliotech.servicio.GestorPrestamos
Las dos últimas líneas lo demuestran sin lugar a dudas: this es un objeto de la clase GestorPrestamos$1, la clase anónima que el compilador ha generado (04-03, apartado 9).
Para llegar a la instancia externa hay que escribir ClaseExterna.this, la misma sintaxis de las clases internas.
¿Por qué importa tanto? Porque el error típico es silencioso:
public class Catalogo {
private String ultimoCriterio;
public void ordenarPorTitulo(Material[] materiales) {
Arrays.sort(materiales, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
// Esto SI funciona: no hay ambiguedad, el campo solo existe fuera
ultimoCriterio = "titulo";
return a.getTitulo().compareToIgnoreCase(b.getTitulo());
}
});
}
}Ese acceso funciona porque no hay ningún ultimoCriterio en la anónima. Pero en cuanto la anónima declara un campo con el mismo nombre —o llamas a un método registrar() que existe en ambas—, la resolución cambia de destino sin avisar y el programa hace algo distinto de lo que esperabas. La regla de resolución, igual que en 04-03: variable local → miembro de la anónima → miembro de la externa.
Guarda esta tabla, porque en 04-05 vas a compararla con el comportamiento de las lambdas y verás que es justo el contrario:
| Expresión | Dentro de una clase anónima |
|---|---|
this |
La instancia anónima |
this.campo |
Campo de la anónima |
Externa.this |
La instancia de la clase envolvente |
this.getClass() |
Externa$1 |
Nombre suelto sin this |
El más cercano: local, luego anónima, luego externa |
- Uso clásico 1: un
Comparator para el catálogo
Comparator para el catálogoEl uso más frecuente. Aquí está el Catalogo de 04-03 reescrito con anónimas, más un tercer criterio compuesto:
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.Material;
import java.util.Arrays;
import java.util.Comparator;
public class Catalogo {
private final Material[] materiales;
public Catalogo(Material[] materiales) {
this.materiales = Arrays.copyOf(materiales, materiales.length);
}
private Material[] copia() {
return Arrays.copyOf(materiales, materiales.length);
}
/** Orden alfabetico por titulo. */
public Material[] ordenadoPorTitulo() {
Material[] c = copia();
Arrays.sort(c, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitulo().compareToIgnoreCase(b.getTitulo());
}
});
return c;
}
/** Mayor multa primero. El parametro se captura: es efectivamente final. */
public Material[] ordenadoPorMulta(int diasTranscurridos) {
Material[] c = copia();
Arrays.sort(c, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return Double.compare(b.calcularMulta(diasTranscurridos),
a.calcularMulta(diasTranscurridos));
}
});
return c;
}
/** Criterio compuesto: primero por tipo, y dentro de cada tipo por titulo. */
public Material[] ordenadoPorTipoYTitulo() {
Material[] c = copia();
Arrays.sort(c, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
int porTipo = a.getTipo().compareTo(b.getTipo());
if (porTipo != 0) {
return porTipo; // criterio principal
}
return a.getTitulo().compareToIgnoreCase(b.getTitulo()); // desempate
}
});
return c;
}
}Material[] datos = {
new Dvd("Refactorizacion en vivo", "DVD-0007", 95),
new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Libro("Patrones de Diseno", "Erich Gamma", "978-0000000002", 1994),
new Libro("Refactorizacion", "Martin Fowler", "978-0000000003", 1999)
};
Catalogo catalogo = new Catalogo(datos);
System.out.println("--- Por tipo y titulo ---");
for (Material m : catalogo.ordenadoPorTipoYTitulo()) {
System.out.printf(" %-10s %s%n", m.getTipo(), m.getTitulo());
}--- Por tipo y titulo --- DVD Refactorizacion en vivo Libro Java Efectivo Libro Patrones de Diseno Libro Refactorizacion Revista Java Magazine
El ordenadoPorTipoYTitulo es un buen ejemplo de anónima que sigue justificándose: tiene lógica de varias líneas con una condición intermedia. Ordenar así con un solo compare es exactamente lo que Comparator.comparing().thenComparing() resolverá de forma más elegante en 04-06.
- Uso clásico 2: una devolución de llamada
Una devolución de llamada (callback) es un objeto con comportamiento que le pasas a otro para que lo invoque cuando ocurra algo. Es el mecanismo que permite que una clase de servicio notifique sin saber a quién.
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.Prestamo;
/** Se avisa a este oyente cada vez que se registra una devolucion. */
public interface OyenteDevolucion {
void alDevolver(Prestamo prestamo, double multa);
}package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.Prestamo;
public class GestorPrestamos {
private OyenteDevolucion oyente;
/** Registra quien quiere ser avisado. */
public void setOyente(OyenteDevolucion oyente) {
this.oyente = oyente;
}
public double procesarDevolucion(Prestamo prestamo, int diasTranscurridos) {
double multa = prestamo.registrarDevolucion(diasTranscurridos);
if (oyente != null) {
oyente.alDevolver(prestamo, multa); // callback
}
return multa;
}
}Y ahora el uso, con dos oyentes distintos definidos en el punto exacto donde se decide qué hacer:
GestorPrestamos gestor = new GestorPrestamos();
// Oyente 1: recibo por consola
gestor.setOyente(new OyenteDevolucion() {
@Override
public void alDevolver(Prestamo prestamo, double multa) {
System.out.printf("RECIBO %s: %s devuelto por %s. Multa %.2f EUR%n",
prestamo.getReferencia(), prestamo.getTituloMaterial(),
prestamo.getNombreEmpleado(), multa);
}
});
gestor.procesarDevolucion(p1, 20);
// Oyente 2: solo alerta si la multa supera un umbral
final double umbral = 5.0;
gestor.setOyente(new OyenteDevolucion() {
@Override
public void alDevolver(Prestamo prestamo, double multa) {
if (multa > umbral) { // captura de 'umbral'
System.out.printf("ALERTA: multa alta (%.2f EUR) en %s%n",
multa, prestamo.getReferencia());
}
}
});
gestor.procesarDevolucion(p2, 40);RECIBO PR-0001: Java Efectivo devuelto por Marta Ruiz. Multa 1,25 EUR ALERTA: multa alta (8,50 EUR) en PR-0002
Fíjate en lo conseguido: GestorPrestamos no sabe nada de recibos ni de alertas. Solo conoce la interfaz OyenteDevolucion y llama a su método. Es la inversión de dependencias de 04-01 en acción, y es la base del patrón Observer que formalizarás en 12-02.
- Uso clásico 3: extender puntualmente una clase
Las anónimas también sirven para crear una variante puntual de una clase existente, típicamente en pruebas:
// Un empleado con un limite distinto, solo para esta prueba
Empleado directivo = new Empleado("Nuria Vidal", "EMP-004") {
@Override
public boolean puedeTomarPrestado() {
return true; // sin limite de prestamos simultaneos
}
};
for (int i = 0; i < 6; i++) {
directivo.registrarPrestamo();
}
System.out.println(directivo.getNombre() + " ha tomado 6 materiales sin aviso.");Y con clases abstractas, que no se pueden instanciar (04-02) pero sí extender anónimamente:
// Un material de prueba, sin necesidad de crear una clase nueva
Material materialPrueba = new Material("Manual interno", "INT-0001", true) {
@Override public String getTipo() { return "Interno"; }
@Override public int getDiasPrestamo() { return 5; }
@Override public double getTarifaDiaria() { return 0.05; }
};
System.out.println(materialPrueba.describir());
System.out.printf("Multa a los 20 dias: %.2f EUR%n", materialPrueba.calcularMulta(20));Este uso —crear al vuelo una implementación mínima de una clase abstracta para probar la lógica de la base— es muy habitual en el módulo 11 con JUnit. Y observa el detalle: new Material(...) está prohibido, pero new Material(...) { ... } sí es legal, porque no instancia Material, sino una subclase anónima que sí es concreta.
- Cuándo sigue siendo la opción correcta y cuándo usar una lambda
Con Java 8 llegaron las lambdas, que expresan lo mismo en mucho menos espacio:
// Clase anonima
Arrays.sort(c, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitulo().compareToIgnoreCase(b.getTitulo());
}
});
// Lambda equivalente (04-05)
Arrays.sort(c, (a, b) -> a.getTitulo().compareToIgnoreCase(b.getTitulo()));Pero la lambda no sustituye a la clase anónima en todos los casos. La regla es tajante:
Una lambda solo puede implementar una interfaz con un único método abstracto (una interfaz funcional, 04-06). Para todo lo demás, sigue haciendo falta una clase anónima.
| Situación | Usa |
|---|---|
| Interfaz con un solo método abstracto y cuerpo breve | Lambda |
| Interfaz con dos o más métodos abstractos | Clase anónima |
| Extender una clase (concreta o abstracta) | Clase anónima |
| Necesitas campos propios con estado entre llamadas | Clase anónima |
| Necesitas un bloque de inicialización | Clase anónima |
Necesitas que this sea el objeto implementador |
Clase anónima |
| Cuerpo de más de ~5 líneas o con varias ramas | Clase anónima o método con nombre |
| Se usa en más de un sitio | Clase con nombre |
Ejemplo de anónima insustituible, con estado propio entre llamadas:
/** Comparador que cuenta cuantas veces se le llama. Una lambda NO puede hacer esto. */
Comparator<Material> instrumentado = new Comparator<Material>() {
private int llamadas; // ESTADO propio: imposible en una lambda
@Override
public int compare(Material a, Material b) {
llamadas++;
if (llamadas % 5 == 0) {
System.out.println(" [" + llamadas + " comparaciones]");
}
return a.getTitulo().compareToIgnoreCase(b.getTitulo());
}
};La sintaxis completa de la lambda, sus formas abreviadas y sus reglas son el contenido de la lección 04-05. Aquí basta con saber que existe y cuándo no te sirve.
- El "double brace initialization" y por qué evitarlo
Existe un truco muy difundido que combina una clase anónima con un bloque de inicialización para inicializar objetos "en línea". Se llama double brace initialization por las dos llaves consecutivas:
// NO HAGAS ESTO
Empleado marta = new Empleado("Marta Ruiz", "EMP-001") {{
registrarPrestamo();
registrarPrestamo();
}};Se lee así: la primera llave abre la clase anónima, la segunda abre un bloque de inicialización de instancia. Parece elegante y compacto. Es una mala práctica reconocida, por cuatro razones concretas:
- Crea una clase extra por cada uso. Cada
{{ }}genera un fichero.classmás. Un método que se llame mil veces con esta sintaxis en un bucle genera una clase, sí, pero mil instancias de una subclase innecesaria. - El objeto ya no es de la clase que crees.
marta.getClass()devuelveApp$1, noEmpleado. Y como losequalsbien escritos comparan congetClass()(03-09),marta.equals(otroEmpleado)devuelvefalseaunque tengan el mismo identificador. Es un bug real y difícil de encontrar. - Retiene la instancia externa. Es una clase interna no estática: arrastra la referencia oculta
this$0con todo lo que eso implica (04-03), incluida la fuga de memoria si el objeto se guarda en una caché. - Rompe la serialización. Una clase anónima no serializa bien (módulo 7).
Las alternativas correctas ya están en tu mano desde el módulo 3:
// Opcion A: simplemente escribe las lineas
Empleado marta = new Empleado("Marta Ruiz", "EMP-001");
marta.registrarPrestamo();
marta.registrarPrestamo();
// Opcion B: un constructor que reciba el estado inicial
Empleado marta2 = new Empleado("Marta Ruiz", "EMP-001", 2);
// Opcion C: un metodo de fabrica con nombre expresivo
Empleado marta3 = Empleado.conPrestamos("Marta Ruiz", "EMP-001", 2);
- Peso en el bytecode y legibilidad
Dos costes que conviene tener presentes.
Una clase por cada anónima. El compilador genera Externa$1.class, Externa$2.class, Externa$3.class... numerando por orden de aparición. Consecuencias:
- Más ficheros en el
.jary más tiempo de carga de clases al arrancar. - Trazas de error crípticas:
at com.nexussoftware.bibliotech.servicio.Catalogo$2.compare(Catalogo.java:41). El nombre no dice nada; hay que ir a la línea 41 para saber de qué comparador se habla. - Renumeración: si añades una anónima antes de otra, todas las siguientes cambian de número. En depuración esto despista.
Las lambdas mitigan esto, porque no generan una clase por lambda: usan la instrucción invokedynamic y crean la implementación en tiempo de ejecución. Es una de sus ventajas menos conocidas y es la razón de que las lambdas no encarezcan el arranque como sí lo hacían miles de anónimas.
Legibilidad. Una anónima de tres líneas mejora la lectura, porque pone la lógica donde se usa. Una anónima de treinta líneas destroza la lectura del método que la contiene: el flujo principal queda enterrado bajo una declaración de clase. La regla informal más útil:
Si la clase anónima no cabe entera en la pantalla junto con el código que la rodea, sácala a una clase con nombre.
- Tabla de decisión final
Este es el resumen del módulo hasta aquí. Guárdalo: responde a la pregunta "¿qué forma de clase uso?" en casi cualquier situación real.
| Forma | Úsala cuando | Evítala cuando |
|---|---|---|
| Clase de primer nivel | El tipo tiene entidad propia, se usa desde varios sitios y forma parte del dominio | Es un auxiliar de quince líneas que solo usa una clase |
| Anidada estática | Es un auxiliar ligado conceptualmente a la externa: nodos, builders, agrupaciones de datos | Necesita el estado de la instancia externa |
| Interna (no estática) | De verdad necesitas acceder al estado de la instancia externa | Casi siempre. Riesgo de fuga de memoria |
| Local | Un tipo con varios campos y métodos que solo existe dentro de un algoritmo | Cabe en una lambda o el método ya es largo |
| Anónima | Implementación de un solo uso que necesita estado, varios métodos abstractos o extender una clase | La interfaz es funcional y el cuerpo es breve |
| Lambda (04-05) | Interfaz funcional, cuerpo breve, sin estado propio | Necesitas this, estado o extender una clase |
flowchart TD
A["Necesito una implementacion"] --> B{"Se usa en mas de un sitio?"}
B -- "Si" --> C["Clase con nombre: primer nivel o anidada estatica"]
B -- "No" --> D{"Extiendo una clase o la interfaz tiene varios metodos abstractos?"}
D -- "Si" --> E["Clase anonima"]
D -- "No" --> F{"Necesito estado propio o que this sea el implementador?"}
F -- "Si" --> E
F -- "No" --> G{"El cuerpo cabe en pocas lineas?"}
G -- "Si" --> H["Lambda"]
G -- "No" --> I["Metodo con nombre y referencia a metodo"]
Errores Comunes y Consejos
Olvidar el punto y coma final. }; cierra una sentencia, no una clase. Es el error número uno, y el mensaje del compilador (';' expected) apunta a la línea siguiente, lo que despista.
Intentar escribir un constructor. No existe. Usa un bloque de inicialización de instancia, o pasa los argumentos al constructor de la superclase en los paréntesis del new.
Llamar desde fuera a un método propio de la anónima. No compila: la variable tiene el tipo de la interfaz o la superclase. Si necesitas exponer métodos nuevos, necesitas nombre.
Creer que this es la clase externa. Es el error conceptual más caro. Dentro de la anónima, this es la anónima. Usa Externa.this. Y prepárate para que en 04-05 esto sea justo al revés con las lambdas.
Modificar una variable local capturada. Prohibido: debe ser efectivamente final. Pero sí puedes modificar el objeto al que apunta una referencia capturada.
Usar double brace initialization. Cambia la clase real del objeto, rompe equals con getClass(), retiene la instancia externa y genera clases de más.
Anónimas gigantes. Una anónima de treinta líneas dentro de una llamada a método es un método ilegible. Sácala.
Consejo: pon siempre @Override. Igual que en herencia (03-05), detecta al instante una firma mal escrita. Sin él, un compare(Material a, Object b) mal tecleado se convierte en un método nuevo y el compilador se queja mucho más tarde y peor.
Consejo: nombra la variable si vas a reutilizar la anónima. Si el mismo comparador se usa en tres métodos, asígnalo a un campo private static final Comparator<Material> POR_TITULO = new Comparator<>() { ... };. Se crea una sola instancia y el nombre documenta el criterio.
Consejo: el diamante <> funciona desde Java 9. new Comparator<>() { ... } es válido y ahorra repetir el tipo. Antes de Java 9 había que escribir new Comparator<Material>() { ... }.
Ejercicios
Ejercicio 1: tres comparadores anónimos
Escribe un método static void mostrarOrdenado(Material[] materiales, String criterio, int dias) que ordene una copia del array con Arrays.sort usando una clase anónima distinta según el criterio recibido: "titulo" (alfabético ascendente), "plazo" (menor plazo primero) y "multa" (mayor multa primero, para los días indicados). Usa switch de flechas (02-03). Prueba los tres con el catálogo de BiblioTech.
Ejercicio 2: callback con estado
Amplía el GestorPrestamos del apartado 9. Escribe una clase anónima OyenteDevolucion que acumule el número de devoluciones y el total de multas en campos propios, e imprima un resumen cada tres devoluciones. Explica por qué esta anónima no podría ser una lambda.
Ejercicio 3: this en una anónima
Escribe una clase Auditor con un campo private String origen = "Auditor" y un método auditar() que cree una clase anónima Runnable con su propio campo origen = "Tarea". Dentro de run(), imprime: el campo de la anónima, el de la clase externa, el nombre de clase de this y el nombre de clase de Auditor.this. Predice la salida antes de ejecutarla.
Soluciones
Solución 1
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.*;
import java.util.Arrays;
import java.util.Comparator;
public class OrdenadorCatalogo {
public static void mostrarOrdenado(Material[] materiales, String criterio, int dias) {
Material[] copia = Arrays.copyOf(materiales, materiales.length);
// switch de flechas (02-03) que devuelve un Comparator distinto en cada rama
Comparator<Material> comparador = switch (criterio) {
case "titulo" -> new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitulo().compareToIgnoreCase(b.getTitulo());
}
};
case "plazo" -> new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
// Integer.compare evita el desbordamiento de 'a - b'
return Integer.compare(a.getDiasPrestamo(), b.getDiasPrestamo());
}
};
case "multa" -> new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
// b antes que a: orden DESCENDENTE
// 'dias' es un parametro capturado, efectivamente final
return Double.compare(b.calcularMulta(dias), a.calcularMulta(dias));
}
};
default -> new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return 0; // sin orden: deja el array como esta
}
};
};
Arrays.sort(copia, comparador);
System.out.println("--- Ordenado por " + criterio + " ---");
for (Material m : copia) {
System.out.printf(" %-10s %-24s plazo %2d multa %5.2f EUR%n",
m.getTipo(), m.getTitulo(),
m.getDiasPrestamo(), m.calcularMulta(dias));
}
}
public static void main(String[] args) {
Material[] catalogo = {
new Dvd("Refactorizacion en vivo", "DVD-0007", 95),
new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Libro("Patrones de Diseno", "Erich Gamma", "978-0000000002", 1994)
};
mostrarOrdenado(catalogo, "titulo", 20);
mostrarOrdenado(catalogo, "plazo", 20);
mostrarOrdenado(catalogo, "multa", 20);
}
}--- Ordenado por titulo --- Libro Java Efectivo plazo 15 multa 1,25 EUR Revista Java Magazine plazo 7 multa 1,30 EUR Libro Patrones de Diseno plazo 15 multa 1,25 EUR DVD Refactorizacion en vivo plazo 3 multa 8,50 EUR --- Ordenado por plazo --- DVD Refactorizacion en vivo plazo 3 multa 8,50 EUR Revista Java Magazine plazo 7 multa 1,30 EUR Libro Java Efectivo plazo 15 multa 1,25 EUR Libro Patrones de Diseno plazo 15 multa 1,25 EUR --- Ordenado por multa --- DVD Refactorizacion en vivo plazo 3 multa 8,50 EUR Revista Java Magazine plazo 7 multa 1,30 EUR Libro Java Efectivo plazo 15 multa 1,25 EUR Libro Patrones de Diseno plazo 15 multa 1,25 EUR
Dos notas técnicas. Primera: Integer.compare(a, b) y Double.compare(a, b) en lugar de restas manuales; la resta de enteros puede desbordar y la de double obliga a convertir el resultado a int, perdiendo precisión. Segunda: el orden entre los dos libros de plazo 15 se mantiene tal como estaban en el array, porque Arrays.sort sobre objetos es estable. Ese detalle importará mucho en 05-09.
Y una observación que anticipa la próxima lección: los cuatro cuerpos tienen exactamente una línea de lógica útil, envuelta cada uno en cinco líneas de ceremonia. En 04-05 este método entero se quedará en cuatro líneas.
Solución 2
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.dominio.*;
import com.nexussoftware.bibliotech.servicio.*;
public class SesionDevoluciones {
public static void main(String[] args) {
GestorPrestamos gestor = new GestorPrestamos();
/*
* Esta clase anonima NO puede ser una lambda porque mantiene ESTADO
* propio entre llamadas (los campos 'devoluciones' y 'totalMultas').
* Una lambda es una implementacion sin campos: solo puede leer
* variables capturadas, que ademas deben ser efectivamente finales.
*/
gestor.setOyente(new OyenteDevolucion() {
private int devoluciones;
private double totalMultas;
{ // bloque de inicializacion: hace de constructor
System.out.println("Sesion de devoluciones iniciada.");
}
@Override
public void alDevolver(Prestamo prestamo, double multa) {
devoluciones++;
totalMultas += multa;
System.out.printf(" %s devuelto: %.2f EUR%n",
prestamo.getReferencia(), multa);
if (devoluciones % 3 == 0) {
System.out.printf(" >>> RESUMEN: %d devoluciones, %.2f EUR acumulados%n",
devoluciones, totalMultas);
}
}
});
Empleado marta = new Empleado("Marta Ruiz", "EMP-001");
Empleado diego = new Empleado("Diego Alonso", "EMP-002");
Material[] materiales = {
new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
new Libro("Patrones de Diseno", "Erich Gamma", "978-0000000002", 1994),
new Dvd("Refactorizacion en vivo", "DVD-0007", 95),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual")
};
int[] diasPorPrestamo = { 20, 16, 25, 30 };
for (int i = 0; i < materiales.length; i++) {
Empleado quien = (i % 2 == 0) ? marta : diego;
Prestamo p = new Prestamo(materiales[i], quien, 100);
gestor.procesarDevolucion(p, diasPorPrestamo[i]);
}
}
}Sesion de devoluciones iniciada. PR-0001 devuelto: 1,25 EUR PR-0002 devuelto: 0,25 EUR PR-0003 devuelto: 11,00 EUR >>> RESUMEN: 3 devoluciones, 12,50 EUR acumulados PR-0004 devuelto: 2,30 EUR
Por qué no puede ser una lambda: una lambda no tiene campos. Su cuerpo solo puede usar sus parámetros, los miembros de la clase envolvente y variables locales capturadas, y estas últimas deben ser efectivamente finales, es decir, no se pueden incrementar. Aquí devoluciones y totalMultas cambian en cada invocación y su valor debe persistir entre llamadas: eso exige estado en el objeto, y estado en el objeto exige una clase. Además, esta anónima usa un bloque de inicialización, algo que tampoco existe en una lambda. Es el caso de manual donde la clase anónima sigue siendo la herramienta correcta en Java 17.
Solución 3
package com.nexussoftware.bibliotech.servicio;
public class Auditor {
private String origen = "Auditor";
public void auditar() {
Runnable tarea = new Runnable() {
private String origen = "Tarea";
@Override
public void run() {
System.out.println("1. origen (sin this) = " + origen);
System.out.println("2. this.origen = " + this.origen);
System.out.println("3. Auditor.this.origen = " + Auditor.this.origen);
System.out.println("4. this.getClass() = " + this.getClass().getName());
System.out.println("5. Auditor.this.getClass = "
+ Auditor.this.getClass().getName());
}
};
tarea.run();
}
public static void main(String[] args) {
new Auditor().auditar();
}
}1. origen (sin this) = Tarea 2. this.origen = Tarea 3. Auditor.this.origen = Auditor 4. this.getClass() = com.nexussoftware.bibliotech.servicio.Auditor$1 5. Auditor.this.getClass = com.nexussoftware.bibliotech.servicio.Auditor
Análisis línea a línea:
origensin prefijo devuelve"Tarea". La resolución va de dentro afuera: no hay variable local con ese nombre, así que gana el campo de la anónima y el deAuditorqueda ensombrecido. Este es exactamente el mecanismo por el que un cambio inocente —añadir un campo a la anónima— redirige silenciosamente accesos que antes iban a la clase externa.this.origenes lo mismo que la línea 1, y aquí queda explícito:thises la anónima.Auditor.this.origenes la única forma de llegar al campo de la clase envolvente.this.getClass()devuelveAuditor$1, la clase que el compilador generó (04-03, apartado 9). El1indica que es la primera anónima deAuditor; si añades otra antes, esta pasará a serAuditor$2.Auditor.this.getClass()devuelveAuditor, confirmando que son dos objetos distintos.
Repite este mismo ejercicio al terminar 04-05 sustituyendo la anónima por una lambda: las cinco líneas cambian de resultado, porque en una lambda this es la clase externa. Esa diferencia es la fuente número uno de errores al migrar código antiguo de anónimas a lambdas.
Conclusión
Ya sabes declarar e instanciar una implementación en una sola expresión y sin ponerle nombre. Dominas la sintaxis diseccionada —el tipo que se implementa o extiende, los paréntesis que van al constructor de la superclase, el cuerpo entre llaves y ese punto y coma final que cierra una sentencia, no una clase— y reconoces el }); como la firma visual de una anónima en cualquier código Java que abras.
Conoces sus límites y el motivo de cada uno: no puede tener constructor porque un constructor lleva el nombre de su clase y esta no lo tiene, y por eso existe el bloque de inicialización de instancia; puede declarar métodos propios, pero son inalcanzables desde fuera porque la variable tiene el tipo de la interfaz y el tipo real no se puede nombrar; y captura variables efectivamente finales, con la misma regla y el mismo motivo que las clases locales. Y tienes muy presente la trampa que más caro se paga: dentro de una clase anónima, this es la anónima, no la clase envolvente; para llegar a esta hay que escribir Externa.this.
Has visto los tres usos que la convirtieron en el caballo de batalla de Java durante quince años: el Comparator que ordena el catálogo de BiblioTech por título, por plazo o por multa; la devolución de llamada que permite a GestorPrestamos avisar sin saber a quién, con dos oyentes distintos definidos donde se decide qué hacer; y la extensión puntual de una clase, incluida la de una clase abstracta, que hace legal new Material(...) { ... } allí donde new Material(...) está prohibido. Sabes por qué el double brace initialization es una mala práctica —cambia la clase real del objeto y rompe equals, retiene la instancia externa y genera clases de más— y qué coste tiene cada anónima en el bytecode: un .class numerado, trazas crípticas y renumeración al reordenar.
Y sobre todo te llevas la tabla de decisión: clase de primer nivel cuando el tipo tiene entidad, anidada estática para auxiliares, local para tipos de un solo algoritmo, anónima cuando hace falta estado, varios métodos abstractos o extender una clase, y lambda cuando nada de eso hace falta.
Porque esa última fila es lo que viene ahora. Repasa los comparadores que has escrito en esta lección: cada uno tiene una línea de lógica envuelta en cinco de ceremonia. El nombre de la interfaz ya lo conoce el compilador, porque lo dice el parámetro de Arrays.sort. El nombre del método también, porque Comparator solo tiene uno. Los tipos de a y b también. Todo eso sobra. En la lección 04-05, Expresiones Lambda, quitarás la ceremonia pieza a pieza hasta dejar solo lo que aporta información, aprenderás la sintaxis completa con todas sus formas abreviadas, entenderás qué es realmente una lambda —no un puntero a función, sino la implementación de una interfaz funcional—, verás por qué this se comporta al revés que aquí, y escribirás tus propias interfaces funcionales de BiblioTech para cambiar el comportamiento de una clase sin tocar su código.
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
