Si se supone que somos buenos programando, cuando creemos una clase es
posible que sea algo útil. De modo que cuando estemos haciendo un programa
distinto y necesitemos esa clase podremos incluirla en el código de ese nuevo
programa. Es la manera más sencilla de reutilizar una clase.
También es posible que utilicemos esa clase incluyendo instancias de la
misma en nuevas clases. A eso se le llama composición. Representa una relación
"tiene un". Es decir, si tenemos una clase Rueda y una clase Coche,
es de esperar que la clase Coche tenga cuatro instancias de Rueda:
class Coche {
Rueda rueda1, rueda2, rueda3, rueda
4;
...
}
Sin embargo, en ocasiones, necesitamos una relación entre clases algo más
estrecha. Una relación del tipo "es un". Por ejemplo, sabemos bien
que un gato es un mamífero. Sin embargo es también un concepto más específico,
lo que significa que una clase Gato puede compartir con Mamífero propiedades y
métodos, pero también puede tener algunas propias.
Herencia.java
class Mamifero {
String especie, color;
}
class Gato extends Mamifero {
int
numero_patas;
}
public class Herencia {
public static void main(String[] args) {
Gato bisho;
bisho = new Gato();
bisho.numero_patas = 4;
bisho.color = "Negro";
System.out.println(bisho.color);
}
}
Como vemos en el ejemplo, el objeto bisho no sólo tiene la propiedad
numero_patas, también color que es una propiedad de Mamifero. Se dice que
Mamifero es la clase padre y Gato la clase hija en una relación de herencia.
Esta relación se consigue en Java por medio de la palabra reservada extends.
Pero, además de heredad la funcionalidad de la clase padre, una clase hija
puede sobreescribirla. Podemos escribir un método en la clase hija que tenga el
mismo nombre y los mismos parámetros que un método de la clase padre:
Herencia.java
class Mamifero {
String especie, color;
public void mover() {
System.out.println("El mamífero se mueve");
}
}
class Gato extends Mamifero {
int
numero_patas;
public void mover() {
System.out.println("El gato es el que se mueve");
}
}
public class Herencia {
public static void main(String[] args) {
Gato bisho = new Gato();
bisho.mover();
}
}
Al ejecutar esta nueva versión veremos que se escribe el mensaje de la
clase hija, no el del padre.
Conviene indicar que Java es una lenguaje en el que todas las clases son
heredadas, aún cuando no se indique explícitamente. Hay una jerarquía de
objetos única, lo que significa que existe una clase de la cual son hijas todas
las demás. Este Adán se llama Object y, cuando no indicamos que nuestras clases
hereden de nadie, heredan de él. Esto permite que todas las clases tengan
algunas cosas en común, lo que permite que funcione, entre otras cosas, el
recolector de basura.
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.
Ejemplo de la Clase punto.
Consideremos que deseamos hacer una clase punto en dos dimensiones y después hacer la representación de la misma en tres dimensiones. La clase punto tendrá un constructor y calculará la distancia entre dos puntos.
public class
punto {
double x, y;
public punto() {
x = 0;
y = 0;
}
public punto(double a, double b) {
x = a;
y = b;
}
public void imprime(String a)
{
System.out.println(a +"(" + x +
", " + y +")");
}
public double distancia(punto
a, punto b)
{
double dx, dy;
dx = a.x - b.x;
dy = a.y - b.y;
return Math.sqrt(dx*dx + dy*dy);
}
}
La extensión de la clase punto a clase punto en tres dimensiones, utilizando
herencia queda como:
public class
punto3d extends punto
{
double z;
public punto3d()
{
super();
z = 0;
}
public punto3d(double a, double b, double c)
{
super(a,b);
z = c;
}
public void imprime(String a)
{
System.out.println(a +"(" + x + ", " + y +
", " + z +")");
}
public double distancia(punto3d a, punto3d
b)
{
double dx = super.distancia(a, b);
double dz = a.z - b.z;
return Math.sqrt(dx*dx + dz*dz);
}
}
En este
ejemplo dada la clase vector de dos dimensiones se realiza la implementación de
la clase complejo utilizando herencia.
public class
vector {
double x, y;
vector(double a, double b)
{
x = a;
y = b;
}
vector()
{
x = 0;
y = 0;
}
void suma(vector a, vector b)
{
x = a.x + b.x;
y = a.y + b.y;
}
void resta(vector a, vector b)
{
x = a.x - b.x;
y = a.y - b.y;
}
double Producto_Punto(vector a,
vector b)
{
return (a.x*b.x + a.y*b.y);
}
void imprime()
{
System.out.print("[" + x +
", " + y + "]");
}
}
La
implementación de la clase complejo queda como
public class
complejo3 extends vector{
complejo3(double a, double b, boolean rec)
{
super(a,b);
if(rec == false) polar_rect(a,b,rec);
super.imprime();
}
void polar_rect(double a, double b, boolean rec) {
double mag, ang;
mag = a;
ang = b*Math.PI/180.0;
x = mag * Math.cos(ang);
y = mag *
Math.sin(ang);
}
double Magnitud()
{
return(Math.sqrt(super.Producto_Punto(this,this)));
}
void multiplica(complejo3 a,
complejo3 b)
{
this.x = a.x*b.x - a.y*b.y;
this.y = a.x*b.y + a.y*b.x;
}
}
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++.