Herencia (II)
Vamos a seguir estudiando la herencia en Java, que tiene cosas algo más
avanzadas que las que vimos en el primer capítulo dedicado a ella.
Clases y métodos abstractos
Como vimos anteriormente, es posible que con la herencia terminemos
creando una familia de clases con un interfaz común. En esos casos es posible,
y hasta probable, que la clase raíz de las demás no sea una clase útil, y que
hasta deseemos que el usuario nunca haga instancias de ella, porque su
utilidad es inexistente. No queremos implementar sus métodos, sólo declararlos
para crear una interfaz común. Entonces declaramos sus métodos como abstractos:
public abstract void mi_metodo();
Como vemos, estamos declarando el método pero no implementandolo, ya que
sustituimos el código que debería ir entre llaves por un punto y coma. Cuando
existe un método abstracto deberemos declarar la clase abstracta o el
compilador nos dará un error. Al declarar como abstracta una clase nos
aseguramos de que el usuario no pueda crear instancias de ella:
Abstractos.java
abstract class Mamifero {
String especie, color;
public abstract void mover();
}
class Gato extends Mamifero {
int numero_patas;
public void mover() {
System.out.println("El gato es el que se mueve");
}
}
public class Abstractos {
public static void main(String[] args) {
Gato bisho = new Gato();
bisho.mover();
}
}
En nuestro ejemplo de herencia, parece absurdo pensar que vayamos a crear
instancias de Mamifero, sino de alguna de sus
clases derivadas. Por eso decidimos declararlo abstracto.
Interfaces
Los interfaces tienen como misión en esta vida llevar el concepto de
clase abstracta un poco más lejos, amén de permitirnos algo parecido a la
herencia múltiple. Pero vamos pasito a pasito. Un interfaz es como una clase
abstracta pero no permite que ninguno de sus métodos esté implementado. Es como
una clase abstracta pero en estado más puro y cristalino. Se declaran
sustituyendo class por interface:
interface Mamifero {
String especie, color;
public void mover();
}
class Gato implements Mamifero {
int numero_patas;
public void mover() {
System.out.println("El gato es el que se mueve");
}
}
No tenemos que poner ningún abstract en
ningún sitio porque ya se le supone. Hay que fijarse también que ahora
Gato no utiliza extends
para hacer la herencia, sino implements.
Sin embargo, la mayor utilidad de los interfaces consiste en permitir la
existencia de herencia múltiple, que consiste en que una clase sea heredera de
más de una clase (que tenga varios papás, vamos). En C++ existía pero daba
enormes problemas, al poder estar implementado un mismo método de distinta
forma en cada una de las clases padre. En Java no existe ese problema. Sólo
podemos heredar de una clase, pero podemos a su vez heredar de uno o varios
interfaces (que no tienen implementación). Modifiquemos nuestro adorado
ejemplo gatuno:
Interfaces.java
interface PuedeMoverse {
public void mover();
}
interface PuedeNadar {
public void nadar();
}
class Mamifero {
String especie, color;
public void mover() {
System.out.println("El mamífero se mueve");
}
}
class Gato extends Mamifero
implements PuedeMoverse, PuedeNadar {
int numero_patas;
public void mover() {
System.out.println("El gato es el que se mueve");
}
public void nadar() {
System.out.println("El gato nada");
}
}
public class Interfaces {
public static void main(String[] args) {
Gato bisho = new Gato();
bisho.mover();
bisho.nadar();
}
}
Vemos que Gato tiene la obligación de
implementar los métodos mover() y
nadar(), ya que sino lo hace provocará un error
de compilación. Podría no implementar mover(),
ya que hereda su implementación de Mamifero.
Pero si decidiéramos no hacerlo no habría problemas, ya que tomaría la
implementación de su clase padre, ya que los interfaces no tienen
implementación. Así nos quitamos los problemas que traía la herencia
múltiple de C++.
Modificador final
Si declaramos un método como final,
indicaremos que no puede ser sobreescrito en clases heredadas, por lo que
mantendrá su implementación. Si es la clase la que lleva el modificador,
entonces nos aseguramos de que nadie pueda heredar de dicha clase (la castramos,
vaya). Por último, si declaramos una propiedad como final,
crearemos una constante, ya que dicha variable se puede inicializar, pero no
modificar. Este último caso presenta un uso más habitual de este modificador:
class Dias {
static final int LUNES=0, MARTES=1, MIERCOLES=2, JUEVES=3,
VIERNES=4, SABADO=5, DOMINGO=6;
}
Como la clase es estática podremos acceder a las constantes como
Dias.LUNES. Lo bueno del modificador
final es que nos aseguramos que nadie pueda
cambiar el valor de esas propiedades.