Extensión de Clases

5 Extensión de Clases

Cuando se construye una clase se hace atendiendo a unas necesidades concretas. Así, un objeto de la clase A se comportará de una determinada manera. Pero resulta bastante común que la clase A dada no se acomode a las necesidades concretas sino que en algunos casos se necesitaría una clase A1 que, manteniendo las propiedades de la clase A, se comporte como un caso particular de ésta. Esto és, que la clase A1 sea una extensión de la clase A. Esta es una de las ventajas de la programación orientada a objeto que hace posible la reutilización de código. Así, si álguien programó perfectamente la clase A, nosotros no tendremos que realizar todo el trabajo otra vez para conseguir la clase A1, sólo tendremos que añadir la funcionalidad que no esté en A.

5.1 Una Clase Extendida: Herencia

Veamos un ejemplo de clase que implementa un reloj en el que la hora se almacena en campos para la hora, minutos y segundos, y se disponen de métodos para poner la hora del reloj y para mostrar la hora.

/**
*** clase para simular un reloj 
*/
class Reloj {
   protected int modo;  // 12 o 24 horas
   protected int horas;
   protected int minutos;
   protected int segundos;
 
   /** construtor no-arg */
   Reloj(){
      modo=24;  // por defecto modo 24h.
      horas=0;
      minutos=0;
      segundos=0;
   }
   /** constructor con todos los argumentos */
   Reloj(int h, int m, int s){
      modo=24;   // por defecto modo 24 h.
      horas=h % 24;
      minutos=m % 60;
      segundos=s % 60;
   }
 
   /** metodo para poner la hora y el modo */
   void ponerEnHora(int md, int hh, int mm, int ss){
      modo=md;
      horas=hh % 24;
      minutos=mm % 60;
      segundos=ss % 60;
   }
   /** metodo para avanzar el tiempo un segundo */
   void unSegundoMas(){
      segundos++;
      if (segundos==60) {  // un minuto mas
         segundos=0;  // a cero los segundos   
         minutos++;   // y un minuto mas
         if (minutos==60) {   // una hora mas
            minutos=0;
            horas=(horas+1) % 24;
         }
      }
   }
   /** metodo para conocer la hora almacenada */
   String verLaHora(){
      String hms="Son las ";
      if(modo==12){  // modo 12 h.
         hms+=(horas>12)?horas-12:horas;
         hms+=":"+minutos+":"+segundos;
         hms+=(horas>=12)?"pm":"am";
      }
      else {  // modo 24 h.
         hms+=horas+":"+minutos+":"+segundos;
      }
      return(hms);
   }
 
  public static void main(String [] args){
     Reloj r = new Reloj();
     r.ponerEnHora(24,12,24,30);
     System.out.println(r.verLaHora());
     r.unSegundoMas();
     System.out.println(r.verLaHora());
  }
}

Las funciones del reloj construido son un poco limitadas y el siguiente paso podría consistir en conseguir un reloj construido para que sea capaz de considerar también el año, el mes y el día. Este proceso se denomina extensión de una clase para obtener una subclase que hereda los campos y métodos de la superclase de la que parte.

Así, la extensión de la clase reloj original se realizaría de la siguiente manera:

/**
*** clase para relojes con fecha
*/
class RelojAnual extends Reloj{  
   private int año;
   private int mes;
   private int dia;

   /** constructor no-args */
   RelojAnual(){
      super(); // se puede omitir
      año=1998;
      mes=1;
      dia=1;
   }
   /** constructor con todos los datos necesarios */
   RelojAnual(int a, int m, int d, int hh, int mm, int ss){
      super(hh,mm,ss);  // constructor no-arg de la superclase
      año=a;   // y luego completamos la construccion de la subclase
      mes=m;
      dia=d;
   }
   /** avance del tiempo en la nueva clase, depende del método de
       avance del tiempo de la superclase */
   void unSegundoMas(){
      super.unSegundoMas();
      if (horas==0) {  // un nuevo dia
         dia++;
         switch (dia){
         case 29: if (mes==2){  // sin bisiestos
                     mes++;
                     dia=1;
                  }
                  break;
         case 31: if (mes==4 || mes==6 || mes==9 || mes==11) {
                     mes++;
                     dia=1;
                  }
                  break;
         case 32: if (mes==1 || mes==3 || mes==5 || mes==7 || 
                      mes==8 || mes==10){
                     mes++;
                     dia=1;
                  }
                  if (mes==12) {
                     mes=1;
                     dia=1;
                     año++;
                  }
                  break;
         }
      }
   } 
   String verLaHora(){
      String dmahms="["+dia+"/"+mes+"/"+año+"] "+super.verLaHora();
      return dmahms;
   }
   public static void main(String [] args){
      RelojAnual ra = new RelojAnual(1998,12,31,23,59,59);
      System.out.println(ra.verLaHora());
      ra.unSegundoMas();
      System.out.println(ra.verLaHora());
   }
}

