Clases internas y anónimas en Java
En esta lección
Hasta ahora cada clase vivía en su propio archivo. Pero Java permite declarar una clase dentro de otra, e incluso dentro de un método o en mitad de una expresión, sin darle nombre. Estas clases anidadas sirven para agrupar piezas que solo tienen sentido juntas y para esconder detalles que nadie más necesita ver. En esta lección conocerás los cuatro tipos (estáticas anidadas, internas, locales y anónimas), sabrás cuándo conviene cada uno y entenderás por qué las clases anónimas fueron el origen de las lambdas.
Para qué sirve una clase dentro de otra
Piensa en una tienda online. Un carrito tiene líneas (producto, unidades y precio). Una línea no tiene sentido fuera de un carrito, así que un archivo Linea.java suelto desordena el proyecto y sugiere que se puede usar en cualquier sitio.
Declararla dentro de Carrito deja claro que es una pieza del carrito. Las clases anidadas aportan:
- Organización: lo que va junto, está junto. Se nota en el nombre:
Carrito.Linea. - Encapsulación: una clase anidada puede ser
privatey quedar invisible para el resto del programa. - Acceso privilegiado: la clase de dentro puede usar los miembros
privatede la de fuera, y al revés.
Hay cuatro tipos:
| Tipo | Dónde se declara | ¿Necesita un objeto de la clase externa? |
|---|---|---|
| Estática anidada | Dentro de la clase, con static | No |
| Interna | Dentro de la clase, sin static | Sí |
| Local | Dentro de un método | Sí, si el método no es static |
| Anónima | En la propia expresión new, sin nombre | Sí, si se crea fuera de un contexto static |
A la clase que contiene a las demás se la llama clase externa (outer class).
Clases estáticas anidadas
Una clase estática anidada se declara dentro de otra con la palabra static. Es, a todos los efectos, una clase normal que vive dentro del “espacio de nombres” de la externa:
import java.util.ArrayList;
import java.util.List;
public class Carrito {
private final List<Linea> lineas = new ArrayList<>();
public static class Linea {
private final String producto;
private final int unidades;
private final double precio;
public Linea(String producto, int unidades, double precio) {
this.producto = producto;
this.unidades = unidades;
this.precio = precio;
}
public double subtotal() { return unidades * precio; }
@Override
public String toString() {
return unidades + " x " + producto + " = " + subtotal() + " €";
}
}
public void anadir(String producto, int unidades, double precio) {
lineas.add(new Linea(producto, unidades, precio));
}
public double total() {
double suma = 0;
for (Linea l : lineas) suma += l.subtotal();
return suma;
}
public List<Linea> getLineas() { return List.copyOf(lineas); }
}
Dentro de Carrito se escribe simplemente Linea. Desde fuera, con el nombre completo Carrito.Linea:
Carrito c = new Carrito();
c.anadir("Camiseta", 2, 15.0);
c.anadir("Gorra", 1, 12.5);
for (Carrito.Linea l : c.getLineas()) {
System.out.println(l);
}
System.out.println("Total: " + c.total() + " €");
// 2 x Camiseta = 30.0 €
// 1 x Gorra = 12.5 €
// Total: 42.5 €
Carrito.Linea suelta = new Carrito.Linea("Calcetines", 3, 4.0);
System.out.println(suelta); // 3 x Calcetines = 12.0 €
Como es static, una Linea no está ligada a ningún carrito concreto. Por eso no puede usar los atributos normales de Carrito:
public static class Linea {
void mostrar() { System.out.println(lineas); }
// error: non-static variable lineas cannot be referenced from a static context
}
Es la misma regla que ya conoces de los métodos static: lo estático no tiene un this de la clase externa.
Nota: si
Lineafueraprivate, nadie fuera deCarritopodría usarla. Aquí espublicporquegetLineas()la devuelve; si solo la usara el carrito por dentro,privatesería lo correcto.
Un uso muy común: el patrón Builder
Cuando un objeto tiene muchos datos opcionales, un constructor con siete parámetros es ilegible: new Pizza("familiar", true, false, ...). El patrón Builder usa una clase estática anidada que va recogiendo los datos paso a paso y al final crea el objeto:
import java.util.ArrayList;
import java.util.List;
public class Pizza {
private final String tamano;
private final List<String> ingredientes;
private Pizza(Builder b) { // privado: solo el Builder crea pizzas
this.tamano = b.tamano;
this.ingredientes = List.copyOf(b.ingredientes);
}
@Override
public String toString() { return "Pizza " + tamano + " con " + ingredientes; }
public static class Builder {
private String tamano = "mediana";
private final List<String> ingredientes = new ArrayList<>();
public Builder tamano(String t) { tamano = t; return this; }
public Builder con(String ingrediente) { ingredientes.add(ingrediente); return this; }
public Pizza build() { return new Pizza(this); }
}
}
Pizza p = new Pizza.Builder()
.tamano("familiar")
.con("tomate").con("mozzarella").con("albahaca")
.build();
System.out.println(p); // Pizza familiar con [tomate, mozzarella, albahaca]
System.out.println(new Pizza.Builder().con("jamón").build()); // Pizza mediana con [jamón]
Cada método del Builder devuelve this, y por eso se pueden encadenar. Fíjate en que el constructor de Pizza lee b.tamano, un atributo private del Builder: entre una clase y sus anidadas no hay secretos.
Clases internas
Si quitas el static, tienes una clase interna (inner class). La diferencia es importante: cada objeto de la clase interna está ligado a un objeto concreto de la clase externa y puede usar todos sus atributos, incluidos los privados.
El uso típico es crear un “ayudante” que trabaja sobre los datos de su objeto externo. Por ejemplo, una lista de reproducción que se puede recorrer con un for-each. Para eso tiene que implementar Iterable y devolver un iterador, un objeto que sabe ir dando los elementos uno a uno:
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Playlist implements Iterable<String> {
private final String nombre;
private final String[] canciones = new String[50];
private int total = 0;
public Playlist(String nombre) { this.nombre = nombre; }
public void anadir(String cancion) {
canciones[total++] = cancion;
}
// Clase interna: cada Recorrido pertenece a una Playlist concreta
private class Recorrido implements Iterator<String> {
private int posicion = 0;
@Override
public boolean hasNext() {
return posicion < total; // atributo privado de Playlist
}
@Override
public String next() {
if (!hasNext()) throw new NoSuchElementException();
return canciones[posicion++]; // otro atributo privado de Playlist
}
}
@Override
public Iterator<String> iterator() {
return new Recorrido();
}
}
Playlist rock = new Playlist("Rock nacional");
rock.anadir("Insurrección");
rock.anadir("Maldito duende");
rock.anadir("La chica de ayer");
for (String cancion : rock) {
System.out.println(cancion);
}
// Insurrección
// Maldito duende
// La chica de ayer
Recorrido lee total y canciones como si fueran suyos. Eso es posible porque, al crearse con new Recorrido() dentro de iterator(), guarda una referencia oculta a la Playlist que lo creó. Si hubiera dos playlists, cada recorrido leería las canciones de la suya. Además, Recorrido es private: quien usa la playlist ni sabe que existe.
Crear objetos internos desde fuera y Externa.this
Como un objeto interno necesita uno externo, no basta con new. Si añadimos a Playlist una clase interna pública:
public class Reproductor {
private String nombre = "Reproductor";
public void info() {
System.out.println(nombre); // el del Reproductor
System.out.println(Playlist.this.nombre); // el de la Playlist
}
}
Desde fuera hay que crearla a partir de un objeto de Playlist, con la sintaxis objeto.new Interna():
Playlist.Reproductor r = rock.new Reproductor();
r.info();
// Reproductor
// Rock nacional
Playlist.Reproductor mal = new Playlist.Reproductor();
// error: an enclosing instance that contains Playlist.Reproductor is required
Dentro de la clase interna, this es el objeto interno. Para referirte al objeto externo (por ejemplo, cuando un atributo tiene el mismo nombre en las dos) se escribe Externa.this, como en Playlist.this.nombre.
Consejo: si tu clase anidada no usa nada del objeto externo, declárala
static. Una clase interna guarda siempre una referencia a su objeto externo; eso gasta memoria y puede impedir que el objeto externo se libere mientras el interno siga vivo.
Clases locales
Una clase local se declara dentro de un método y solo existe ahí. Es útil cuando necesitas una pequeña estructura auxiliar para un único cálculo:
static void informe(List<String> nombres, int minimo) {
class Estadistica {
int cortos, largos;
void contar(String n) {
if (n.length() < minimo) cortos++; else largos++;
}
}
Estadistica e = new Estadistica();
for (String n : nombres) e.contar(n);
System.out.println("Cortos: " + e.cortos + ", largos: " + e.largos);
}
informe(List.of("Ana", "Bartolomé", "Eva", "Guillermo", "Iker"), 5);
// Cortos: 3, largos: 2
La clase Estadistica usa minimo, que es un parámetro del método. Las clases locales (y las anónimas) pueden capturar variables locales, pero solo si son final o efectivamente finales: que nunca se reasignan (lo viste en static y final). Si después de declarar minimo hicieras minimo = 3;, no compilaría.
En la práctica, las clases locales se ven poco. Si solo necesitas agrupar unos datos dentro de un método, un record local suele ser más cómodo.
Clases anónimas
Una clase anónima se declara y se instancia a la vez, en la misma expresión, y no tiene nombre. Sirve para crear un único objeto que implementa una interfaz o extiende una clase, cuando no merece la pena escribir una clase aparte.
Partimos de una interfaz para los descuentos de la tienda:
interface Descuento {
double aplicar(double precio);
}
Sin clase anónima, tendrías que escribir class DescuentoRebajas implements Descuento { ... } en otro sitio. Con ella:
Descuento rebajas = new Descuento() {
@Override
public double aplicar(double precio) {
return precio * 0.8;
}
};
System.out.println(rebajas.aplicar(50)); // 40.0
Lee new Descuento() { ... } como “crea un objeto de una clase sin nombre que implementa Descuento con este cuerpo”. No estás haciendo new de una interfaz (eso es imposible): estás creando un objeto de una clase nueva cuyo código va entre las llaves. Fíjate en el ; final, porque todo es una única instrucción de asignación.
Igual que las locales, las anónimas pueden capturar variables efectivamente finales:
double porcentaje = 15;
Descuento socio = new Descuento() {
@Override
public double aplicar(double precio) {
return precio - precio * porcentaje / 100;
}
};
System.out.println(socio.aplicar(40)); // 34.0
Si porcentaje se reasignara en algún momento, el compilador diría: local variables referenced from an inner class must be final or effectively final.
Anónimas que extienden una clase abstracta
Una clase anónima también puede heredar de una clase abstracta, implementar varios métodos y tener sus propios atributos:
abstract class Notificacion {
abstract void enviar(String texto);
void enviarVarias(String... textos) {
for (String t : textos) enviar(t);
}
}
Notificacion porConsola = new Notificacion() {
private int enviadas = 0;
@Override
void enviar(String texto) {
enviadas++;
System.out.println("[" + enviadas + "] " + texto);
}
};
porConsola.enviarVarias("Pedido recibido", "Pedido enviado");
// [1] Pedido recibido
// [2] Pedido enviado
Una clase anónima no puede tener constructor (no tiene nombre con el que llamarlo), pero sí atributos inicializados, como enviadas.
De las clases anónimas a las lambdas
Antes de Java 8, las clases anónimas eran la forma habitual de pasar “un trozo de comportamiento” a un método, por ejemplo para ordenar una lista:
List<String> productos = new ArrayList<>(List.of("Mochila", "Gorra", "Cantimplora"));
productos.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
});
System.out.println(productos); // [Gorra, Mochila, Cantimplora]
Seis líneas para decir “compara por longitud”. Cuando la interfaz tiene un solo método abstracto (una interfaz funcional, como Comparator o nuestro Descuento), Java 8 permite escribir lo mismo con una lambda:
productos.sort((a, b) -> Integer.compare(b.length(), a.length()));
System.out.println(productos); // [Cantimplora, Mochila, Gorra]
Descuento lambda = precio -> precio - precio * porcentaje / 100;
System.out.println(lambda.aplicar(40)); // 34.0
La lambda es, conceptualmente, una clase anónima reducida a lo esencial: los parámetros y el cuerpo. La sintaxis completa la tienes en lambdas y streams. Pero no son exactamente lo mismo:
| Clase anónima | Lambda | |
|---|---|---|
| Con qué funciona | Interfaces con cualquier número de métodos y clases (también abstractas) | Solo interfaces funcionales (un método abstracto) |
| Atributos propios | Sí | No |
Qué es this dentro | El objeto anónimo | El objeto de la clase que la contiene |
| Variables capturadas | Efectivamente finales | Efectivamente finales |
Archivo .class | Genera uno propio (Tienda$1.class) | No genera archivo propio |
La diferencia de this se ve en este ejemplo:
public class Tienda {
private String nombre = "Tienda Norte";
void probar() {
Runnable anonima = new Runnable() {
@Override
public void run() {
System.out.println(this.getClass().getName());
}
};
Runnable lambda = () -> System.out.println(this.nombre);
anonima.run(); // Tienda$1
lambda.run(); // Tienda Norte
}
}
En la anónima, this es el objeto anónimo, cuya clase el compilador ha llamado Tienda$1. En la lambda, this sigue siendo la Tienda.
¿Cuándo sigue teniendo sentido una clase anónima? Cuando la interfaz tiene más de un método, cuando extiendes una clase (abstracta o no), o cuando el objeto necesita guardar estado propio, como el contador de Notificacion. Para todo lo demás, usa una lambda o una referencia a método (Comparator.comparingInt(String::length)).
Qué genera el compilador
La máquina virtual no conoce las clases anidadas: el compilador crea un .class aparte para cada una, con un $ en el nombre. Verás archivos como Carrito$Linea.class, Playlist$Recorrido.class o Tienda$1.class (las anónimas se numeran por orden). Conviene reconocerlos cuando aparecen en un error o en la carpeta de compilación.
¿Cuál uso?
| Situación | Elección |
|---|---|
| Una clase auxiliar que no necesita datos del objeto externo (línea de carrito, Builder) | Estática anidada |
| Un ayudante que trabaja con los datos de un objeto concreto (iterador) | Interna |
| Una estructura auxiliar para un único método | Record local o clase local |
| Implementar una interfaz funcional una sola vez | Lambda |
| Implementar una interfaz de varios métodos o extender una clase una sola vez | Anónima |
| Una clase que se usa en muchos sitios | Clase normal en su propio archivo |
Errores frecuentes
- Crear un objeto interno desde
mainconnew Interna(). Da non-static variable this cannot be referenced from a static context: falta el objeto externo. Haz la clasestaticsi no lo necesita, o usaexterno.new Interna(). - Usar atributos de la clase externa desde una estática anidada. No tiene objeto externo. Pásale los datos por parámetro o quita el
staticsi de verdad los necesita. - Modificar una variable local que usa una anónima o una lambda. Solo pueden capturar variables efectivamente finales. Usa una variable nueva o un atributo.
- Olvidar el
;después de una clase anónima. La anónima forma parte de una instrucción, que termina en punto y coma. - Confundir
thisen una clase anónima. Es el objeto anónimo, no el de la clase que lo contiene. UsaExterna.thiso una lambda. - Hacer interna una clase que no usa nada del objeto externo. Gasta memoria sin necesidad. Por defecto, anidada
static.
Resumen
| Tipo | Sintaxis | Idea clave |
|---|---|---|
| Estática anidada | static class Linea { } dentro de Carrito | Clase normal agrupada; se usa como Carrito.Linea |
| Interna | class Recorrido { } dentro de Playlist | Ligada a un objeto externo; ve sus atributos privados |
| Crear interna desde fuera | rock.new Reproductor() | Necesita un objeto de la clase externa |
Externa.this | Playlist.this.nombre | Referencia al objeto externo |
| Local | class Estadistica { } dentro de un método | Solo existe en ese método |
| Anónima | new Descuento() { ... }; | Se declara y se crea a la vez, sin nombre |
| Lambda | precio -> precio * 0.8 | Sustituye a la anónima en interfaces funcionales |
En la siguiente lección verás un tipo especial de clase para representar conjuntos fijos de valores: los enums.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.