Cuatro tipos de clases anidadas
Java permite declarar una clase dentro de otra clase o método. Según dónde se declare y si captura la instancia externa, se clasifica en cuatro tipos:
| Tipo | Dónde se declara | Captura instancia externa | Uso típico |
|------|------------------|---------------------------|------------|
| Static nested class | Dentro de una clase, con static | No | Builders, iteradores, helpers |
| Inner class | Dentro de una clase, sin static | Sí | Ligada a su contenedor |
| Local class | Dentro de un método | Sí (si accede a variables) | Ayudas muy específicas |
| Anonymous class | En una expresión | Sí (si accede) | Callbacks simples (pre-lambda) |
Static nested class
Una clase declarada dentro de otra con el modificador static. No guarda referencia implícita a una instancia de la clase contenedora: es como una clase top-level, pero vive dentro del namespace de la externa.
public class LinkedList<E> {
private Nodo<E> primero;
// Static nested: no depende de una LinkedList concreta
private static class Nodo<E> {
E valor;
Nodo<E> siguiente;
Nodo(E valor, Nodo<E> siguiente) {
this.valor = valor;
this.siguiente = siguiente;
}
}
}
Ventajas:
- No consumen memoria extra (no hay referencia oculta al exterior).
- Pueden instanciarse sin tener instancia externa.
- Conceptualmente son clases helper con visibilidad limitada.
Caso clásico: Builder
public final class Persona {
private final String nombre;
private final int edad;
private final String email;
private Persona(Builder b) {
this.nombre = b.nombre;
this.edad = b.edad;
this.email = b.email;
}
public static class Builder {
private String nombre;
private int edad;
private String email;
public Builder nombre(String n) { this.nombre = n; return this; }
public Builder edad(int e) { this.edad = e; return this; }
public Builder email(String e) { this.email = e; return this; }
public Persona build() {
return new Persona(this);
}
}
}
// Uso
Persona p = new Persona.Builder()
.nombre("Ana")
.edad(30)
.email("ana@example.com")
.build();
Inner class (non-static nested)
Una inner class captura una referencia a la instancia externa. Esa referencia se pasa automáticamente al constructor. La inner class puede acceder a los miembros privados del exterior.
public class Coche {
private String modelo;
private int kilometros;
public class Motor {
public String estado() {
// Puede acceder directamente a modelo y kilometros del Coche externo
return modelo + " con " + kilometros + " km";
}
}
}
Para crear una inner class necesitas una instancia del exterior:
Coche c = new Coche();
Coche.Motor m = c.new Motor();
System.out.println(m.estado());
Referenciar this externo
Dentro de una inner class, this es la instancia de la inner. Para acceder al exterior usa TipoExterno.this:
public class Contenedor {
private int valor = 10;
public class Hijo {
private int valor = 20;
public void imprimir() {
System.out.println(this.valor); // 20 (Hijo)
System.out.println(Contenedor.this.valor); // 10 (Contenedor)
}
}
}
Cuándo usar inner class
- Cuando la clase tiene sentido solo en relación a una instancia específica del exterior.
- Iteradores internos que necesitan acceso al estado del contenedor.
- Listeners ligados a una instancia de componente UI.
Advertencia: la referencia oculta al exterior puede causar memory leaks si la inner sobrevive a su exterior (p.ej. registrada como callback global).
Local class
Una clase declarada dentro de un método. Solo es visible dentro del bloque donde se define. Puede acceder a variables locales del método si son efectivamente finales.
public List<String> procesar(List<Cliente> clientes) {
String timestamp = Instant.now().toString(); // effectively final
// Local class dentro del método
class Reportador {
String reporte(Cliente c) {
return "[" + timestamp + "] " + c.nombre();
}
}
Reportador r = new Reportador();
return clientes.stream().map(r::reporte).toList();
}
Uso muy puntual. Hoy en día suele sustituirse por métodos privados o lambdas.
Anonymous class (clase anónima)
Una expresión que crea una clase sin nombre y una instancia en el mismo sitio. Era la forma previa a las lambdas de pasar comportamiento:
// Pre-lambda: clase anónima
List<String> lista = new ArrayList<>();
Collections.sort(lista, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
// Con lambda (Java 8+)
Collections.sort(lista, (a, b) -> a.length() - b.length());
Las lambdas son más concisas y eficientes para interfaces funcionales. Las clases anónimas siguen siendo útiles cuando:
- Necesitas implementar varios métodos (no solo una interfaz funcional).
- Necesitas estado interno (la lambda no tiene campos propios).
- Quieres un tipo concreto (clase abstracta con métodos implementados).
// Anónima sigue siendo necesaria aquí: HashMap no es una interfaz funcional
Map<String, Integer> m = new HashMap<>() {{
put("uno", 1);
put("dos", 2);
put("tres", 3);
}};
// Nota: este patrón con double-brace initialization crea una inner class, úsalo con cuidado
Anónima extendiendo una clase
Thread t = new Thread("miHilo") {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("iter " + i);
}
}
};
t.start();
Visibilidad y modificadores
- Static nested e inner classes pueden ser
public,protected,private, o package-private. - Pueden ser
finaloabstract. - Una inner class no puede tener miembros
static(exceptostatic finalde tipos primitivos oStringcon valor constante conocido en compilación: regla relajada en Java 16+). - Static nested class puede tener miembros estáticos.
Cuándo usar cada una (guía práctica)
| Necesitas... | Usa |
|--------------|-----|
| Una clase helper que solo tiene sentido dentro de otra (builder, nodo, iterador) | Static nested class |
| Una clase ligada lógicamente a una instancia externa (listener, componente de una UI) | Inner class (con cuidado) |
| Implementar comportamiento una sola vez en un método | Lambda (preferido) |
| Implementar comportamiento complejo con estado o múltiples métodos en un método | Clase anónima o local |
| Un thread one-off | Lambda con Runnable o clase anónima extendiendo Thread |
Resumen
Las clases anidadas permiten estructurar código con visibilidad controlada y acoplamiento deliberado. En código moderno:
- Static nested: muy usadas en builders, nodos internos, iteradores.
- Inner: menos común; cuidado con leaks.
- Locales: casi reemplazadas por métodos privados + lambdas.
- Anónimas: casi reemplazadas por lambdas (salvo casos especiales).
Saber cuál elegir es señal de madurez en diseño OOP.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Java es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Java
Explora más contenido relacionado con Java y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Diferenciar clase estática anidada, inner class, local y anónima. Usar inner classes para ligar comportamiento a una instancia externa. Declarar clases locales dentro de métodos. Reconocer cuándo una clase anónima es preferible a una lambda. Comprender la variable TipoExterno.this desde una inner class. Aplicar el patrón Builder con static nested class.