La clase RelojAnual, una subclase de Reloj, hereda automáticamente todos los campos y miembros de la clase Reloj.

La herencia nos hace mucho más fácil la generación y el mantenimiento del código al poder reutilizar código escrito y depurado con anterioridad. Pero vamos a estudiar con un poco más de detalle el procedimiento seguido para extender la clase Reloj.

5.2 Los Campos en la Extensión de Clases

Como se ha dicho en temas anteriores, resulta una buena y aconsejable práctica de programación proteger los campos de accesos y manipulaciones no controladas haciéndolos private. De este modo se impide que se pueda acceder a ellos desde fuera de la clase donde se crean. Si hiciéramos esto con el campo horas de la clase Reloj, por ejemplo, resultaría implosible de acceder desde la subclase RelojAnual. Es por esto que se declara protected, junto al resto de los campos. Cuando un campo es declarado protected se está permitiendo el acceso a él desde cualquier clase que extienda esa clase.

Un método protected se comporta del mismo modo, sólo puede ser invocado usando una referencia a un objeto que sea de la clase que declara el método o de alguna de las subclases que extienden esa clase.

5.3 Constructores en las Clases Extendidas

Cuando se extiende una clase, la nueva clase debe elefir uno de los constructores de sus superclase para invocarlo. La parte del objeto controlada por la superclase debe estar adecuadamente construida, además de garantizar un estado inicial correcto para los posibles campos añadidos por la subclase.

En un constructor de la subclase se puede invocar directamente uno de los constructores de la superclase usando la construcción super(), tal y como se puede observar en los constuctores RelojAnual() del ejemplo. Si no se invoca un constructor de la superclase como primera sentencia ejecutable del nuevo constructor, es invocado automáticamente el constructor no-arg de la superclase antes de que se ejecutre ninguna sentencia del nuevo constructor. Si la superclase no tiene un constructor no-arg hay que invocar explícitamente algún constructor de la superclase con argumentos, tal y como se ve en el segundo constructor RelojAnual(), que invoca el constructor de la superclase con las horas, minutos y segundos.

El lenguaje Java proporciona siempre un constructor no-arg por defecto. El constructor no-arg por defecto de una clase extendida comienza invocando el constructor no-arg de la superclase. Sin embargo, si la superclase no tiene un constructor no-arg, la nueva clase extendidad debe porporcionar al menos un constructor.

Cuando se crea un objeto se les asignan valores iniciales a todos sus campos (cero para los tipos numéricos, /u0000 para char, false para boolean y null para las referencias a objetos). A continuación se invoca el constructor. Cada constructor tiene tres fases:

  1. Invocar un constructor de la superclase.
  2. Inicializar los campos usando las sentencias de inicialización.
  3. Ejecutar el cuerpo del constructor.

Y esta secuencia debe ser tenida en cuenta para construir cualquier objeto.

5.4 Anulación y Ocultación

En la clase RelojAnual hemos anulado el método VerLaHora() y hemos sobrecargado el constructor RelojAnual():

La referencia super se puede usar en las invocaciones de método para acceder a métodos de la superclase que están por lo demás anulados en esa clase.

Cuando se anulan métodos la signatura debe ser la misma, así como el tipo devuelto.

Una clase extendida puede cambiar el acceso de los métodos de una superclase, pero sólo si proporciona más acceso.

Los campos no se pueden anular; sólo se pueden ocultar. Si se declara en una clase un campo con el mismo nombre que uno de su superclase, ese otro campo sigue existiendo, pero no se puede acceder directamente a él por su nombre sencillo. Hay que usar super y otra referencia del tipo de la superclase para acceder a él.

5.5 super y this

La palabra clave super está disponible en todos los métodos no estáticos de una clase extendida. En el acceso a campos y la invocación de métodos, super actúa como referencia al objeto actual tomado como instancia de su superclase. Una invocación como super.metodo() usa siempre la implementación del método de la superclase, y no una implementación anulada de ese método situada más abajo en la jerarquía de la clase.

