1. ¿Qué es una clase interna (inner class)?
En Java, una clase puede declararse no solo a nivel superior (en un archivo), sino también dentro de otra clase. Tal clase se llama anidada (nested class). Si dicha clase se declara sin el modificador static, entonces se denomina interna (o non-static inner class, o simplemente inner class).
Una clase interna es una clase declarada dentro de otra clase y vinculada a la instancia de esa clase externa. Puede acceder a todos los campos y métodos de la clase externa, incluso si son private. Es como si el objeto tuviera su «ayudante secreto», ¡al que se le permite todo!
Es, más o menos, como una casa con habitaciones. La casa es la clase externa y la habitación es la clase interna. La habitación no puede existir sin la casa, pero tiene acceso a sus recursos: luz, calefacción, muebles. Si la casa desaparece, también lo hará la habitación. ¡Así funcionan las clases internas en Java!
¿Para qué sirven las clases internas?
- Relación lógica: cuando una clase solo se necesita para trabajar con otra clase y no tiene sentido fuera de ella.
- Encapsulación: permite ocultar detalles de implementación sin ensuciar el espacio de nombres del paquete.
- Acceso a miembros privados: la clase interna puede acceder a los campos y métodos privados de la clase externa.
- Compacidad: reduce la cantidad de clases «ruidosas» a nivel de paquete.
Sintaxis de declaración de una clase interna
Declarar una clase interna es muy sencillo: se declara dentro del cuerpo de otra clase, sin el modificador static.
class Outer {
// campos y métodos de la clase externa
class Inner {
// campos y métodos de la clase interna
void printHello() {
System.out.println("Hello from Inner!");
}
}
}
Aquí Inner es una clase interna con respecto a Outer.
Imagen: visualización
Outer
│
├─ campos/métodos
│
└─ Inner (clase interna)
└─ sus propios campos/métodos
2. Cómo crear una instancia de una clase interna
¡Aquí se esconde una de las trampas más comunes para principiantes! Una instancia de una clase interna siempre está vinculada a un objeto concreto de la clase externa.
Ejemplo:
Outer outer = new Outer(); // creamos el objeto de la clase externa
Outer.Inner inner = outer.new Inner(); // creamos la clase interna a través del objeto externo
inner.printHello(); // Hello from Inner!
Intentar hacer simplemente new Inner() provocará un error de compilación, porque Java no sabe con qué objeto de la clase externa debe vincular esa clase interna.
¿Por qué?
Porque la clase interna puede acceder a los campos y métodos de la clase externa. Por eso debe «saber» con qué objeto externo está trabajando.
3. Ejemplo de uso de una clase interna
Veamos un ejemplo donde una clase interna resulta realmente útil.
Ejemplo 1: Modelo «Mochila y objetos»
Supongamos que tenemos la clase Backpack, que puede almacenar objetos (Item). Pero queremos que la clase Item esté disponible solo dentro de Backpack, porque fuera de la mochila los objetos no nos interesan.
public class Backpack {
private String owner;
public Backpack(String owner) {
this.owner = owner;
}
// Clase interna: ¡el objeto solo puede existir dentro de la mochila!
class Item {
private String name;
public Item(String name) {
this.name = name;
}
public void printInfo() {
// ¡Magia! Vemos el campo privado de la clase externa
System.out.println(owner + " tiene el objeto: " + name);
}
}
}
Uso:
Backpack bp = new Backpack("Vasya");
Backpack.Item item = bp.new Item("Libro de Java");
item.printInfo(); // Vasya tiene el objeto: Libro de Java
Fíjate: Item puede acceder al campo owner, ¡aunque sea private!
Ejemplo 2: Iterador para tu propia colección
En Java, las colecciones a menudo implementan un iterador como clase interna. Hagamos nuestra colección sencilla con una clase interna-iterador.
public class IntList {
private int[] data = new int[10];
private int size = 0;
public void add(int value) {
data[size++] = value;
}
// Clase interna: ¡iterador!
class Iterator {
private int index = 0;
public boolean hasNext() {
return index < size;
}
public int next() {
return data[index++];
}
}
}
Uso:
IntList list = new IntList();
list.add(10);
list.add(20);
IntList.Iterator it = list.new Iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
4. Particularidades de las clases internas
Acceso a los miembros de la clase externa
Una clase interna puede acceder a cualquier campo o método de la clase externa (incluso a los private). Es útil, pero requiere cuidado: si la clase externa cambia, la interna puede «romperse» de repente.
class Outer {
private int secret = 42;
class Inner {
void showSecret() {
System.out.println("Secreto: " + secret);
}
}
}
Una clase interna no puede contener miembros estáticos
Una clase interna (non-static inner) no puede contener campos o métodos estáticos, salvo constantes static final (por ejemplo, public static final int MY_CONST = 123;). Esto se debe a que la clase interna siempre está «ligada» a una instancia de la clase externa.
Si necesitas declarar una clase anidada estática, usa el modificador static (hablaremos de ello en la siguiente lección).
Visibilidad de la clase interna
Una clase interna puede ser private, protected, public o tener visibilidad de nivel de paquete (package-private).
public class Outer {
private class Inner { /* ... */ }
}
5. Detalles útiles
Cuándo usar clases internas
Las clases internas no son para «decoración», sino para organizar el código lógicamente. Úsalas cuando:
- La clase se necesita solo en un lugar (por ejemplo, un iterador auxiliar, un manejador, parte de una estructura compleja).
- La clase está estrechamente vinculada con la clase externa y no tiene sentido fuera de ella.
- Quieres ocultar detalles de implementación a otras clases del paquete.
¡No uses clases internas solo por moda! A veces es mejor llevar la clase al nivel superior si puede ser útil en otros lugares.
La clase interna y this
Dentro de una clase interna puedes acceder a los campos y métodos de la clase externa mediante OuterClassName.this.
class Car {
private String model = "Tesla";
class Engine {
void printModel() {
// Accedemos explícitamente al objeto externo
System.out.println("Modelo: " + Car.this.model);
}
}
}
Esta sintaxis es especialmente útil si la clase interna y la externa tienen campos con los mismos nombres.
Cuándo NO conviene usar clases internas
No las uses si:
- La clase interna no accede a campos/métodos de la clase externa
- La clase puede ser útil en otras partes del programa
- La clase externa se vuelve demasiado grande y compleja
En tales casos es mejor:
- Hacer la clase anidada estática (static class)
- Mover la clase a un archivo separado
- Usar métodos normales en lugar de toda una clase
6. Errores típicos al trabajar con clases internas
Error n.º 1: intentar crear una clase interna sin un objeto de la clase externa.
Si escribes new Inner(), el compilador dará el error: "No enclosing instance of type Outer is accessible". Crea siempre la clase interna a través del objeto de la clase externa: outer.new Inner().
Error n.º 2: intentar declarar campos o métodos estáticos en una clase interna.
Una clase interna no puede contener miembros estáticos (salvo constantes). Si lo necesitas, utiliza una clase anidada estática; es decir, declara la clase con el modificador static.
Error n.º 3: uso excesivo de clases internas.
Si la clase interna no utiliza campos/métodos de la clase externa, probablemente deba ser static o incluso extraerse al exterior. No crees clases internas «por si acaso».
Error n.º 4: confusión al acceder a los campos de la clase externa.
Si la clase interna y la externa tienen campos con los mismos nombres, utiliza OuterClassName.this.field para acceder explícitamente al campo de la clase externa.
GO TO FULL VERSION