La herencia es un mecanismo que permite la definición de una clase a partir de la definición de otra ya existente. Es decir, una clase hija extiende la funcionalidad de una clase padre.
Por ejemplo, supongamos que tenemos una clase Coche con el siguiente código:
import java.util.Arrays; public class Coche { private String marca, modelo; private int velocidad; private float[] presionRuedas = new float[4]; private boolean ruedaRepuesto; public Coche(String marca, String modelo, int velocidad, boolean ruedaRepuesto, float[] presionRuedas) { this.marca = marca; this.modelo = modelo; this.velocidad = velocidad; this.ruedaRepuesto = ruedaRepuesto; this.presionRuedas = presionRuedas; } public void acelerar(int incremento) { this.velocidad += incremento; } public void frenar(int decremento) { this.velocidad -= decremento; } public void cambiarRueda(){ if(this.ruedaRepuesto) { this.ruedaRepuesto = false; } else { System.out.println("No existe rueda de repuesto"); } } public void aumentarPresion(int rueda, float presion){ presionRuedas[rueda] = Math.round((presionRuedas[rueda] + presion) * 10.0) / 10.0f; } public void reducirPresion(int rueda, float presion) { presionRuedas[rueda] = Math.round((presionRuedas[rueda] - presion) * 10.0) / 10.0f; } @Override public String toString(){ return "Marca: " + this.marca + "\nModelo: " + this.modelo + "\nVelocidad: " + this.velocidad + "\nRueda repuesto: " + this.ruedaRepuesto + "\nPresión ruedas: " + Arrays.toString(this.presionRuedas); } }
Nuestra clase tiene los atributos marca, modelo y velocidad. Además, tiene un atributo que indica si existe o no rueda de repuesto y un array con la presión de las ruedas. Los métodos de la clase son acerlerar() y frenar(), que aumenta o disminuye la velocidad, cambiarRueda(), que, si existe la rueda de repuesto, pone el atributo a False para indicar que se ha sustituido una rueda y aumentarPresion() y reducirPresion(), que aumenta o reduce la presión de una de las ruedas.
Además, tiene un último método toString() que muestra por pantalla el estado del vehículo.
Vamos a crear un nuevo objeto de tipo Coche en nuestra clase principal, ejecutar algunos de sus métodos e ir sacando por pantalla el estado del objeto para ver como cambia:
public class Main { public static void main(String[] args) { Coche coche1 = new Coche( "Kia", "Niro", 0, true, new float[] {2.4f, 2.4f, 2.4f, 2.4f} ); System.out.println(coche1.toString()); coche1.acelerar(10); coche1.cambiarRueda(); coche1.reducirPresion(0, 0.5f); coche1.aumentarPresion(1, 0.3f); coche1.frenar(3); coche1.cambiarRueda(); System.out.println(coche1.toString()); }
Marca: Kia Modelo: Niro Velocidad: 0 Rueda repuesto: true Presión ruedas: [2.4, 2.4, 2.4, 2.4] No existe rueda de repuesto Marca: Kia Modelo: Niro Velocidad: 7 Rueda repuesto: false Presión ruedas: [1.9, 2.7, 2.4, 2.4]
Por ahora, nuestros métodos funcionan como toca. Supongamos ahora que queremos tener una nueva clase Moto. Esta clase tendrá también los atributos marca, modelo, velocidad y presionRuedas (aunque esta vez, tendrá solo 2 ruedas, en lugar de 4). Tendrá, además, los métodos acelerar(), frenar(), aumentarPresion(), reducirPresion() y toString():
import java.util.Arrays; public class Moto { private String marca, modelo; private int velocidad; private float[] presionRuedas = new float[2]; public Moto(String marca, String modelo, int velocidad, float[] presionRuedas) { this.marca = marca; this.modelo = modelo; this.velocidad = velocidad; this.presionRuedas = presionRuedas; } public void acelerar(int incremento) { this.velocidad += incremento; } public void frenar(int decremento) { this.velocidad -= decremento; } public void aumentarPresion(int rueda, float presion){ presionRuedas[rueda] = Math.round((presionRuedas[rueda] + presion) * 10.0) / 10.0f; } public void reducirPresion(int rueda, float presion) { System.out.println(presion); presionRuedas[rueda] = Math.round((presionRuedas[rueda] - presion) * 10.0) / 10.0f; } @Override public String toString(){ return "Marca: " + this.marca + "\nModelo: " + this.modelo + "\nVelocidad: " + this.velocidad + "\nPresión ruedas: " + Arrays.toString(this.presionRuedas); } }
Si te fijas, la mayoría de atributos y métodos son iguales (o muy similares) en ambas clases. Con la herencia, podemos reutilizar parte del código de ambas clases creando una clase padre que contendrá ese código repetido. Por ejemplo, crea una clase Vehiculo con el siguiente código:
import java.util.Arrays; public class Vehiculo { private String marca, modelo; private int velocidad; private float[] presionRuedas; public Vehiculo(String marca, String modelo, int velocidad, float[] presionRuedas) { this.marca = marca; this.modelo = modelo; this.velocidad = velocidad; this.presionRuedas = presionRuedas; } public void acelerar(int incremento) { this.velocidad += incremento; } public void frenar(int decremento) { this.velocidad -= decremento; } public void aumentarPresion(int rueda, float presion){ presionRuedas[rueda] = Math.round((presionRuedas[rueda] + presion) * 10.0) / 10.0f; } public void reducirPresion(int rueda, float presion) { presionRuedas[rueda] = Math.round((presionRuedas[rueda] - presion) * 10.0) / 10.0f; } @Override public String toString(){ return "Marca: " + this.marca + "\nModelo: " + this.modelo + "\nVelocidad: " + this.velocidad + "\nPresión ruedas: " + Arrays.toString(this.presionRuedas); } }
Hemos creado una clase Vehiculo con el código común a ambas clases (incluso hemos eliminado la restricción del número de ruedas en el atributo presionRuedas). Lo que deberíamos hacer ahora, es crear dos clases hijas Coche y Moto que hereden de Vehiculo para que compartan sus métodos y atributos. Para crear clases que hereden de otra utilizamos la palabra reserveda extends seguido de la clase padre en la definición de la clase hija.
Vamos a crear primero la clase Coche:
public class Coche extends Vehiculo{ }
Si creamos la clase Coche como el ejemplo anterior, nos dará un error con el mensaje “Implicit super constructor Vehiculo() is undefined for default constructor. Must define an explicit constructorJava(134217868)”. Esto ocurre porque nuestra clase padre Vehiculo tiene definido un constructor, con lo que cuando creemos un objeto de la clase hija Coche tenemos que ejecutar obligatoriamente ese constructor. Para solucionarlo, tenemos que crear un constructor en la clase Coche.
¿Y que contendrá ese constructor de la clase hija? Ese constructor tiene que ejecutar primero el constructor de la clase padre, y después el código que queramos añadir en la clase hija. En nuestro caso, deberá asignar valor a la marca, modelo, velocidad y la presión de las ruedas desde el constructor de la clase padre (ya que son atributos comunes que hemos definido en la clase Vehiculo) y, además, asignarle un valor inicial al atributo ruedaRepuesto, propia de la clase Coche. Para ello, tenemos que ser capaces de ejecutar métodos de la clase padre desde la clase hija. Ésto se puede hacer con la palabra reservada super().
Como hemos dicho, para ejecutar métodos de la clase padre tenemos que usar la palabra reservada super. Podemos usarla de dos formas:
Vamos a ver un par de ejemplos del uso de super. Crea un constructor en la clase Coche con el siguiente contenido:
public class Coche extends Vehiculo{ public Coche(String marca, String modelo, int velocidad, float[] presionRuedas) { super(marca, modelo, velocidad, presionRuedas); } }
Si te fijas, el constructor de la clase Coche hace referencia al constructor de la clase Vehiculo mediante super(). Obviamente, al método le tenemos que pasar las variables que espera el constructor de la clase padre (Vehiculo). En este caso, marca, modelo, velocidad y presionRuedas.
Vamos a crear un nuevo objeto de la clase Coche en nuestra clase principal y comprobar que todo funciona como toca:
public class Main { public static void main(String[] args) { Coche coche1 = new Coche( "Kia", "Niro", 0, new float[] {2.4f, 2.4f, 2.4f, 2.4f} ); System.out.println(coche1.toString()); } }
Marca: Kia Modelo: Niro Velocidad: 0 Presión ruedas: [2.4, 2.4, 2.4, 2.4]
Si te fijas, hemos creado un objeto de la clase Coche y ejecutado uno de los métodos de la clase Vehiculo (toString). En realidad, podemos ejecutar cualquier método public o protected de la clase padre desde las clases hijas:
public static void main(String[] args) { Coche coche1 = new Coche( "Kia", "Niro", 0, new float[] {2.4f, 2.4f, 2.4f, 2.4f} ); System.out.println(coche1.toString()); coche1.acelerar(10); coche1.reducirPresion(0, 0.5f); coche1.aumentarPresion(1, 0.3f); coche1.frenar(3); System.out.println(coche1.toString()); }
Marca: Kia Modelo: Niro Velocidad: 0 Presión ruedas: [2.4, 2.4, 2.4, 2.4] Marca: Kia Modelo: Niro Velocidad: 7 Presión ruedas: [1.9, 2.7, 2.4, 2.4]
Vamos a ver otro ejemplo de uso de super como acceso a métodos o propiedades de la clase padre. Añade lo siguiente al constructor de la clase hija (Coche):
public class Coche extends Vehiculo{ public Coche(String marca, String modelo, int velocidad, float[] presionRuedas) { super(marca, modelo, velocidad, presionRuedas); super.acelerar(10); } }
Si creamos ahora un objeto de la clase Coche y mostramos su estado (mediante el método toString()), veremos como su velocidad inicial ya no es 0, si no 10, ya que en el constructor ejecutamos el método acelerar() de la clase padre mediante super.acelerar():
public static void main(String[] args) { Coche coche1 = new Coche( "Kia", "Niro", 0, new float[] {2.4f, 2.4f, 2.4f, 2.4f} ); System.out.println(coche1.toString()); }
Marca: Kia Modelo: Niro Velocidad: 10 Presión ruedas: [2.4, 2.4, 2.4, 2.4]
Ya tenemos la clase Coche con los atributos y métodos comunes. Ahora nos faltaría añadir la propiedad ruedaRepuesto y el método cambiarRueda():
public class Coche extends Vehiculo{ private boolean ruedaRepuesto; public Coche(String marca, String modelo, int velocidad, float[] presionRuedas) { super(marca, modelo, velocidad, presionRuedas); } public void cambiarRueda(){ if(this.ruedaRepuesto) { this.ruedaRepuesto = false; } else { System.out.println("No existe rueda de repuesto"); } } }
Vamos a crear un objeto Coche, utilizar sus funciones y mostrar el estado por pantalla:
public static void main(String[] args) { Coche coche1 = new Coche( "Kia", "Niro", 0, new float[] {2.4f, 2.4f, 2.4f, 2.4f} ); System.out.println(coche1.toString()); coche1.acelerar(10); coche1.reducirPresion(0, 0.5f); coche1.aumentarPresion(1, 0.3f); coche1.frenar(3); coche1.cambiarRueda(); System.out.println(coche1.toString()); } }
Marca: Kia Modelo: Niro Velocidad: 0 Presión ruedas: [2.4, 2.4, 2.4, 2.4] No existe rueda de repuesto Marca: Kia Modelo: Niro Velocidad: 7 Presión ruedas: [1.9, 2.7, 2.4, 2.4]
¿Por qué nos dice que no hay rueda de repuesto? Es lógico, ya que no le hemos pasado el valor inicial en el constructor. Para inicializar el atributo, debemos pasarle el valor inicial al constructor:
public Coche(String marca, String modelo, int velocidad, float[] presionRuedas, boolean ruedaRepuesto) { super(marca, modelo, velocidad, presionRuedas); this.ruedaRepuesto = ruedaRepuesto; }
Y añadir ese valor cuando creemos el objeto Coche desde nuestra clase inicial:
public static void main(String[] args) { Coche coche1 = new Coche( "Kia", "Niro", 0, new float[] {2.4f, 2.4f, 2.4f, 2.4f}, true ); System.out.println(coche1.toString()); coche1.acelerar(10); coche1.reducirPresion(0, 0.5f); coche1.aumentarPresion(1, 0.3f); coche1.frenar(3); coche1.cambiarRueda(); System.out.println(coche1.toString()); }
Marca: Kia Modelo: Niro Velocidad: 0 Presión ruedas: [2.4, 2.4, 2.4, 2.4] Marca: Kia Modelo: Niro Velocidad: 7 Presión ruedas: [1.9, 2.7, 2.4, 2.4]
Ya no nos muestra el error anterior, pero sigue sin aparecernos la propiedad ruedaRepuesto en el método toString(). Para eso, tenemos que modificar el método toString() en la clase hija mediante la sobreescritura de métodos.
A menudo necesitamos modificar métodos de la clase padre desde las clases hijas. Por ejemplo, en nuestro caso, el método toString() de la clase Vehiculo no muestra la propiedad ruedaRepuesto en la clase Coche. Para que se muestre, tenemos que cambiar el código y añadirle el atributo. Este proceso se llama sobreescritura de métodos (Override).
En Java, si queremos sobreescribir un método tenemos que añadir el decorador @Override antes de la cabecera del método que queremos sobreescribir.
Vamos a modificar el método toString() en la clase Coche para que muestre si existe o no la rueda de repuesto:
@Override public String toString(){ return "Marca: " + this.marca + "\nModelo: " + this.modelo + "\nVelocidad: " + this.velocidad + "\nPresión ruedas: " + Arrays.toString(this.presionRuedas) + "\nRueda de repuesto: " + this.ruedaRepuesto; }
Si ejecutamos el código, vemos varios errores (en realidad, el compilador ya nos avisa de dichos errores antes de la ejecución):
The field Vehiculo.marca is not visible The field Vehiculo.modelo is not visible The field Vehiculo.velocidad is not visible The field Vehiculo.presionRuedas is not visible
¿Recuerdas el tema de visibilidad? El error es, precisamente, ese. Los atributos de la clase Vehiculo los hemos definido como privados, con lo que no podemos acceder a ellos desde las clases hijas.
Podemos solucionarlos de varias formas. Por ejemplo, podemos cambiar la visibilidad de los atributos a protected:
public class Vehiculo { protected String marca, modelo; protected int velocidad; protected float[] presionRuedas; ...
Otra opción es mantener los atributos privados, crear getters públicos de los atributos y usarlos en el método toString() de las clases hijas:
public class Vehiculo { private String marca, modelo; private int velocidad; private float[] presionRuedas; public float[] getPresionRuedas() { return presionRuedas; } ...
public class Coche extends Vehiculo{ private boolean ruedaRepuesto; ... @Override public String toString(){ return "Marca: " + this.getMarca() + "\nModelo: " + this.getModelo() + "\nVelocidad: " + this.getVelocidad() + "\nPresión ruedas: " + Arrays.toString(this.getPresionRuedas()) + "\nRueda de repuesto: " + this.ruedaRepuesto; }
Una última opción sería usar super para recoger el String devuelto por el método toString() de la clase padre y añadirle el texto que falta:
public class Coche extends Vehiculo{ private boolean ruedaRepuesto; ... @Override public String toString(){ String respuesta = super.toString(); respuesta += "\nRueda de repuesto: " + this.ruedaRepuesto; return respuesta; }
public class Main { public static void main(String[] args) { Coche coche1 = new Coche( "Kia", "Niro", 0, new float[] {2.4f, 2.4f, 2.4f, 2.4f}, true ); System.out.println(coche1.toString()); coche1.acelerar(10); coche1.reducirPresion(0, 0.5f); coche1.aumentarPresion(1, 0.3f); coche1.frenar(3); coche1.cambiarRueda(); System.out.println(coche1.toString()); } }
Marca: Kia Modelo: Niro Velocidad: 0 Presión ruedas: [2.4, 2.4, 2.4, 2.4] Rueda de repuesto: true Marca: Kia Modelo: Niro Velocidad: 7 Presión ruedas: [1.9, 2.7, 2.4, 2.4] Rueda de repuesto: false
Por último, nos quedaría crear la clase Moto, también hija de la clase Vehiculo. En este caso, al no tener atributos ni métodos propios, solo tendríamos que añadir el constructor:
public class Moto extends Vehiculo{ public Moto(String marca, String modelo, int velocidad, float[] presionRuedas) { super(marca, modelo, velocidad, presionRuedas); } }
public class Main { public static void main(String[] args) { Coche coche1 = new Coche( "Kia", "Niro", 0, new float[] {2.4f, 2.4f, 2.4f, 2.4f}, true ); System.out.println(coche1.toString()); coche1.acelerar(10); coche1.reducirPresion(0, 0.5f); coche1.aumentarPresion(1, 0.3f); coche1.frenar(3); coche1.cambiarRueda(); System.out.println(coche1.toString()); Moto moto1 = new Moto( "Honda", "NC 750", 0, new float[] {2.5f, 2.5f}); System.out.println(moto1.toString()); moto1.acelerar(50); moto1.reducirPresion(1, 0.4f); System.out.println(moto1.toString()); } }
Marca: Kia Modelo: Niro Velocidad: 0 Presión ruedas: [2.4, 2.4, 2.4, 2.4] Rueda de repuesto: true Marca: Kia Modelo: Niro Velocidad: 7 Presión ruedas: [1.9, 2.7, 2.4, 2.4] Rueda de repuesto: false Marca: Honda Modelo: NC 750 Velocidad: 0 Presión ruedas: [2.5, 2.5] Marca: Honda Modelo: NC 750 Velocidad: 50 Presión ruedas: [2.5, 2.1]
¿Recuerdas como definíamos las constantes? Utilizamos final para indicar que el valor no podía cambiar. Podemos hacer algo similar con las clases y los métodos.
Si usamos la palabra final en una clase, estamos indicando que esa clase no puede tener herederos (clases hijas). Por ejemplo, si modificamos la clase Vehiculo añadiendo la palabra final, veremos como Java nos muestra un error diciendo que las clases Coche y Moto no pueden heredar de la clase Vehiculo:
public final class Vehiculo { private String marca, modelo; private int velocidad; private float[] presionRuedas; ...
The type Coche cannot subclass the final class Vehiculo
Algo parecido ocurre con los métodos. Si añadimos final a un método, le estamos indicando a Java que ese método no se puede cambiar. Es decir, no puede ser sobreescrito:
public final class Vehiculo { private String marca, modelo; private int velocidad; private float[] presionRuedas; ... @Override public final String toString(){ return "Marca: " + this.marca + "\nModelo: " + this.modelo + "\nVelocidad: " + this.velocidad + "\nPresión ruedas: " + Arrays.toString(this.presionRuedas); }
Cannot override the final method from Vehiculo
El uso de final es un método para asegurarnos que esa clase no puede tener clases hijas o ese método no puede ser sobreescrito.
Uno de los mecanismos más útiles de la herencia es el polimorfismo. El polimorfismo es una relajación del sistema de tipos, de tal manera que una referencia a una clase acepta direcciones de objetos de dicha clase y de sus clases derivadas (hijas, nietas, …).
Aunque pueda parecer un concepto complejo, en realidad es más sencillo de lo que parece. Vamos a ver un ejemplo para comprenderlo. Vamos a crear una clase Parking en nuestra aplicación que tendrá un array con las plazas del parking. Por ahora, solo vamos a permitir aparcar coches en nuestro parking:
public class Parking { private Coche[] plazas = new Coche[150]; public void aparcarCoche(int plaza, Coche coche) { if(this.plazas[plaza] == null) { this.plazas[plaza] = coche; } else { System.out.println("La plaza está ocupada"); } } @Override public String toString(){ int count = 0; for (Coche coche : plazas) { if(coche != null) { count++; } } return "Hay " + count + " coches aparcados en el parking"; } }
El código anterior es muy simple. Creamos un array de objetos de tipo Coche de tamaño 150 (serán nuestras plazas del parking). Como no inicializamos el array, cuando se cree un objeto de tipo Praking todas las plazas estarán vacías (su valor será null). El método aparcarCoche() aceptará un número de plaza (posición en el array) y un objeto de tipo Coche. Si la plaza está libre (el valor de la posición en el array es distinto de null) almacenamos en esa posición el coche que le hayamos pasado al método.
Por último, tenemos el clásico método toString(), que lo único que nos mostrará será el número de coches que hay aparcados en nuestro parking.
Si creamos dos coches en nuestra clase principal y los aparcamos en nuestro parking, vemos que todo funciona como toca:
public class Main { public static void main(String[] args) { Coche coche1 = new Coche( "Kia", "Niro", 0, new float[] {2.4f, 2.4f, 2.4f, 2.4f}, true ); Coche coche2 = new Coche( "Seat", "Ibiza", 0, new float[] {2.5f, 2.5f, 2.5f, 2.5f}, true ); Parking parking = new Parking(); parking.aparcarCoche(0, coche1); parking.aparcarCoche(50, coche2); System.out.println(parking.toString()); } }
Hay 2 coches aparcados en el parking
Supongamos ahora que queremos permitir aparcar motos también en nuestro parking (y que no hay distinción entre las plazas de coche y motos). Lo primero que vemos es que nuestras plazas de parking (el array de 150 elementos de la clase Parking) son de tipo Coche. Debemos cambiar eso para permitir aparcar también motos, pero ¿Cómo podemos hacerlo?.
Acuérdate que Java es de tipado estático, con lo que siempre tenemos que indicar de que tipo es la variable que estamos creando. Una posible solución sería crear otro array con elementos de tipo Moto, pero complicaría mucho la aplicación, ya que, primero, tendríamos que mantener dos listas para indicar lo mismo (las plazas libres y ocupadas) y, además, para insertar un coche, por ejemplo, deberíamos comprobar que esa posición está libre en ambas listas (los array de Coches y Motos).
Aquí es donde entra en juego el polimorfismo. Podemos tomarlo como una relajación de tipos. La solución es cambiar el tipo de elementos del array plazas por Vehiculo:
public class Parking { private Vehiculo[] plazas = new Vehiculo[150]; public void aparcarCoche(int plaza, Coche coche) { if(this.plazas[plaza] == null) { this.plazas[plaza] = coche; } else { System.out.println("La plaza está ocupada"); } } @Override public String toString(){ int count = 0; for (Vehiculo vehiculo : plazas) { if(vehiculo != null) { count++; } } return "Hay " + count + " vehículos aparcados en el parking"; } }
Si ejecutamos la aplicación (sin modificar nada de Main), vemos que sigue funcionando como toca:
Hay 2 coches aparcados en el parking
Fíjate en el método aparcarCoche() de la clase Parking:
public void aparcarCoche(int plaza, Coche coche) { if(this.plazas[plaza] == null) { this.plazas[plaza] = coche; } else { System.out.println("La plaza está ocupada"); } }
Aunque nuestro array plazas espera objetos de tipo Vehiculo, le estamos pasando un objeto de tipo Coche y no da ningún error. Eso es precisamente uno de los tipos de polimorfismo que existen. Podemos usar objetos de clases derivadas como si fuesen la clase principal.
En realidad, ya habíamos usado polimorfismo antes sin saberlo. Cuando hablamos de colecciones, vimos que era habitual definir el tipo de datos como la interfaz genérica, por ejemplo:
Set<Integer> conjunto = new HashSet<>();
Si te das cuenta, la variable conjunto es de tipo Set, aunque después le asignamos un tipo distinto (HashSet).
Ya tenemos preparado nuestro parking para poder aparcar coches y motos. El problema, es que el método aparcarCoche() espera un objeto de tipo Coche, con lo que no lo podemos utilizar para aparcar motos. Una posible solución sería crear otro método aparcarMoto() donde le pasásemos una variable de tipo Moto, pero el código sería casi el mismo, con lo que estaríamos repitiendo código. Otra solución más eficiente sería usar polimorfismo también en el método. En lugar de pasarle una variable de tipo Coche, podríamos modificar el método y pasarle una variable de tipo Vehiculo:
public void aparcar(int plaza, Vehiculo vehiculo) { if(this.plazas[plaza] == null) { this.plazas[plaza] = vehiculo; } else { System.out.println("La plaza está ocupada"); } }
Ahora ya podemos pasarle coches y motos a nuestro método aparcar:
public static void main(String[] args) { Coche coche1 = new Coche( "Kia", "Niro", 0, new float[] {2.4f, 2.4f, 2.4f, 2.4f}, true ); Coche coche2 = new Coche( "Seat", "Ibiza", 0, new float[] {2.5f, 2.5f, 2.5f, 2.5f}, true ); Moto moto1 = new Moto( "Honda", "NC 750", 0, new float[] {2.5f, 2.5f} ); Parking parking = new Parking(); parking.aparcar(0, coche1); parking.aparcar(50, coche2); parking.aparcar(30, moto1); System.out.println(parking.toString()); }
Hay 3 vehículos aparcados en el parking
Como hemos visto, la herencia es un mecanismo de la POO que nos ofrece una serie de ventajas muy útiles, entre ellas:
Aunque pueda parecer que es la solución a casi todos nuestros problemas, existen una serie de desventajas que desaconsejan muchas veces su uso:
Las dos últimas desventajas pueden hacer que nuestra aplicación sea difícil de escalar y mantener, haciendo nuestro código cada vez más complicado y sin sentido. Por ejemplo, supongamos que añadimos la clase Barco a nuestra aplicación. Tal y como la hemos diseñado, si hacemos que Barco herede de la clase Vehiculo tendremos dos métodos inservibles como son aumentarPresion() y reducirPresion(). Además, no tendría mucho sentido aparcar un barco en un parking, aunque según nuestra aplicación se podría hacer sin problemas.
Puedes pensar que podríamos hacer la clase Barco de forma independiente, sin heredar de la clase Vehiculo para solucionar los problemas anteriores, pero, entonces, si queremos tener un listado de los vehículos que tiene una persona (incluyendo coches, motos, barcos…) se nos complicaría el código bastante.
También podríamos aumentar la jerarquía de clases, creando dos clases nuevas VehiculoTerrestre y VehiculoAcuatico que heredasen de Vehiculo. Nuestras clases Coche y Moto heredarían, a su vez, de VehiculoTerrestre y Barco de VehiculoAcuatico. Esto podría solucionar el tema del parking, ya que podríamos hacer que solo se pudiesen aparcar vehículos terrestres. El problema es que estamos aumentando la complejidad de nuestras clases, pudiendo darse el caso que las clases superiores tengan poco o nada de código. Además, si añadimos las clases MotoAgua o MotoNieve, al solo poder heredar de una clase padre, no podríamos reutilizar métodos y propiedades comunes que puedan compartir con la clase Moto.
Existen una serie de síntomas que indican que no estamos utilizando la herencia de forma correcta:
Aunque la herencia puede ser muy útil en según que situaciones, normalmente es preferible favorecer la composición sobre la herencia.
La composición consisten en declarar clases que contienen otras clases. Es decir, es una relación de tipo tiene un/a, en lugar de es un/a como lo era la herencia. La idea general es tener instancias de una clase que contiene instancias de otras clases que implementan la funcionalidad deseada.
Por ejemplo, en nuestro ejemplo de los vehículos, en lugar de crear una clase padre Vehiculo de la cual heredarán el resto, podríamos crear las clases Coche y Moto y añadirle las partes (clases) que necesiten (en nuestro caso, la clase Rueda).
Para ver como quedaría, vamos a crear primero la clase Rueda:
public class Rueda { String marca; float presion; public Rueda(String marca, float presion) { this.marca = marca; this.presion = presion; } public void aumentarPresion(float incremento) { this.presion = Math.round((this.presion + presion) * 10.0) / 10.0f; } public void reducirPresion(float incremento) { this.presion = Math.round((this.presion - presion) * 10.0) / 10.0f; } @Override public String toString(){ return "[Marca:" + this.marca + ", Presión:" + this.presion + "]"; } }
La clase solo tiene la marca de la rueda y la presión. Además, tiene los métodos aumentarPresion() y reducirPresion() que ya vimos anteriormente, y el método toString() para mostrar el estado de la rueda por pantalla. De esta forma, nuestra clase Coche quedaría de la siguiente forma:
public class Coche{ private String marca, modelo; private int velocidad; private Rueda[] ruedas; private Rueda ruedaRepuesto; public Coche(String marca, String modelo, int velocidad, Rueda[] ruedas, Rueda ruedaRepuesto) { this.marca = marca; this.modelo = modelo; this.velocidad = velocidad; this.ruedas = ruedas; this.ruedaRepuesto = ruedaRepuesto; } public void acelerar(int incremento) { this.velocidad += incremento; } public void frenar(int decremento) { this.velocidad -= decremento; } public void cambiarRueda(int posicionRueda){ if(this.ruedaRepuesto != null) { this.ruedas[posicionRueda] = ruedaRepuesto; this.ruedaRepuesto = null; } else { System.out.println("No existe rueda de repuesto"); } } @Override public String toString(){ String respuesta; respuesta = "Marca: " + this.marca + "\nModelo: " + this.modelo + "\nVelocidad: " + this.velocidad + "\nRuedas: "; for (Rueda rueda : ruedas) { respuesta += rueda.toString() + " "; } respuesta += "\nRueda de repuesto: "; if(this.ruedaRepuesto != null) { respuesta += ruedaRepuesto.toString(); } else { respuesta += "Sin rueda de repuesto"; } return respuesta; } }
La clase sigue teniendo los atributos marca, modelo y velocidad, además de los métodos acelerar() y frenar(). Lo que ha cambiado, es que ahora tiene 2 atributos nuevos llamados ruedas, que es un array de objetos de tipo Rueda, y ruedaRepuesto, que es una instancia de Rueda.
De esta forma, podemos aumentar fácilmente nuestra funcionalidad. Fíjate, por ejemplo, en el método cambiarRueda(). Antes, lo único que hacíamos era poner el atributo ruedaRepuesto a null. Ahora, podemos pasarle la posición de la rueda que queremos cambiar, y sustituir esa rueda.
El último método (toString) muestra, como siempre, la situación del coche, pero ahora, podemos añadir más información, ya que podemos utilizar el método toString() de la clase Rueda. Si creamos un coche en nuestra clase principal y ejecutamos su método toString(), vemos que todo funciona correctamente:
public class Main { public static void main(String[] args) { Coche coche1 = new Coche( "Kia", "Niro", 0, new Rueda[] { new Rueda("Pirelli", 2.5f), new Rueda("Pirelli", 2.5f), new Rueda("Pirelli", 2.5f), new Rueda("Pirelli", 2.5f), }, new Rueda("Michelline", 2.0f) ); System.out.println(coche1.toString()); } }
Marca: Kia Modelo: Niro Velocidad: 0 Ruedas: [Marca:Pirelli, Presión:2.5] [Marca:Pirelli, Presión:2.5] [Marca:Pirelli, Presión:2.5] [Marca:Pirelli, Presión:2.5] Rueda de repuesto: [Marca:Michelline, Presión:2.0]
Vamos a probar a cambiar una rueda y ver como queda:
public static void main(String[] args) { Coche coche1 = new Coche( "Kia", "Niro", 0, new Rueda[] { new Rueda("Pirelli", 2.5f), new Rueda("Pirelli", 2.5f), new Rueda("Pirelli", 2.5f), new Rueda("Pirelli", 2.5f), }, new Rueda("Michelline", 2.0f) ); coche1.cambiarRueda(2); System.out.println(coche1.toString()); }
Marca: Kia Modelo: Niro Velocidad: 0 Ruedas: [Marca:Pirelli, Presión:2.5] [Marca:Pirelli, Presión:2.5] [Marca:Michelline, Presión:2.0] [Marca:Pirelli, Presión:2.5] Rueda de repuesto: Sin rueda de repuesto
Nuestra clase Moto, quedaría de la siguiente forma:
public class Moto{ private String marca, modelo; private int velocidad; private Rueda[] ruedas; public Moto(String marca, String modelo, int velocidad, Rueda[] ruedas) { this.marca = marca; this.modelo = modelo; this.velocidad = velocidad; this.ruedas = ruedas; } public void acelerar(int incremento) { this.velocidad += incremento; } public void frenar(int decremento) { this.velocidad -= decremento; } @Override public String toString(){ String respuesta; respuesta = "Marca: " + this.marca + "\nModelo: " + this.modelo + "\nVelocidad: " + this.velocidad + "\nRuedas: "; for (Rueda rueda : ruedas) { respuesta += rueda.toString() + " "; } return respuesta; } }
Creamos una moto para comprobar que todo funciona bien:
Moto moto1 = new Moto( "Honda", "NC 750", 0, new Rueda[] { new Rueda("Pirelli", 2.8f), new Rueda("Pirelli", 2.8f), } ); System.out.println(moto1.toString());
Marca: Honda Modelo: NC 750 Velocidad: 0 Ruedas: [Marca:Pirelli, Presión:2.8] [Marca:Pirelli, Presión:2.8]
De esta forma, podríamos añadir todas las clases que necesitaran nuestros vehículos (motor, carrocería…) sin obligar a que todos tuvieran las mismas piezas.
Hay varias consideraciones que tenemos que tener en cuenta cuando usamos composición:
Para el primer problema, tenemos varias soluciones. Una posible solución, sería combinar la herencia con la composición. Podríamos crear la clase Vehiculo que contiene objetos de la clase Rueda con los métodos comunes, y las clases hijas Coche y Moto. El problema, es que tendríamos los mismos problemas que usando solo herencia.
Otra solución bastante habitual es usar una clase aparte con los métodos comunes. Estas clases suelen llamarse helpers y sus métodos suelen ser estáticos. Por ejemplo, podríamos crear la clase VehiculoHelper con los métodos acelerar() y frenar(). Esos métodos recibirían, por ejemplo, la velocidad actual del vehículo (coche o moto) y el incremento/decremento (obviamente, con métodos tan sencillos como los que estamos haciendo no valdría la pena, al fin y al cabo lo único que hacemos es sumar/restar un número).
El segundo problema (no poder usar polimorfismo, con lo útil que es) tiene una solución muy sencilla como veremos en el siguiente tema: el uso de interfaces.
Ejercicio 1
Crea la clase Product con los atributos id y price. Haz el constructor y los getters correspondientes, además del métodos toString(). Crea dos clases que hereden de Product: Clothes y Books.
Ejercicio 2
Añade los atributos type, size y colour a la clase Clothes, y author y title a la clase Books. Haz que los constructores de ambas clases reciban como parámetros todos los atributos (los de la clase padre y los suyos propios).
Ejercicio 3
En tu clase principal, crea varios productos de ambas clases (ropa y libros) y haz un método que muestre por pantalla los productos creados (almacena los productos en una lista de productos en tu clase principal).
Ejercicio 4
Sobreescribe el método toString() en ambas clases hijas para que se muestren los atributos propios de cada clase.
Ejercicio 5
Crea una constante en la clase Book llamada DISCOUNT con valor 0.8. Haz que el método getPrice() de esa clase devuelva el precio aplicándole el descuento correspondiente.
Ejercicio 6
Modifica el método de la clase principal que muestra por pantalla los productos para que se puede elegir los productos a mostrar (todos, la ropa o los libros).
Ejercicio 7
Crea 3 clases llamadas HD, Memory y CPU. La clase HD tendrá las propiedades type y capacity, la clase Memory la propiedad capacity y la clase CPU model y speed.
Haz los correspondientes getters, el método toString() y el constructor en cada una de las clases recién creadas.
Crea una nueva clase que herede de Product llamada Computer. Esta clase estará compuesta de una memoria, una CPU y un disco duro. En la clase principal, crea un nuevo ordenador y modifica el método que muestra por pantalla los productos para que se pueda elegir mostrar solo los ordenadores.