Ir al contenido principal

Clases anidadas y clases internas

Introducción

En Java, además de las clases convencionales, existe la posibilidad de definir clases dentro de otras clases. Estas estructuras, conocidas como clases anidadas (nested classes), permiten agrupar lógicamente clases que solo se utilizan en un lugar, aumentar la encapsulación y crear código más legible y mantenible.

Las clases anidadas son una característica poderosa del lenguaje que nos permite organizar mejor nuestro código, especialmente cuando una clase está estrechamente relacionada con otra y no tiene sentido que exista de forma independiente. A lo largo de este artículo, exploraremos los diferentes tipos de clases anidadas en Java, sus características y cuándo debemos utilizar cada una de ellas.

Tipos de clases anidadas

En Java, existen dos categorías principales de clases anidadas:

  1. Clases anidadas estáticas (static nested classes)
  2. Clases internas (inner classes)
    • Clases internas normales (o miembro)
    • Clases internas locales
    • Clases internas anónimas

Cada tipo tiene características específicas que las hacen adecuadas para diferentes situaciones.

Clases anidadas estáticas

Definición y características

Una clase anidada estática es una clase definida dentro de otra clase y declarada con el modificador static. A diferencia de las clases internas, una clase anidada estática no tiene acceso a los miembros de instancia de su clase contenedora.

Características principales:

  • Se declaran con la palabra clave static
  • Pueden acceder a miembros estáticos de la clase externa
  • No pueden acceder directamente a miembros de instancia de la clase externa
  • Pueden ser instanciadas sin necesidad de una instancia de la clase externa
  • Pueden contener miembros estáticos y no estáticos

Sintaxis básica

public class ClaseExterna {
    private static String atributoEstatico = "Atributo estático";
    private String atributoInstancia = "Atributo de instancia";
    
    // Clase anidada estática
    public static class ClaseAnidadaEstatica {
        public void mostrarInfo() {
            // Puede acceder a miembros estáticos de la clase externa
            System.out.println(atributoEstatico);
            
            // No puede acceder directamente a miembros de instancia
            // System.out.println(atributoInstancia); // Error de compilación
        }
    }
    
    public void metodoExterno() {
        // Crear instancia de la clase anidada estática
        ClaseAnidadaEstatica anidada = new ClaseAnidadaEstatica();
        anidada.mostrarInfo();
    }
}

Ejemplo práctico

Las clases anidadas estáticas son útiles cuando necesitamos una clase auxiliar que esté lógicamente agrupada con otra clase, pero no necesita acceder a sus miembros de instancia:

public class Calculadora {
    // Clase anidada estática para operaciones matemáticas avanzadas
    public static class Operaciones {
        public static double raizCuadrada(double numero) {
            return Math.sqrt(numero);
        }
        
        public static double potencia(double base, double exponente) {
            return Math.pow(base, exponente);
        }
        
        public static int factorial(int n) {
            if (n <= 1) return 1;
            return n * factorial(n - 1);
        }
    }
    
    public double sumar(double a, double b) {
        return a + b;
    }
    
    public double restar(double a, double b) {
        return a - b;
    }
    
    public double multiplicar(double a, double b) {
        return a * b;
    }
    
    public double dividir(double a, double b) {
        if (b == 0) throw new ArithmeticException("División por cero");
        return a / b;
    }
    
    // Uso de la clase anidada estática
    public double calcularHipotenusa(double cateto1, double cateto2) {
        double sumaCuadrados = Operaciones.potencia(cateto1, 2) + Operaciones.potencia(cateto2, 2);
        return Operaciones.raizCuadrada(sumaCuadrados);
    }
}

// Clase para probar
public class PruebaCalculadora {
    public static void main(String[] args) {
        // Usar la calculadora normal
        Calculadora calc = new Calculadora();
        System.out.println("Suma: " + calc.sumar(5, 3));
        
        // Usar directamente la clase anidada estática
        double raiz = Calculadora.Operaciones.raizCuadrada(16);
        System.out.println("Raíz cuadrada de 16: " + raiz);
        
        int fact = Calculadora.Operaciones.factorial(5);
        System.out.println("Factorial de 5: " + fact);
        
        // Usar método que utiliza la clase anidada
        double hipotenusa = calc.calcularHipotenusa(3, 4);
        System.out.println("Hipotenusa del triángulo 3-4: " + hipotenusa);
    }
}

