28 de enero de 2019

Clases anónimas

Terminábamos nuestra sesión de clases abstractas preguntándonos ¿por qué una clase abstracta (que no se puede instanciar) tiene constructores?

Obviando que los constructores declarados en VehiculoConRuedas sirven para aprovechar constructores de Vehiculo, lo correcto es que las clases abstractas contengan todo el código relacionado con sus miembros.

Es normal que el código que implementemos para inicializar los atributos modelo y color, pueda heredarlo para no tener que implementarlo en cada subtipo (mejorando el mantenimiento como ya hemos repetido varias veces)

No obstante, hay un caso muy importante para tener también constructores incluso en clases abstractas: instanciar con clases anónimas.

Una clase anónima nos va a permitir declarar una clase e instanciarla en la pequeña parte de código donde nos haga falta, haciendo nuestro código más legible y económico. Como podemos deducir de su definición, es una clase que no tiene nombre, no está contenida en su propio archivo .java, ni es interna (esto último todavía no lo hemos visto)

Una clase anónima nos va a permitir declarar una clase e instanciarla brevemente, haciendo nuestro código más legible y económico


Hasta ahora hemos visto que cada clase tenía que tener su propio archivo .java con el mismo nombre o Eclipse nos daba un error (MiClase tiene el código en el archivo MiClase.java).

Sin embargo, vamos a encontrarnos infinidad de situaciones en las que necesitaremos un objeto que podría usar un tipo prácticamente igual a otro ya creado, salvo por un pequeñísimo cambio que además sólo vamos a utilizar en ese objeto en un punto concreto de nuestro código y nunca más nos interesará.

Es demasiado repetitivo y excesivo tener que crear un archivo para una nueva clase que herede de un tipo sólo para instanciar un objeto en una línea de código (imaginad crear un botón en un menú donde lo único que cambia con otro botón es qué tiene que pasar al hacer click... ¿nos creamos una clase para cada botón?). Para estos casos, Java tiene las clases anónimas que son una herramienta poderosísima de personalización de un tipo.

Una clase anónima se crea al vuelo a la hora de instanciar un objeto que necesitamos y no encaja exactamente con ningún tipo. Se define llamando al constructor de un tipo conocido y añadiéndole un cuerpo con el comportamiento personalizado.

Vamos a ver un ejemplo con nuestro código sobre Vehiculo creándonos un triciclo. No nos vamos a crear una nueva clase Triciclo, pues sólo queremos imprimir nuestro objeto triciclo, pero no encaja con ningún tipo que tengamos: tiene tres ruedas (no es una Moto) y no tiene matrícula (no es un Coche).
NOTA: No es una buena práctica utilizar tipos que tengan todo lo que necesitamos y hacer null el resto de atributos. Los objetos deben ser del tipo con el que se construyen, el resto son apaños que aumentan la deuda técnica.

Lo primero es seleccionar la clase que más se acerca a lo que queremos: VehiculoConRuedas. Éste sería el código para crearme un triciclo:
VehiculoConRuedas triciclo = new VehiculoConRuedas("Fisher-Price", "Multicolor") {
    
    @Override
    public int getNumeroDeRuedas() {
        return 3;
    }

};
Vemos que declaramos una variable del tipo VehiculoConRuedas (pues hereda de éste tipo), pero al ser una clase abstracta y no poderse instanciar (está incompleta) directamente nos añadirá lo que le falta por implementar: getNumeroDeRuedas().

Si imprimimos nuestro triciclo veremos que el método toString() funciona igual que en las clases NO anónimas Coche y Moto que heredaban de VehiculoConRuedas. Tenemos la siguiente salida.

Fisher-Price (Multicolor), 3 ruedas

¿Sólo puedo modificar métodos que ya existan?

No. Al ser VehiculoConRuedas una clase abstracta debemos implementar los métodos no implementados, pero podemos añadir más miembros o modificar (incluso reutilizar) los que ya estén implementados.

En nuestros ejemplos anteriores nos hemos creado una Moto. Vamos a crearnos otra que sea una Harley-Davidson. Ésta marca de moto sólo permite dos colores: negro y rojo. Vamos a cambiar el método setColor(String) para que no acepte ningún color que no sea uno de esos dos:
Moto harley = new Moto("Harley-Davidson", "Rosa") {

    @Override
    public void setColor(String color) {
        if(!(color.equals("Rojo") || color.equals("Negro"))) {
            System.out.println("No se permite ese color para " + modelo);
        } else {
            super.setColor(color);
        }
    }
    
};
System.out.println(harley);
Con este código tenemos la salida:

No se permite ese color para Harley-Davidson
Moto: Harley-Davidson (null), 2 ruedas

Nos avisa que el color no está permitido y de hecho vemos que nos imprime null porque no se ha asignado. Si le asignamos (reutilizando con super el método que hereda) un color válido si que funcionará:
harley.setColor("Negro");
System.out.println(harley);
Moto: Harley-Davidson (Negro), 2 ruedas

¿Necesito una clase abstracta para crear una clase anónima?

No. Vamos a crearnos un barco usando la clase Vehiculo que no es abstracta:
Vehiculo barco = new Vehiculo("CMB Yachts", "Blanco") {
    double eslora = 47.8;

    private double getEslora() {
        return eslora;
    }
    
    @Override
    public String toString() {
        return super.toString() + " con " + getEslora() + "m de eslora";
    }
    
};
System.out.println(barco);
Vemos que podemos tanto sobrescribir, como crear nuevos miembros (incluyendo atributos como eslora) y no afecta si el tipo del que heredamos es abstracto o no. Incluso podemos crear clases anónimas usando una Interface como veremos cuando las conozcamos. El resultado por consola es:

CMB Yachts (Blanco) con 47.8m de eslora

Las clases anónimas y el concepto scope

Un aspecto importante a destacar es que las clases anónimas se crean en el ámbito (scope) de otra y tienen acceso a los miembros tanto de su clase como del tipo donde se crean incluso con el modificador de acceso private. Esto nos permite referenciar valores que permanecen privados pero que pueden ser usados en la implementación de nuestra clase anónima (por ejemplo un botón puede desencadenar un cambio en un componente privado de un formulario).

Las clases anónimas tienen acceso a todos sus miembros y a los del tipo donde se crean


Vamos a ver un ejemplo. Declaro un atributo privado:
private static int miCodigoSecreto = 12345; // ¿En serio?
Pero si me instancio una clase anónima dentro de su ámbito voy a tener acceso:
Coche cocheConPin = new Coche() {

    @Override
    public String toString() {
        return "El código secreto es: " + miCodigoSecreto;
    }
    
};
System.out.println(cocheConPin);
Si imprimo mi nuevo objeto tendré la salida:

El código secreto es: 12345

Siendo anónimas ¿Cómo nos referimos a su class?

Las clases van a tomar por defecto el nombre del tipo donde se instancian más un autonumérico precedido de un dolar ($). Podemos verlo con el siguiente código:
System.out.println(triciclo.getClass());
System.out.println(harley.getClass());
System.out.println(barco.getClass());
Como vemos nos salen 3 clases distintas (instanciadas en la clase Igualdad) y lo único que cambie es el autonumérico:

class Igualdad$1
class Igualdad$2
class Igualdad$3

Conclusión

Puede que todavía no veas las posibilidades que aportan las clases anónimas, pero cuando conozcas los Listener, las lambdas, los stream, las Interfaces Funcionales y otras capacidades de Java, no querrás seguir programando sin ellas.

No hay comentarios:

Publicar un comentario

Compárteme

Entradas populares