Herencia

Herencia

 

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.

 

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.

 

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);

  }

 

}

 

Ejemplo de la clase complejo

 

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;

  }

}

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++.

 

Regresar.