En este ejemplo, Operaciones es una clase anidada estática que proporciona funcionalidades auxiliares relacionadas con la calculadora, pero que no necesitan acceder a sus miembros de instancia.

Clases internas (miembro)

Definición y características

Una clase interna (o clase interna miembro) es una clase no estática definida dentro de otra clase. A diferencia de las clases anidadas estáticas, las clases internas tienen acceso a todos los miembros (incluidos los privados) de su clase contenedora.

Características principales:

  • No se declaran con la palabra clave static
  • Pueden acceder a todos los miembros de la clase externa (incluso privados)
  • Mantienen una referencia implícita a la instancia de la clase externa que la contiene
  • No pueden contener miembros estáticos
  • Requieren una instancia de la clase externa para ser instanciadas

Sintaxis básica

public class ClaseExterna {
    private String atributoExterno = "Atributo externo";
    
    // Clase interna
    public class ClaseInterna {
        private String atributoInterno = "Atributo interno";
        
        public void mostrarInfo() {
            // Puede acceder directamente a miembros de la clase externa
            System.out.println(atributoExterno);
            System.out.println(atributoInterno);
        }
    }
    
    public void crearClaseInterna() {
        // Crear instancia de la clase interna
        ClaseInterna interna = new ClaseInterna();
        interna.mostrarInfo();
    }
}

Ejemplo práctico

Las clases internas son útiles cuando necesitamos una clase que esté estrechamente relacionada con otra y necesita acceder a sus miembros:

public class ListaEnlazada<T> {
    private Nodo<T> cabeza;
    private int tamaño;
    
    public ListaEnlazada() {
        this.cabeza = null;
        this.tamaño = 0;
    }
    
    // Clase interna para representar nodos de la lista
    public class Nodo<E> {
        private E dato;
        private Nodo<E> siguiente;
        
        public Nodo(E dato) {
            this.dato = dato;
            this.siguiente = null;
        }
        
        public E getDato() {
            return dato;
        }
        
        public void setDato(E dato) {
            this.dato = dato;
        }
        
        public Nodo<E> getSiguiente() {
            return siguiente;
        }
        
        public void setSiguiente(Nodo<E> siguiente) {
            this.siguiente = siguiente;
        }
    }
    
    // Métodos de la lista enlazada
    public void agregar(T elemento) {
        Nodo<T> nuevoNodo = new Nodo<>(elemento);
        
        if (cabeza == null) {
            cabeza = nuevoNodo;
        } else {
            Nodo<T> actual = cabeza;
            while (actual.getSiguiente() != null) {
                actual = actual.getSiguiente();
            }
            actual.setSiguiente(nuevoNodo);
        }
        
        tamaño++;
    }
    
    public T obtener(int indice) {
        if (indice < 0 || indice >= tamaño) {
            throw new IndexOutOfBoundsException("Índice fuera de rango");
        }
        
        Nodo<T> actual = cabeza;
        for (int i = 0; i < indice; i++) {
            actual = actual.getSiguiente();
        }
        
        return actual.getDato();
    }
    
    public int tamaño() {
        return tamaño;
    }
    
    // Método para imprimir la lista
    public void imprimir() {
        Nodo<T> actual = cabeza;
        System.out.print("Lista: [");
        
        while (actual != null) {
            System.out.print(actual.getDato());
            if (actual.getSiguiente() != null) {
                System.out.print(", ");
            }
            actual = actual.getSiguiente();
        }
        
        System.out.println("]");
    }
}

// Clase para probar
public class PruebaListaEnlazada {
    public static void main(String[] args) {
        ListaEnlazada<String> lista = new ListaEnlazada<>();
        
        lista.agregar("Java");
        lista.agregar("Python");
        lista.agregar("C++");
        
        lista.imprimir();
        
        System.out.println("Elemento en índice 1: " + lista.obtener(1));
        System.out.println("Tamaño de la lista: " + lista.tamaño());
    }
}

En este ejemplo, Nodo es una clase interna que representa un nodo en una lista enlazada. Tiene acceso a los miembros de ListaEnlazada y está estrechamente relacionada con su funcionamiento.

Clases internas locales

Definición y características

Una clase interna local es una clase definida dentro de un método o bloque de código. Solo es visible dentro del método o bloque donde está definida.

