25 de enero de 2019

Clases abstractas

Las clases abstractas son clases pensadas para ser heredadas de ellas y que encapsulan código dirigido . Se comportan exactamente igual que las clases que hemos visto hasta ahora con estas diferencias:
  1. Debe declararse que son abstractas usando en su declaración la palabra reservada abstract como modificador.
  2. Tendrán métodos abstractos que se declararán también con abstract. Estos métodos no tienen cuerpo y serán implementados por sus subtipos.
  3. Al carecer de una implementación completa (los métodos abstractos no están implementados), no podrán construirse instancias de este tipo pues para poder crear objetos deben tener todos sus miembros implementados.
NOTA: Si bien el código no nos arrojará ningún error ni nos obligará a cumplir las condiciones 2 y 3, el hecho de no cumplirlas sería un indicativo de que no tiene sentido declararla abstracta.

Con las clases abstractas vamos a tener código relacionado en un único sitio para que por herencia lo completen y aprovechen sus subtipos


Como todo se ve mejor con un ejemplo, vamos a crearnos una clase abstracta llamada VehiculoConRuedas, que servirá para heredar de Vehiculo y gestionar lo que tenga que ver con ruedas. Vamos a crearla de la misma forma que lo hicimos la sesión anterior, pero en esta ocasión, aunque pensemos que debemos escoger el atributo numeroDeRuedas, no lo vamos a marcar y sólo le vamos a poner el nombre "VehiculoConRuedas" a la clase abstracta.
NOTA: Si ya tuviera más clases con comportamientos a agrupar (por ejemplo ya tuviera creada Moto), Eclipse nos permite añadir todas las clases de las que queramos extraer la superclase.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class VehiculoConRuedas extends Vehiculo {

  public VehiculoConRuedas() {
    super();
  }

  public VehiculoConRuedas(String modelo, String color) {
    super(modelo, color);
  }

}

Eclipse ha insertado en nuestra jerarquía la clase VehiculoConRuedas entre Coche y Vehiculo para que todo siga funcionando correctamente. Todo esto también podríamos haberlo escrito a mano y no debe dejar de practicarse para coger soltura.

Si abrimos nuestra nueva clase vemos que sólo tiene dos constructores y que es una clase normal. Vamos a añadirle un método abstracto para ver qué una clase normal no lo permite. Lo haremos simplemente declarándolo incluyendo el modificador abstract y en vez de un cuerpo terminaremos con punto y coma:

1
public abstract int getNumeroDeRuedas();

Tendremos un error en Eclipse que nos dirá que quitemos el modificador abstract del método o que hagamos la clase abstracta. Vamos a declarar nuestra nueva clase abstracta y ahora ya no tendremos errores.

Nuestra clase completa queda:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public abstract class VehiculoConRuedas extends Vehiculo {

  public VehiculoConRuedas() {
    super();
  }

  public VehiculoConRuedas(String modelo, String color) {
    super(modelo, color);
  }

  public abstract int getNumeroDeRuedas();
}

Guardamos los cambios en VehiculoConRuedas y volvemos al código de Coche para ver que hereda de VehiculoConRuedas (lo hizo Eclipse) pero nos aparece un error: No tiene implementado el método getNumeroDeRuedas().

Podemos usar Eclipse para que nos añada este método vacío o hacerlo a mano. Si aceptamos la solución que nos ofrece Eclipse (Add unimplemented methods) cuando nos situamos encima del error vamos a obtener en Coche el siguiente código (seguramente estará añadido al final de todo):

1
2
3
4
5
  @Override
  public int getNumeroDeRuedas() {
    // TODO Auto-generated method stub
    return 0;
  }

