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:


Motivación

Los objetos de las clases Box, Ellipse y Text representan figuras geométricas. Estos objetos se crean por ejemplo mediante:

    Box box= new Box(30, 30, 30, 15); // x, y, ancho, alto
    Ellipse eli= new Ellipse(20, 20, 30, 10);
                                      // xcentro, ycentro, rhoriz, rvert
    Text text=  new Text("palabra", 10, 10);
                                      // string, x, y (ezquina sup. izq.)
Todos estos objetos poseen implementaciones distintas porque son de naturaleza distinta. Sin embargo, se puede distinguir un grupo de operaciones comunes a todos ellos:

Ejemplo Significado Encabezado
box.moveTo(20, 30); Mueve la figura geométrica de modo que el punto de referencia quede en (20,30) moveTo(int x, int y)
int x= eli.getX() Obtiene la coordenada x del punto de referencia int getX()
int y= text.getY() Obtiene la coordenada y del punto de referencia int getY()

Debe entenderse que cada una de las operaciones se aplica a cualquera de estos objetos. Por ejemplo, moveTo se puede invocar también con la elipse o el texto.

Para dibujar estos objetos en la pantalla se dispone de la clase Animator. Esta clase admite las siguientes operaciones:

Ejemplo Significado Encabezado
Animator anim= new Animator("Figuras geometricas"); Crea una ventana en donde se observarán las figuras geométricas. El título de la ventana será "Figuras geometricas". Animator(String titulo)
anim.push(box); Coloca la caja box en la ventana anim. void push(Glyph glyph)
anim.push(eli); Coloca la elipse eli en la ventana anim.
anim.push(text); Coloca el texto text en la ventana anim.
anim.sleep(50); Dibuja todos las figuras geométricas y hace una pausa de 50 milisegundos void sleep(int milis)

Observe que el parámetro que recibe la operación push es de tipo Glyph.

Problema 1

Escribir un procedimiento que desplace la caja hacia un punto (x,y) a una cierta velocidad expresada en pixels por segundo. El procedimiento será invocado por ejemplo mediante:

    mover(anim, box, 120, 100, 80);
Solución: mover la caja a intervalos de 50 milisegundos. La caja aparentará moverse en forma continua, de la misma forma que una imagen en la televisión parece ser continua a pesar de que se dibujan 30 cuadros por segundo.

   void mover(Animator anim, Box box, int xf, int yf, int v) {
     double xi= box.getX();
     double yi= box.getY();
     double dx= xf-xi;
     double dy= yf-yi;
     double d= sqrt(dx*dx+dy*dy);
     double vx= v*dx/d;
     double vy= v*dy/d;
     double tf= d/v;
     double t= 0;
     double dt= 0.05;

     while (t<tf) {
       box.moveTo(trunc(xi+t*vx+0.5), trunc(yi+t*vy+0.5));
       anim.sleep(trunc(dt*1000));
       t+= dt;
     }

     anim.sleep(trunc((tf-t)*1000));
     box.moveTo(xf, yf);
  }
Problema 2

Escribir un procedimiento similar que desplace la elipse.

Solución:

Se podría escribir un nuevo procedimiento mover que ahora recibiese un objeto de la clase Ellipse. Pero lo mismo habría que hacer si se quisiese mover un texto. La programación orientada a objetos permite escribir una sola solución para el problema de mover las distintas figuras geométricas. Esta solución se base en subclases.

Subclases

Las clases Box, Ellipse y Text son subclases de Glyph, que representa la clase de todas las figuras geométricas. Dado que todas las figuras geométricas admiten las operaciones moveTo, getX y getY, se puede escribir un procedimiento mover que reciba una figura geométrica como argumento:

   void mover(Animator anim, Glyph glyph, int xf, int yf, int v) {
     double xi= glyph.getX();
     double yi= glyph.getY() ;
     ...

     while (t<tf) {
       glyph.moveTo(trunc(xi+t*vx+0.5), trunc(yi+t*vy+0.5));
       anim.sleep(trunc(dt*1000));
       t+= dt;
     }

     anim.sleep(trunc((tf-t)*1000));
     glyph.moveTo(xf, yf);
  }
