Anteriormente hemos visto que son las clases (aquí) y también hemos visto algunos detalles sobre la clase String usando ejemplos (aquí) pero hay cosas más importantes que tendríamos que saber sobre las clases antes de poder trabajar con ellas. Los conceptos más importantes son la herencia y jerarquía.
Las clases de Java, la última frontera. Más o menos es lo que deberíamos pensar, porque todo el tiempo que estemos cerca del mundo de Java estaremos viendo clases y herencia por todas partes.
Y sí, ha llegado el temible momento en que aprenderemos como podemos tener cosas de un tipo ¡¡¡pero que a la vez son de otro tipo!!! Es decir, como he dicho anteriormente, magia negra.
Uno de los conceptos más importantes de la POO (programación orientada a objectos) es la herencia, la cual nos permite reusar un montón de código de una manera correcta. Básicamente esto significa que algunas clases estarán basadas en otras clases y su relación será una relación de jerarquía, siendo siempre el primer elemento de todas las jerarquías la clase Object.
Siempre he pensado que la mejor manera de entender las cosas es con un ejemplo, así que aquí vamos a ver un ejemplo de herencia en dónde podremos ver como se va estructurando la jerarquía entre las clases que creemos.
Aquí tenemos un interfaz. Un interfaz es una clase especial que solamente tiene la firma de los métodos y campos y es muy útil porque nos permite crear una buena jerarquía entre nuestras clases, dado que no es posible instanciar un objeto directamente y obliga a que sus implementaciones tengan los mismos métodos y campos con el código que consideremos oportuno.
En este caso, este interfaz tiene solamente un método, getWheels, que devuelve un valor de tipo int y que no tiene ningún parámetro.
public interface VehicleDesign { int getWheels(); }
Lo siguiente es una clase abstracta. Este es otro tipo especial de clase que se parece bastante más a una clase de las que hemos visto anteriormente pero que no es posible crear un objeto a partir de él.
Si miramos el código de abajo, veremos que tenemos un “implements VehicleDesign” junto al nombre de la clase. Esto significa que implementará la definición del interfaz VehicleDesign, es decir, tendrá los mismos métodos y variables que su padre.
Una de las mayores ventajas que tienen las clases abstractas es que son muy útiles para compartir métodos con sus hijos.
public abstract class Vehicle implements VehicleDesign { int wheels; public Vehicle (int number) { this.wheels = number; } public int getWheels() { return wheels; } }
Y ahora veremos dos hijos de las clase que hemos visto previamente. La clase Car es un hijo de la clase Vehicle y hereda todo el contenido de su padre. Esto no es del todo real, porque dependería de la visibilidad de cada método y de cada variable, pero por ahora nos sirve como aproximación (supondremos que todos los métodos y campos son de tipo “public”) y ya hablaremos de este tema en una próxima entrega.
Lo que vemos al observar la clase es que extendemos la clase abstracta Vehicle que habíamos declarado anteriormente y esto lo hacemos con la palabra reservada “extend” y el nombre de su antecesor.
Dentro de esta clase, nosotros podemos llamar directamente al método getWheels porque es una parte de su definición, ya que lo hereda de su padre. De la misma manera se podría hacer con cualquier otro método o campo que tuviera. En otras entradas hemos visto lo que es un constructor, pero ahora que estamos hablando de los hijos y de herencia, quizás pensemos que funcionan de manera diferente, pero la realidad es que el funcionamiento es el mismo salvo por el detalle de que podemos llamar al constructor de su clase padre a través del comando “super”. En este ejemplo, llamamos al constructor del padre pasándole el valor de 4.
public class Car extends Vehicle { public Car() { super(4); } public boolean isFerrari() { return false; } }
Y esta otra clase, Motorbike, es también una hija de la clase Vehicle y llamamos al constructor del padre con el valor de 2.
public class Motorbike extends Vehicle { public Motorbike () { super(2); } }
Ahora que ya tenemos definida nuestra estructura de datos, vamos a ver unos cuantos ejemplos sobre como funciona la herencia en Java entre estas clases. Si miramos la clase siguiente, clase Main, vemos que solamente tiene un método main que usaremos para poder hacer pruebas con las clases que hemos ido definiendo. También veremos que hay algunas líneas que están comentadas (si no recuerdas que son los comentarios… échale un vistazo a esta entrada donde explico que son los comentarios). Estas líneas comentadas son líneas que están mal pero que las he dejado porque creo que son muy explicativas de ciertas situaciones que nos podemos encontrar.
public class Main { public static void main (String args[]) { Car car1 = new Car(); System.out.println("Car1:" + car1.getWheels()); System.out.println("Car1:" + car1.isFerrari()); // Vehicle car2 = new Vehicle(); Error because Vehicle is an abstract class and it is not possible to instance it Vehicle car3 = new Car(); System.out.println("Car3:" + car3.getWheels()); // System.out.println(car3.isFerrari()); Error because Vehicle doesn´t have that method System.out.println("Car3:" + ((Car)car3).isFerrari()); Vehicle motorbike1 = new Motorbike(); System.out.println("Motorbike1:" + motorbike1.getWheels()); // Vehicle motorbike2 = new VehicleDesign(); Error because it is not possible to instance an interface // VehicleDesign motorbike3 = new Vehicle(); Error because Vehicle is an abstract class and it is not possible to instance it VehicleDesign motorbike4 = new Motorbike(); System.out.println("Motorbike4:" + motorbike4.getWheels()); } }
La primera sección es sobre el objeto car1. Aquí vemos un ejemplo normal de como creamos el objeto car1 a partir de la clase Car y tenemos dos salidas a consola, una que será “Car1: 4” y otra que es “Car1: true”. Si nos fijamos en este ejemplo, a efectos funcionales no estamos viendo nada sobre herencia, solo que parece que tenemos el código del padre en el hijo.
La segunda sección es sobre car2. La línea está comentada porque está mal pero, ¿por qué está mal? Básicamente es que estamos intentando crear un objeto directamente utilizando el constructor de una clase abstracta, lo cuál está mal por la propia definición que da una clase abstracta: no se puede instanciar directamente.
La tercera sección es sobre el objeto car3. Aquí vemos algo bastante raro: “Vehicle car3 = new Car()”. ¿Cómo es que estamos creando un objeto de tipo Vehicle con el constructor de Car? La respuesta se haya fácilmente en la herencia y es que Car es un Vehicle y este es su padre y todos los hijos de una clase son del tipo de su padre, de ahí que podamos crear un objeto de tipo Vehicle (inclusive cuando es una clase abstracta).
De acuerdo, si lo ejecutamos veremos que imprimimos el número de ruedas (4, porque el objeto creado es un coche, Car, y por tanto hemos llamado al constructor de la clase padre con el parámetro acorde gracias a que tenemos la herencia definida) pero entonces, ¿por qué está la siguiente línea comentada?
La respuesta es sencilla, el motivo de esto es que el método isFerrari() está solamente definida en la clase Car, no en la clase Vehicle y por tanto si creamos un objeto de tipo Vehicle, aunque lo hayamos hecho usando el constructor de Car, no disponemos en su definición de ese método. Así que nos toca recordar que solamente podemos llamar directamente a aquello que tenemos definido, aunque el constructor sea otro que tenga definidas otras cosas.
Pero (siempre hay un pero), vemos que hay otra línea más. Y es que aquí podemos utilizar de nuevo magia negra y podemos comprobar si es un ferrari o no. ¿Qué como lo hacemos? Pues con un bonito (o más bien feo) truco con el que forzamos a Java a que interprete un tipo diferente del que realmente es.
Básicamente, nosotros estamos diciéndole a Java que somos muy valientes y que nos atrevemos a que aunque todo diga que es un objeto de tipo tal, usemos otro tipo, con todas las consecuencias que eso tiene. La manera de hacerlo es muy sencilla, solamente tenemos que poner entre paréntesis el tipo de clase que queremos que interprete delante del objeto en cuestión: (clase)objeto. En el ejemplo es “((Car)car3).isFerrari()”, de manera que car3 se vuelve durante un instante un objeto de tipo Car y entonces podemos llamar a todos los métodos de ese tipo, es decir, ahora podremos comprobar si es un ferrari o no.
Este truco nos ofrece muchas posibilidades, pero tenemos que tener mucho cuidado con él, porque al estar forzando a Java que crea que algo es de un tipo perdemos todas las comprobaciones que hace en el momento de compilación, así que si está mal, nos explotará durante la ejecución mediante una excepción (qué también veremos en el futuro). Una forma de controlar esto es que, antes de hacer nada raro, comprobemos si es de ese tipo o no mediante la palabra reservada instanceof (y sí, también hablaremos sobre eso en otro post).
La cuarta sección es sobre motorbike1. Aquí nos encontramos con la misma situación que en la sección tres, salvo que es un objeto creado a partir de un constructor de Motorbike. Es posible hacer esto por la herencia de la clase Vehicle.
La quinta sección es sobre motorbike2 y está mal porque está intentando crear un objeto de tipo Vehicle utilizando el constructor de la interfaz VehicleDesign, lo cuál está mal porque ninguna interfaz puede ser utilizada para crear un objeto.
La sexta sección es sobre motorbike3 y está comentada porque intenta crear un objeto utilizando la interfaz VehicleDesign y el constructor de la clase abstracta Vehicle, lo cual no es posible porque no se pueden crear objetos utilizando una clase abstracta para ello.
La séptima sección es sobre motorbike4 y aquí vemos que estamos creando un objeto utilizando la definición del interfaz VehicleDesign utilizando para ello el constructor de una clase que hereda de la clase que implementa dicho interfaz, en este caso Motorbike.
Con todo el conocimiento que hemos visto aquí sobre herencia y jerarquía entre clases en Java, ahora podrías crear complejos hechizos de magia negra, dado que podrás reutilizar mucho código y estructuras de datos, con lo que todos tus desarrollos en Java mejorarán ostensiblemente.
Aunque no nos confiemos, aun quedan un montón de detalles que veremos en futuras entradas en el blog.