Definición de Subclases

Objetivos: Mostrar el concepto de subclases como una manera de lograr el reuso de código y el manejo uniforme de tipos de datos con implementaciones diversas, pero con una interfaz común.

Temas:


Definición de clases con subclases

Una clase que posee subclases se define de la misma forma que se define una clase normal. La diferencia es que algunos métodos pueden quedar parcialmente especificados, a la espera que se complete su definición en las subclases.

Por ejemplo, todos los glyphs tienen operaciones comunes como getX, getY y moveTo. Además, desde el punto de vista de representación, todos tienen un punto de referencia. Estas operaciones se definen normalmente en la clase:

en la clase:

    class Glyph extends Program {
      int x, y; // el punto de referencia
      // las operaciones comunes a todos los glyphs
      int getX() {
        return x;
      }
      int getY() {
        return y;
      }
      void moveTo(int x, int y) {
        this.x= x;
        this.y= y;
      }
      void draw(Pizarra p) {
      }
    }
Los métodos getX, getY y moveTo son los métodos que se usaron la clase pasada. Son métodos definidos para el usuario de esta biblioteca de clases. En cambio el método draw nunca será usado por el usuario de la biblioteca. Este método se coloca en la clase Glyph para poder implementar la clase Animator. Esta clase mantiene un arreglo con todos los glyphs puestos en la ventana. Cuando llega el momento de dibujar todos estos glyphs, basta ejecutar el siguiente código:

    int i= 0;
    while (i<numGlyphs) {
      glyphs[i].draw(p);
      i= i+1;
    }
en donde glyphs es el arreglo de objetos de la clase Glyph y p es un objeto de la clase Pizarra (representa la ventana).

Es importante destacar que el usuario utilizará finalmente las subclases de Glyph, es decir Box, Ellipse y Text, y no Glyph propiamente tal. Sin embargo, es útil definir la clase Glyph, porque es el factor común que permite simplificar el uso y la definición de las subclases. El programador de las subclases usará Glyph como un molde, preocupándose solamente de los aspectos específicos de las subclases.


Definición de subclases

La sintaxis de definición de las subclases es la misma de la de una clase normal, excepto por el uso de la palabra extends. Por ejemplo, la clase Box se define de la siguiente forma:

    class Box extends Glyph {
      int w; // nuevas variables de instancia
      int h;
      // el constructor
      Box(int x, int y, int w, int h) {
        this.x= x;
        this.y= y;
        this.w= w;
        this.h= h;
      }
      // redefinición de métodos
      void draw(Pizarra p) {
        p.drawRect(x, y, x+w, y+h);
      }
      // nuevos métodos
      void setDim(int w, int h) {
        this.w= w;
        this.h= h;
      }
    }
Explicaciones:

Herencia

Una de las ventajas de la definición de subclases es la herencia. La herencia consiste en que la clase derivada hereda todas las características de la clase base. Por lo tanto, no es necesario escribir un método moveTo para cada subclase de Glyph. Al colocarlo en la clase base se coloca automáticamente en todas las subclases.

Una subclase hereda de su clase base:

Como la clase base ha heredado las variables de instancia y métodos de su propia clase base, entonces la subclase también las hereda. Es decir, la herencia también es transitiva.

Por ejemplo, la clase Program define los métodos println, readLine, readInt, etc. Estos métodos son heredados por la clase Glyph y por lo tanto por la clase Box.

Observación importante: la subclase no hereda los constructores.


Redefinición de métodos

En ocasiones, la definición de un método de la clase no es la adecuada en una subclase. Entonces ese método se redefine en la subclase. Por ejemplo, en la clase Glyph el método draw no hace nada porque no se conoce la forma que adoptará el glyph. Por lo tanto, en la clase Box se redefine de modo que dibuje la caja.

      void draw(Pizarra p) {
        p.drawRect(x, y, x+w, y+w);
      }
Los métodos redefinidos se distinguen de la definición de nuevos métodos porque poseen el mismo nombre, número y tipo de paramétros que un método de la clase base (que eventualmente fue heredado de otra clase).

Enlace dinámico

En el siguiente código:

    Pizarra p= ...;
    Glyph gl= new Box(100, 200, 20, 30);
    ...
    gl.draw(p);
¿Qué método se invoca? ¿El que se definió en la clase Glyph? ¿El que se definió en la clase Box?

La respuesta es que se invoca el de la clase Box.

En el código que dibuja todos los glyphs del animator:

    int i= 0;
    while (i<numGlyphs) {
      glyphs[i].draw(p);
      i= i+1;
    }
Los glyphs pertenecerán a distintas subclases: Box, Ellipse y Text. Este código invoca el método que se ha redefinido en cada una de estas subclases. En realidad el método draw definido en la clase Glyph nunca será invocado porque no existirán objetos que sean exactamente de la clase Glyph.

Aún así es necesario declarar el método draw en la clase Glyph para indicar que es un método común a todas las subclases.


Definición de nuevos métodos

Las subclases pueden agregar métodos que son específicos de esa clase. Esto significa que solo podrán ser invocados para objetos de esa subclase. No pueden ser usados con objetos de otra subclase. Por ejemplo, el método setDim permite cambiar el tamaño de una caja:

      void setDim(int w, int h) {
        this.w= w;
        this.h= h;
      }
La desventaja es que estos métodos no pueden ser invocados a partir de variables que sean de tipo Glyph:

    Box box= new Box( ... );
    Glyph gl= new Box( ... );
    ...
    box.setDim( ... ); // Ok
    gl.setDim( ... );  // error!