Con esto Eclipse nos crea ese método vacío y nos añade una tarea (//TODO) para rellenar la implementación. Como nosotros tenemos precisamente un atributo llamado numeroDeRuedas vamos a devolver su valor como hacemos con cualquier otro getter:

1
2
3
4
  @Override
  public int getNumeroDeRuedas() {
    return numeroDeRuedas;
  }

Ahora si imprimimos un coche veremos que el método toString() que implementamos funciona correctamente:
  1. Obtiene el modelo y color desde Vehiculo
  2. Obtiene el número de ruedas usando u miembro heredado de VehiculoConRuedas
  3. El método toString() está implementado en Coche.
Compruébalo añadiendo este código:

1
  System.out.println(coche1);

A nuestro main hecho en la sesión de igualdad, verás que nos muestra por consola:

Placa 1234 BBB - Seat Ibiza (Rojo), 4 ruedas

Reutilizamos nuestra jerarquía

Ahora llega el momento de reutilizar nuestra jerarquía creándonos una clase Moto. Podemos usar las ventajas de Eclipse y decir en la ventana de creación de clase que hereda de VehiculoConRuedas. Así tenemos ya trabajo hecho:

1
2
3
4
5
6
7
8
9
public class Moto extends VehiculoConRuedas {

  @Override
  public int getNumeroDeRuedas() {
    // TODO Auto-generated method stub
    return 0;
  }

}

Nos queda implementar el método abstracto getNumeroDeRuedas(). No es necesario crearse un atributo para completar este método. Bastaría con que siempre devuelva el valor 2 para ver el ejemplo:

1
2
3
4
  @Override
  public int getNumeroDeRuedas() {
    return 2;
  }

Colocando más cosas en su sitio

Si ahora hago la prueba de imprimir una nueva moto veré que obtengo un resultado bastante feo como ya vimos en la sesión sobre el método toString():

1
2
  System.out.println(new Moto());
  // Devuelve algo como Moto@52e922

Tiene sentido colocar el código que está relacionado en el mismo sitio: Si todos los Vehiculo tienen modelo y color, y todos los VehiculoConRuedas tienen ruedas, el método toString() podría ir mejorándose en función de qué nivel ocupa mi tipo en la jerarquía. Voy a implementarlo haciendo dos cosas:
  1. Crearé un constructor para Moto que acepte modelo y color. (puedes usar Source > Generate Constructors from Superclass
  2. Moveré la parte del código de Coche.toString() a las clases a las que pertenecen los miembros correspondientes.
Aquí están los cambios que hay que hacer:

En Vehiculo:

1
2
3
4
  @Override
  public String toString() {
    return modelo + " (" + getColor() + ")";
  }

En VehiculoConRuedas:

1
2
3
4
  @Override
  public String toString() {
    return super.toString() + ", " + getNumeroDeRuedas() + " ruedas";
  }

En Coche:

1
2
3
4
  @Override
  public String toString() {
    return "Placa " + matricula + " - " + super.toString();
  }

En Moto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Moto extends VehiculoConRuedas {

  public Moto(String modelo, String color) {
    super(modelo, color);
  }

  @Override
  public int getNumeroDeRuedas() {
    return 2;
  }

  @Override
  public String toString() {
    return "Moto: " + super.toString();
  }

}

Ahora si imprimo los ejemplos de toString() anteriores:

1
2
  System.out.println(coche1);
  System.out.println(new Moto("Suzuki", "Negro"));

Tengo la salida:

Placa 1234 BBB - Seat Ibiza (Rojo), 4 ruedas
Moto: Suzuki (Negro), 2 ruedas

Queda demostrado el ahorro de tiempo tanto construyendo como manteniendo nuestro código si nos apoyamos en jerarquías que me permitan tener el código relacionado escrito en un único sitio para que luego sea aprovechado de manera particular en otras clases más especializadas.

Si alguno ha examinado el código quizás pueda preguntarse: Si las clases abstractas no se pueden instanciar ¿Por qué VehiculoConRuedas tiene constructores?

Es una buena pregunta, vamos a verlo mejor en la sesión que explica qué son las clases anónimas.

No hay comentarios:

Publicar un comentario

Compárteme

Entradas populares