====== 09 - POO IV: Herencia y composición ====== ===== Herencia ===== 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. Fíjate en la forma de redondear la presión de las ruedas cuando utilizamos //floats//. Existen muchas maneras de hacerlo. Lo que hacemos aquí es multiplicar por 10 (al querer tener solo un decimal), redondear al entero más cercano y dividir el resultado por //10.0f// para que solo muestre un decimal. El método //toString()// es un método de la clase //Object//. Como todos los objetos en Java heredan de dicha clase (precisamente en este tema entenderás el concepto de herencia), todos ellos tienen acceso a este método. Lo habitual es sobreescribir (de nuevo, en este tema aprenderás el concepto de sobreescritura) el método para mostrar el estado del objeto como nosotros queramos (por eso tiene el decorador //@Override// antes de la cabecera del método). 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()**. ==== super() ==== Como hemos dicho, para ejecutar métodos de la clase padre tenemos que usar la palabra reservada //super//. Podemos usarla de dos formas: * Si estamos en un constructor o en un método que sobreescribamos de la clase padre, la referencia a //super()// ejecutará el método padre que estemos sobreescribiendo (o el constructor del padre). * Podemos utilizar //super// para hacer referencia a la clase padre, con lo que podemos ejecutar métodos o acceder a propiedades de la clase padre. 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] ==== Añadiendo propiedades y métodos propios a las clases hijas ==== 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**. ==== Sobreescribir 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. En realidad, la anotación @Override sirve para comprobar en tiempo de compilación si estás cumpliendo con los requisitos para sobrescribir un método. Si no usamos la anotación, el código funcionaría igual, pero Java no nos avisaría de posibles errores. 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 [[clase:daw:prog:2eval:poo_visibilidad|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] ==== Clases y métodos finales ==== ¿Recuerdas como definíamos las [[clase:daw:prog:1eval:variables#constantes|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. ==== Polimorfismo ==== 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 [[clase:daw:prog:1eval:collections#conjuntos_set|colecciones]], vimos que era habitual definir el tipo de datos como la interfaz genérica, por ejemplo: Set conjunto = new HashSet<>(); Si te das cuenta, la variable //conjunto// es de tipo //Set//, aunque después le asignamos un tipo distinto (//HashSet//). En el caso anterior, estamos trabajando con //interfaces// (//Set//) en lugar de con herencia, pero el concepto es similar. Cuando veamos //interfaces// volveremos al tema del polimorfismo con //interfaces//. 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 ==== Ventajas y desventajas de la herencia ==== Como hemos visto, la herencia es un mecanismo de la POO que nos ofrece una serie de ventajas muy útiles, entre ellas: * Nos permite estructura nuestra aplicación como una jerarquía de clases. * Favorece la reutilización de código, implementando los métodos comunes en las clases superiores. * Como consecuencia de lo anterior, reduce mucho el código de nuestra aplicación. * Cuenta con un mecanismo muy potente como es el //polimorfismo//, que nos facilita mucho las cosas en según que situaciones. Aunque pueda parecer que es la solución a casi todos nuestros problemas, existen una serie de desventajas que desaconsejan muchas veces su uso: * Una clase no puede heredar de 2 o más clases a la vez. Solo puede tener una clase padre. * Es una relación de tipo //es un/a//, con lo que obliga a una clase hija a ser siempre del tipo clase padre. * Podemos tener problemas si ampliamos la aplicación con situaciones que no hayamos previsto al comienzo del diseño. 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: * Clases hijas que sobreescriben varios métodos de las clases padres. * Clases hijas con métodos o propiedades inservibles o sin sentido. * Clases padres que agrupan clases que no tienen relación entre sí, aunque tengan algún método común. Aunque la herencia puede ser muy útil en según que situaciones, normalmente es preferible **favorecer la composición sobre la herencia**. Que la herencia tenga una serie de desventajas, no quiere decir que no sea útil. Hay situaciones donde nos viene muy bien (como la herencia de //interfaces//). El problema suele ser el mal uso de la herencia. Cuando aprendemos a programar OO, tendemos a tratar de meter herencia en casi todas nuestras clases, aunque no tenga sentido. Como siempre, un buen diseño reduce mucho los problemas futuros de nuestra aplicación. ===== Composición ===== 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. Usando solo herencia también podríamos aumentar la funcionalidad igual que hemos hecho en el ejemplo anterior, pero, según sea el problema, podrías ser bastante más complicado de implementar. 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: * No estamos reutilizando código, ya que ambas clases (//Coche// y //Moto//) tienen métodos similares que estamos codificando 2 veces * No podemos usar //polimorfismo//, con lo que si creamos la clase //Parking// sería complicado añadir vehículos a las plazas 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**. Aunque uno de los consejos más comunes de la POO es **favorecer la composición sobre la herencia**, ambas técnicas no son incompatibles. En nuestro ejemplo de los vehículos, podría ser una buena idea crear una clase padre //Vehículo// añadiendo partes con composición (ruedas, motor...) y crear clases hijas, según las necesidades de nuestra aplicación. ===== Ejercicios ===== ** 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.