Características principales:

  • Se definen dentro de un método o bloque
  • Solo son visibles dentro del método o bloque donde están definidas
  • Pueden acceder a variables locales del método (deben ser final o efectivamente final)
  • No pueden tener modificadores de acceso o ser declaradas static
  • Pueden acceder a todos los miembros de la clase externa

Sintaxis básica

public class ClaseExterna {
    private String atributoExterno = "Atributo externo";
    
    public void metodo(final String parametro) {
        final String variableLocal = "Variable local";
        
        // Clase interna local
        class ClaseLocal {
            public void mostrarInfo() {
                // Puede acceder a miembros de la clase externa
                System.out.println(atributoExterno);
                
                // Puede acceder a variables locales (final o efectivamente final)
                System.out.println(parametro);
                System.out.println(variableLocal);
            }
        }
        
        // Crear instancia de la clase local
        ClaseLocal local = new ClaseLocal();
        local.mostrarInfo();
    }
}

Ejemplo práctico

Las clases internas locales son útiles cuando necesitamos una clase que solo se usa en un contexto específico:

public class Ordenador {
    public void ordenarLista(final int[] numeros) {
        // Clase interna local para el algoritmo de ordenamiento
        class AlgoritmoQuickSort {
            private void intercambiar(int[] arr, int i, int j) {
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
            
            private int particion(int[] arr, int bajo, int alto) {
                int pivote = arr[alto];
                int i = (bajo - 1);
                
                for (int j = bajo; j < alto; j++) {
                    if (arr[j] <= pivote) {
                        i++;
                        intercambiar(arr, i, j);
                    }
                }
                
                intercambiar(arr, i + 1, alto);
                return i + 1;
            }
            
            public void quickSort(int[] arr, int bajo, int alto) {
                if (bajo < alto) {
                    int indicePivote = particion(arr, bajo, alto);
                    
                    quickSort(arr, bajo, indicePivote - 1);
                    quickSort(arr, indicePivote + 1, alto);
                }
            }
        }
        
        // Usar la clase local
        AlgoritmoQuickSort quickSort = new AlgoritmoQuickSort();
        quickSort.quickSort(numeros, 0, numeros.length - 1);
    }
    
    public void imprimirArray(int[] arr) {
        System.out.print("Array: [");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i]);
            if (i < arr.length - 1) {
                System.out.print(", ");
            }
        }
        System.out.println("]");
    }
}

// Clase para probar
public class PruebaOrdenador {
    public static void main(String[] args) {
        int[] numeros = {5, 2, 9, -1, 7, 3, 8, 0};
        
        Ordenador ordenador = new Ordenador();
        System.out.println("Array original:");
        ordenador.imprimirArray(numeros);
        
        ordenador.ordenarLista(numeros);
        
        System.out.println("Array ordenado:");
        ordenador.imprimirArray(numeros);
    }
}

En este ejemplo, AlgoritmoQuickSort es una clase interna local que implementa el algoritmo de ordenamiento QuickSort y solo se utiliza dentro del método ordenarLista.

Clases internas anónimas

Definición y características

Una clase interna anónima es una clase interna sin nombre, declarada y creada en una sola expresión. Se utiliza cuando necesitamos crear una instancia única de una clase.

Características principales:

  • No tienen nombre
  • Se definen y se instancian en una sola expresión
  • Se utilizan para crear implementaciones rápidas de interfaces o clases
  • Pueden acceder a variables locales (final o efectivamente final)
  • Pueden acceder a todos los miembros de la clase externa
  • No pueden tener constructores explícitos
  • No pueden tener miembros estáticos, excepto constantes

Sintaxis básica

public class ClaseExterna {
    private String mensaje = "Mensaje de la clase externa";
    
    public void metodo() {
        // Clase anónima que extiende de una clase
        Object objeto = new Object() {
            @Override
            public String toString() {
                return mensaje; // Puede acceder a miembros de la clase externa
            }
        };
        System.out.println(objeto.toString());
        
        // Clase anónima que implementa una interfaz
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Ejecutando: " + mensaje);
            }
        };
        runnable.run();
    }
}

Ejemplo práctico