También se puede usar super para acceder a los miembros protected de la superclase.

El uso de las palabras reservadas this y super es doble: el primero es el de poder referenciar los campos y métodos ocultos de una clase y de su superclase. El otro es el de actuar como nombre de método representando los constructores de la clase actual y de su superclase.

5.5.1 this y super para Referenciar Campos

Las variables locales en un método pueden compartir los mismos nombres que los campos del objeto. Las subclases pueden definir sus propios campos para ocultar otros definidos en su superclase. Por todo esto debe existir una manera de referirnos a uno u otro campo. Siempre hay disponibles dos referencias especiales dentro de cualquier métodos para el acceso a los campos ocultados por el método donde nos encontremos: this y super. this se utiliza para referenciar al objeto del método desde donde se utiliza y super se utiliza para acceder a métodos o campos definidos en la superclase.

Por ejemplo, la clase Punto2D que almacena las coordenadas x e y de un punto en dos dimensiones puede definirse de la siguiente manera:

public class Punto2D{
   int x,y;

   Punto2D(int x, int y){
      this.x=x;
      this.y=y;
   }

   double longitud(){
      return Math.sqrt(x*x + y*y);
   }
}

En este caso, this.x representa el campo x mientras que x de la parte derecha de la asignación representa al argumento del método. sqrt() es un método definido en la clase Math del paquete java.lang que sirve para calcular la raíz cuadrada de un número. Se puede definir una subclase con los mismos campos:

public class MiPunto2D extends Punto2D{
   int x,y;

   MiPunto2D(int x, int y){
      this.x=super.x=x;
      this.y=super.x=y;
   }

   double longitud(){
      return Math.sqrt(x*x + y*y);
   }

   double distancia(){
      return Math.abs(this.longitud() - super.longitud());
   }
}

Ahora this.x se refiere al campo x definida en la clase MiPunto2D y super.x se refiere al campo definido en Punto2D. El método abs() se define en la clase Math del paquete java.lang y calcula el valor absoluto de su argumento.

5.5.2 this y super para Referenciar Constructores

Ya hemos comentado que en los constructores de las subclases siempre se realiza una llamada al constructor no-args de la superclase. Este comportamiento se puede omitir utilizando una llamada diferente a los constructores this() o super() del propio objeto o de la superclase, respectivamente.

Por ejemplo, se puede añadir un constructor a la clase Punto2D con un argumento que pone la otra coordenada a cero:

   Punto2D(int x){
      this(x,0);
   }

Podemos definir ahora un constructor no-args para fijar las coordenadas a los valores por defecto de la siguiente manera:

   Punto2D(){
      this(0,0);
   }

Además, el constructor de la clase MiPunto2D puede reescribirse de la siguiente manera:

   MiPunto2D(int x, int y){
      super(x,y);
      this.x=x;
      this.y=y;
   }

En el ejemplo, super se refiere al constructor con dos argumentos enteros definidos en la clase Punto2D.

5.6 Métodos y Clases final

Marcar un método como final significa que ninguna clase extendida puede anular el método para cambiar su comportamiento, es la versión final de dicho método. También se puede declarar clases enteras como finales. Esto implica que no pueden tener subclases y todos los métodos son implícitamente finales.

Las clases y los métodos finales aumentan la seguridad. Si una clase es final, no se puede declara una clase que la extienda y, por tanto, que pueda cambiar su comportamiento. Si un método es final, se puede confiar en su implementación, a menos que invoque métodos no finales. Se podría utilizar, por ejemplo, el método validadContraseña para asegurarse de que hace lo que tiene que hacer, en lugar de ser anulado por otro que siempre devuelva true.

En muchos casos la seguridad que da marcar una clase como final puede lograr se dejando la clase extensible y marcando cada uno de sus métodos como final. De esta manera, se puede confiar en el comportamiento de esos métodos, y al mismo tiempo, se permiten las extensiones que puede añadir funcionalidad sin anular los métodos. Naturalmente, los campos en los que confían los métodos final deben ser private para que una clase extendida no pueda cambiar el comportamiento de los métodos cambiando el contenido de esos campos.

5.7 Bibliografía


Jesús Vegas
Dpto. Informática
Universidad de Valladolid
jvegas@infor.uva.es