Clases internas en Java
Define clases internas no estáticas en Java que mantienen una referencia implícita a una instancia de la clase contenedora.
Una clase interna es una clase anidada no estática — declarada dentro de otra clase sin el modificador static. Su característica principal: cada instancia de la clase interna está ligada a una instancia de la clase contenedora y lleva una referencia implícita a ella. Desde dentro de la clase interna, puedes leer y escribir los campos de la instancia exterior y llamar a sus métodos como si fueran propios.
Esto convierte a las clases internas en la herramienta adecuada cuando una clase pequeña y secundaria necesita participar de forma íntima en el estado de otra clase — el caso más famoso son los iteradores que recorren los datos internos de su contenedor.
Esta página explica cómo declarar una clase interna, cómo crear una (siempre necesitas una instancia exterior), el calificador Outer.this, el caso de uso canónico del iterador, el problema de pérdida de memoria que conlleva la referencia implícita, y cómo decidir entre una clase interna y una clase anidada static.
Declarar una clase interna
Omite static en la declaración de una clase anidada:
public class Outer {
private int x = 1;
class Inner { // no static — inner class
int get() { return x; } // reads Outer's x directly
}
}Inner no tiene campos propios y aun así get() devuelve 1. El x sin calificador se resuelve como Outer.this.x a través de la referencia implícita.
Crear una instancia
Dado que cada instancia de la clase interna está ligada a una exterior, necesitas una instancia exterior para crear una interior. Hay dos formas:
Outer o = new Outer();
Outer.Inner i = o.new Inner(); // bind explicitly to o…o desde dentro de un método no estático de Outer:
public class Outer {
void demo() {
Inner i = new Inner(); // implicitly bound to this
}
}La sintaxis o.new Inner() es poco común y sorprendente — la mayoría del código crea instancias de clases internas desde dentro de los propios métodos de la clase exterior, donde el vínculo es implícito.
Outer.this — acceder más allá de una colisión de nombres
Cuando la clase interna declara un campo con el mismo nombre que uno de la clase exterior, el interno lo oculta. Para acceder al exterior, califícalo con Outer.this:
public class Outer {
int x = 1;
class Inner {
int x = 2;
void demo() {
System.out.println(x); // 2 — Inner's x
System.out.println(this.x); // 2 — Inner's x
System.out.println(Outer.this.x); // 1 — Outer's x
}
}
}this en una clase interna hace referencia a la instancia interna; Outer.this hace referencia a la instancia exterior que la contiene.
El caso de uso canónico — iteradores
La razón clásica para usar una clase interna es implementar un iterador sobre los datos privados internos de un contenedor:
public class IntList {
private int[] data;
private int size;
// ... constructors, add, ...
public Iterator<Integer> iterator() {
return new InnerIterator();
}
private class InnerIterator implements Iterator<Integer> {
private int i = 0;
public boolean hasNext() { return i < size; }
public Integer next() { return data[i++]; }
}
}InnerIterator accede directamente a data y size a través de la referencia exterior implícita. No se necesita ningún setter ni accessor — la clase interna forma parte de la implementación de IntList.
Observa private class InnerIterator. Desde fuera, los llamadores ven la interfaz pública Iterator<Integer>; no saben que InnerIterator existe. Ese es el beneficio de encapsulación que aporta el anidamiento.
Las clases internas mantienen una referencia — y mantienen viva la exterior
Un problema sutil. Mientras una instancia de la clase interna sea alcanzable, la JVM no puede recolectar como basura la instancia exterior a la que está vinculada. Devolver una instancia de clase interna a código de larga vida (por ejemplo, instalar un listener en algún lugar) puede mantener grafos de objetos enteros vivos más tiempo del esperado.
public class Window {
Listener installListener() {
return new Listener(); // returned to whoever calls this
}
class Listener { ... } // holds a Window reference forever
}Si installListener() se almacena en un registro estático, el Window permanece vivo hasta que se vacíe el registro. La solución habitual es hacer que la clase anidada sea static y pasar los datos necesarios de forma explícita, rompiendo la referencia implícita.
Esta es la razón más común por la que los equipos optan por static clases anidadas de forma predeterminada y solo cambian a clases internas cuando necesitan específicamente el vínculo.
Miembros estáticos en clases internas
Durante la mayor parte de la historia de Java, las clases internas no podían declarar miembros static (campos, métodos o clases anidadas static). Java 16 relajó esta restricción — las clases internas ahora pueden tener miembros estáticos. Aun así, si te encuentras queriendo usarlos, suele ser una señal de que la clase debería ser static en sí misma.
static vs interna — la elección
Una regla útil: hazla static a menos que necesites activamente la referencia exterior.
- Clase anidada estática: más simple, más ligera, no mantiene viva la exterior.
- Clase interna: azúcar sintáctico conveniente para acceder a
outerInstance.fieldcuando la relación es genuina.
Si lo único que haces es Outer.this.field, simplemente toma un Outer como parámetro del constructor y haz la clase estática.
Un ejemplo elaborado
Qué sigue
El siguiente tipo de clase anidada es la versión en línea y de un solo uso: las clases anónimas, utilizadas para situaciones rápidas de subclasificar e instanciar en una sola expresión. Continúa con clases anónimas.