Las clases anónimas son especialmente útiles para implementar listeners de eventos o interfaces con pocos métodos:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class OrdenadorProductos {
    private List<Producto> productos;
    
    public OrdenadorProductos() {
        productos = new ArrayList<>();
    }
    
    public void agregarProducto(Producto p) {
        productos.add(p);
    }
    
    public void ordenarPorPrecio() {
        // Clase anónima que implementa Comparator
        Collections.sort(productos, new Comparator<Producto>() {
            @Override
            public int compare(Producto p1, Producto p2) {
                return Double.compare(p1.getPrecio(), p2.getPrecio());
            }
        });
    }
    
    public void ordenarPorNombre() {
        // Clase anónima que implementa Comparator
        Collections.sort(productos, new Comparator<Producto>() {
            @Override
            public int compare(Producto p1, Producto p2) {
                return p1.getNombre().compareTo(p2.getNombre());
            }
        });
    }
    
    public void mostrarProductos() {
        for (Producto p : productos) {
            System.out.println(p);
        }
    }
    
    // Clase interna para representar productos
    public static class Producto {
        private String nombre;
        private double precio;
        
        public Producto(String nombre, double precio) {
            this.nombre = nombre;
            this.precio = precio;
        }
        
        public String getNombre() {
            return nombre;
        }
        
        public double getPrecio() {
            return precio;
        }
        
        @Override
        public String toString() {
            return nombre + " - " + precio + "€";
        }
    }
}

// Clase para probar
public class PruebaOrdenador {
    public static void main(String[] args) {
        OrdenadorProductos ordenador = new OrdenadorProductos();
        
        // Agregar productos
        ordenador.agregarProducto(new OrdenadorProductos.Producto("Teclado", 25.99));
        ordenador.agregarProducto(new OrdenadorProductos.Producto("Monitor", 199.50));
        ordenador.agregarProducto(new OrdenadorProductos.Producto("Ratón", 15.75));
        
        System.out.println("Lista original:");
        ordenador.mostrarProductos();
        
        ordenador.ordenarPorPrecio();
        System.out.println("\nLista ordenada por precio:");
        ordenador.mostrarProductos();
        
        ordenador.ordenarPorNombre();
        System.out.println("\nLista ordenada por nombre:");
        ordenador.mostrarProductos();
        
        // Crear un botón con un listener anónimo (ejemplo conceptual)
        System.out.println("\nEjemplo de listener:");
        Boton boton = new Boton("Guardar");
        boton.setOnClickListener(new ClickListener() {
            @Override
            public void onClick() {
                System.out.println("Botón " + boton.getTexto() + " pulsado");
            }
        });
        boton.click();
    }
    
    // Clases auxiliares para el ejemplo de listener
    static class Boton {
        private String texto;
        private ClickListener listener;
        
        public Boton(String texto) {
            this.texto = texto;
        }
        
        public void setOnClickListener(ClickListener listener) {
            this.listener = listener;
        }
        
        public void click() {
            if (listener != null) {
                listener.onClick();
            }
        }
        
        public String getTexto() {
            return texto;
        }
    }
    
    interface ClickListener {
        void onClick();
    }
}

En este ejemplo, vemos dos usos de clases anónimas: uno para implementar comparadores personalizados y otro para implementar un listener de eventos.

Comparación entre tipos de clases anidadas

A continuación, se presenta una tabla comparativa de los diferentes tipos de clases anidadas:

Característica Clase anidada estática Clase interna (miembro) Clase interna local Clase interna anónima
Acceso a miembros de la clase externa Solo estáticos Todos Todos Todos
Acceso a variables locales No aplica No aplica Sí (final o efectivamente final) Sí (final o efectivamente final)
Donde se define Dentro de una clase Dentro de una clase Dentro de un método o bloque En una expresión
Puede tener constructores No
Puede tener miembros estáticos No No No (excepto constantes)
Nombre Tiene nombre Tiene nombre Tiene nombre Sin nombre
Instanciación Independiente Requiere instancia externa Dentro del método En la expresión
Visibilidad Según modificador Según modificador Solo en el método/bloque Solo en el contexto inmediato

Cuándo usar cada tipo de clase anidada

Clases anidadas estáticas

  • Cuando la clase está asociada lógicamente con la clase externa
  • Cuando la clase no necesita acceder a los miembros no estáticos de la clase externa
  • Para agrupar clases utilitarias dentro de una clase principal
  • Para encapsular una clase que solo se usa en el contexto de otra clase