Con este procedimiento es posible escribir:

    mover(anim, box, 120, 100, 80);
    mover(anim, eli, 200, 100, 100);
    mover(anim, text, 100, 200, 70);
Aparentemente, aquí habría un error porque un objeto que pertenece a la clase Box no puede usarse como argumento cuando lo que se espera es un objeto de la clase Glyph. Sin embargo, en este caso box, eli y text también pertenecen a la clase Glyph, porque Box, Ellipse y Text son subclases de Glyph.

Definición: Subclases Cuando una clase X es una subclase de Y, todos los objetos de la clase X también pertenecen a la clase Y y por lo tanto se pueden colocar en cualquier lugar en donde es válido colocar objetos de la clase Y.

Por otra parte, cuando X es una subclase de Y, todas las operaciones que admiten los objetos de la clase Y, también son aplicables a los objetos de la clase X. La inversa normalmente no es cierta. Los objetos de la subclase pueden poseer operaciones que no están en otros objetos de la clase. Por ejemplo, los objetos de la clase Box poseen el método setDim que cambia el tamaño de la caja:

    box.setDim(60, 30); // ancho, alto
La siguiente instrucción no es válida porque setDim no es una operación válida para objetos de la clase Text.

    text.setDim( ... ); // error!
Desde un punto de vista de teoría de conjuntos, si denotamos por Box, Ellipse, Text y Glyph los conjuntos de objetos de cada clase, entonces Box, Ellipse y Text son subconjuntos de Glyph.


Proyección

Dado que Box es una subclase de Glyph, entonces se puede escribir:

    Glyph gl= box;
porque box también es un Glyph. Esto se denomina una proyección y consiste en asignar a una variable de tipo Y, un objeto de una subclase de Y.

Normalmente, la asignación inversa (que no es una proyección) es un error:

    Box box2= gl; // error!
porque el compilador no puede garantizar que el objeto referenciado por la variable gl sea efectivamente de la clase Box. Java garantiza en compilación o en ejecución que una variable de tipo Box sólo referencia objetos de la clase Box y que una variable de tipo Glyph sólo referencia objetos de la clase Glyph. En el caso de la proyección, la asignación se puede hacer porque un objeto de la clase Box también es de la clase Glyph.

Al realizar una proyección se ocultan las operaciones específicas del tipo inicial. Por ejemplo:

    Glyph gl= box; // proyección
    gl.setDim(30, 15); // error! setDim no es válida sobre un Glyph

Pertenencia

Se puede consultar si un objeto pertenece a una determinada clase mediante el operador instanceof:

    Glyph gl= new Box(...);
    ...
    if (gl instanceof Box) { // verdadero
      ...
    }
    if (gl instanceof Ellipse) { // falso
      ...
    }
La forma general es:

expresión instanceof Clase

Es importante hacer notar que aún cuando se tenga seguridad de que el objeto pertenece a la clase Box, todavía no se puede invocar sus operaciones específicas.

    if (gl instanceof Box) { // verdadero
      gl.setDim(30, 15); // error
    }
    if (gl instanceof Ellipse) { // falso
      ...
    }

Estrechamiento

Si se está seguro que un objeto pertenece a una clase determinada, se puede asignar a una variable de ese tipo utilizando un cast:

    Glyph gl= ...;
    ...
    if (gl instanceof Box) { // verdadero
      Box box2= (Box)gl;
      box2.setDim(30,15);
      ...
    }
    if (gl instanceof Ellipse) { // falso
      Ellipse eli2= (Ellipse)gl;
      ...
    }
Observe que al conocer un objeto por su clase más específica (su subclase), es posible invocar las operaciones de la subclase (setDim en el ejemplo).