Ordenamiento Genérico de arreglos

Hasta el momento, cada vez que necesitamos ordenar algún tipo de arreglo debemos programar un nuevo procedimiento de ordenamiento. Esto es desventajoso porque corremos el riesgo de cometer errores que se podrían evitar si pudiésemos escribir una vez el algoritmo de ordenamiento y poder reutilizarlo con distintos tipos de arreglos.

Esto se puede lograr gracias al concepto de subclases. La idea consiste en definir una clase que encapsule el algoritmo de ordenamiento En esta clase se dejan pendiente las operaciones de las cuales depende el algortimo de ordenamiento. La metodología toma real sentido cuando se definen susclases que redefinen las operaciones faltantes.

La examinar los distintos algoritmos de ordenamiento se puede observar que la parte dependiente del tipo de arreglo se encuentra en:

Entonces podemos colocar estas partes en métodos de la clase base que incluye el algortimo de ordenamiento. Estos métodos quedan en blanco a la espera que se redefinan en las subclases. A modo de ejemplo definiremos una clase que encapula el ordenamiento por selección y reemplazo. La clase base para el ordenamiento queda entonces como:

    class Ordenador extends Program {
      void seleccion(int n) { // n: número de elementos en el arreglo
        for (int i= 0; i<n; i++) {
          // Buscamos la posicion del minimo en a[i], a[i+1], ..., a[n-1]
          int k= i;
          for (int j= i+1; j<n j++) {
            if (comparar(j, k)<0)
              k= j;
          }
          // intercambiamos a[i] con a[j]
          intercambiar(i, j);
        }
      }
      int comparar(int i, int j) {
        return 0;
      }
      void intercambiar(int i, int j) {
      }
    }

Subclases para ordenar arreglos de tipos primitivos

La clase Ordenador es en sí inútil. La idea es crear subclases de ella para definir el arreglo que se pretende ordenar y el cómo se comparan e intercambiar sus elementos. Por ejemplo, para ordenar un arreglo de enteros se define la siguiente subclase:

    class OrdenadorInt extends Ordenador {
      int[] a;
      OrdenadorInt(int[] a) {
        this.a= a;
      }
      int comparar(int i, int j) {
        if (a[i]<a[j])
          return -1;
        if (a[i]>a[j])
          return 1;
        return 0;
      }
      void intercambiar(int i, int j) {
        int aux= a[i];
        a[i]= a[j];
        a[j]= aux;
      }
    }
Para ordenar un arreglo se debe crear una instancia de la clase OrdenadorInt y luego invocar el método seleccion:

    int[] a= new int[10];
    a[0]= ...;
    a[1]= ...;
    ...
    OrdenadorInt ord= new OrdenadorInt(a);
    ord.seleccion(10);
La dos últimas instrucciones se pueden abreviar en una sola:

    new OrdenadorInt(a).seleccion(10);

Ejercicio: arreglos de strings El arreglo nombres contiene n nombres que deben ordenarse según el orden lexicográfico. Para realizar el ordenamiento se define una subclase de Ordenador:

    class OrdenadorString extends Ordenador {
      String[] s;
      OrdenadorString(int[] s) {
        this.s= s;
      }
      int comparar(int i, int j) {
        if (s[i]<s[j])
          return -1;
        if (s[i]>s[j])
          return 1;
        return 0;
      }
      void intercambiar(int i, int j) {
        String aux= s[i];
        s[i]= s[j];
        s[j]= aux;
      }
    }
Para ordenar el arreglo de nombres basta ejecutar:

    new OrdenadorString(a).seleccion(10);

Subclases para ordenar arreglos de objetos

La clase Persona se define como:

    class Persona extends Program {
      String nombres;
      String apellidos;
      int edad;
      double peso;
      ...
    }
Se dispone de un arreglo de n personas. Para ordenar este arreglo lexicográficamente primero por apellidos y luego por nombre se define la siguiente subclase:

    class OrdenadorPersona extends Ordenador {
      Persona[] personas;
      OrdenadorPersona(Persona[] personas) {
        this.personas= personas;
      }
      int comparar(int i, int j) {
        int cmp= compare(personas[i].apellidos, personas[j].apellidos);
        if (cmp<0)
          return -1;
        if (cmp>0)
          return 1;
        cmp= compare(personas[i].nombres, personas[j].nombres);
        if (cmp<0)
          return -1;
        if (cmp>0)
          return 1;
        return 0;
      }
      void intercambiar(int i, int j) {
        Persona aux= personas[i];
        personas[i]= personas[j];
        personas[j]= aux;
      }
    }
Ahora se desea ordenar el mismo arreglo por edad. Como la clase OrdanedorPersona ya incluye la definición adecuada del arreglo y el intercambio se puede definir una subclase de OrdenarPersona:

    class OrdenadorPersonaXEdad extends OrdenadorPersona {
      // El arreglo se hereda
      OrdenadorString(int[] personas) { // Los constructores no se heredan
        this.personas= personas;
      }
      int comparar(int i, int j) {
        if (personas[i].edad<personas[j].edad)
          return -1;
        if (personas[i].edad>personas[j].edad)
          return 1;
        return 0;
      }
      // El metodo intercambiar se hereda
    }
Ejercicio:

Ordene el arreglo de personas primero descendentemente por edad y ascendentemente por peso cuando las edades coinciden.