Clases internas (miembro)

  • Cuando necesitas acceder a miembros de instancia de la clase externa
  • Para encapsular una clase que está estrechamente relacionada con la clase externa
  • Cuando la clase se utiliza en múltiples métodos de la clase externa
  • Para implementar patrones de diseño como el patrón Observer

Clases internas locales

  • Cuando necesitas una clase que solo se usa en un método específico
  • Para crear una implementación específica de una interfaz o clase utilizada solo en un contexto
  • Para acceder a variables locales del método (final o efectivamente final)
  • Para mejorar la encapsulación y ocultar la implementación

Clases internas anónimas

  • Para implementaciones rápidas de interfaces o clases abstractas
  • Para definir manejadores de eventos o callbacks
  • Cuando solo necesitas una instancia de la clase
  • Para implementaciones sencillas que no requieren reutilización

Consideraciones de rendimiento y patrones de diseño

Las clases anidadas, además de mejorar la organización del código, también pueden tener implicaciones en el rendimiento y facilitar ciertos patrones de diseño:

  1. Patrón Iterator: Las clases internas son ideales para implementar iteradores personalizados, ya que pueden acceder directamente a la estructura de datos interna de la clase contenedora.

  2. Patrón Builder: Las clases anidadas estáticas son excelentes para implementar el patrón Builder, que ayuda a construir objetos complejos paso a paso.

  3. Patrón Strategy: Las clases anónimas son útiles para implementar diferentes estrategias en tiempo de ejecución sin necesidad de crear múltiples archivos de clase.

  4. Encapsulación y visibilidad: Las clases anidadas ayudan a encapsular clases auxiliares que no necesitan ser accesibles fuera de su contexto.

// Ejemplo de patrón Builder con clase anidada estática
public class Persona {
    private final String nombre;
    private final int edad;
    private final String direccion;
    private final String telefono;
    private final String email;
    
    private Persona(Builder builder) {
        this.nombre = builder.nombre;
        this.edad = builder.edad;
        this.direccion = builder.direccion;
        this.telefono = builder.telefono;
        this.email = builder.email;
    }
    
    // Clase Builder como clase anidada estática
    public static class Builder {
        // Campos obligatorios
        private final String nombre;
        private final int edad;
        
        // Campos opcionales
        private String direccion = "";
        private String telefono = "";
        private String email = "";
        
        public Builder(String nombre, int edad) {
            this.nombre = nombre;
            this.edad = edad;
        }
        
        public Builder direccion(String direccion) {
            this.direccion = direccion;
            return this;
        }
        
        public Builder telefono(String telefono) {
            this.telefono = telefono;
            return this;
        }
        
        public Builder email(String email) {
            this.email = email;
            return this;
        }
        
        public Persona build() {
            return new Persona(this);
        }
    }
    
    @Override
    public String toString() {
        return "Persona{" +
               "nombre='" + nombre + '\'' +
               ", edad=" + edad +
               ", direccion='" + direccion + '\'' +
               ", telefono='" + telefono + '\'' +
               ", email='" + email + '\'' +
               '}';
    }
}

// Uso del patrón Builder
public class PruebaBuilder {
    public static void main(String[] args) {
        Persona persona = new Persona.Builder("Juan", 30)
                              .direccion("Calle Ejemplo, 123")
                              .telefono("666123456")
                              .email("juan@ejemplo.com")
                              .build();
        
        System.out.println(persona);
    }
}

Resumen

Las clases anidadas en Java son herramientas poderosas que nos permiten organizar mejor nuestro código, aumentar la encapsulación y crear soluciones más elegantes para ciertos problemas. Cada tipo de clase anidada (estática, interna, local o anónima) tiene características únicas que las hacen adecuadas para diferentes situaciones.

Las clases anidadas estáticas nos permiten agrupar clases relacionadas lógicamente sin necesidad de acceder a los miembros de instancia de la clase externa. Las clases internas nos dan acceso completo a la clase contenedora, ideal para clases estrechamente acopladas. Las clases locales son útiles en contextos específicos, y las clases anónimas nos permiten crear implementaciones rápidas sin necesidad de definir nuevas clases con nombre.

Al utilizar adecuadamente las clases anidadas, podemos crear código más modular, mantenible y claro, aprovechando al máximo las capacidades de encapsulación que ofrece Java. Son especialmente útiles en la implementación de ciertos patrones de diseño y en la organización de código relacionado lógicamente.