viernes, 1 de julio de 2011

POLIMORFISMO

TEMA 8
- Nuevas consideraciones sobre la generalización
   - Por qué olvidar el tipo de un objeto
- El secreto
   - Acoplamiento de las llamadas a métodos
   - Especificación del comportamiento correcto
   - Ampliabilidad
   - Error: "sustitución" de métodos private
   - Error: campos y métodos static
- Constructores y polimorfismo
   - Orden de las llamadas a los constructores
   - Herencia y limpieza
   - Comportamiento de los métodos polimórficos dentro de 
     los constructores   
- Tipos de retorno covariantes
- Diseño de sistemas con herencia
   - Sustitución y extensión
   - Especialización e información de tipos en tiempo de
     ejecución


"En ocasiones me he preguntado, Sr. Babbage, si introduce en la máquina datos erróneos, ¿podrá suministrar las respuestas correctas? Debo confesar que no soy capaz de comprender qué tipo de confusión de ideas puede hacer que alguien plantee semejante pregunta". Charles Babbage (1791-1871)

El polimorfismo, la abstracción de datos y la herencia son las tres características esenciales de un lenguaje de programación orientado a objetos.


Vamos a ver el polimorfismo, también llamado acoplamiento dinámico o acoplamiento tardío o acoplamiento en tiempo de ejecución.
El polimorfismo da otra dimensión a la separación entre interfaz e implementación, con el fin de desacoplar el qué con respecto al como. Mejora la organización y legibilidad del código, también permite crear programas ampliables que pueden crecer tanto en el proceso original de desarrollo como cuando se quieren añadir nuevas características.
El polimorfismo trata la cuestión del acoplamiento en términos de tipos. La herencia permite tratar un objeto como si fuera de su propio tipo o de su tipo base. Esto permite tratar los tipos derivados de una clase como si fueran del mismo tipo, y pueden ser procesados de la misma manera utilizando un mismo fragmento de código. La llamada a un método polimórfico permite que cada tipo exprese su distinción con respecto a los otros tipos similares, siempre que deriven de un mismo tipo base. Así los métodos que se pueden invocar a través de la clase base se comportan de manera diferente.

NUEVAS CONSIDERACIONES SOBRE LA GENERALIZACIÓN
Hemos visto como podemos tomar una referencia a un objeto y tratarla como si fuera una referencia a su tipo base, es el upcasting o generalización. Vamos a ver un ejemplo. En estos ejemplos que vamos a ver tenemos instrumentos que hacen sonar notas (Note), crearemos una enumeración separada Note dentro de un paquete:
//: polymorphism/music/Note.java
// Notes to play on musical instruments.
package polymorphism.music;

public enum Note {
    MIDDLE_C, C_SHARP, B_FLAT; // Etc.
} ///:~


Seguimos con la clase Instrument:
//: polymorphism/music/Instrument.java
package polymorphism.music;
import static net.mindview.util.Print.*;

class Instrument {
  public void play(Note n) {
    print("Instrument.play()");
  }
}
 ///:~


La clase Wind hereda de Instrument:
//: polymorphism/music/Wind.java
package polymorphism.music;

// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
  // Redefine interface method:
  public void play(Note n) {
    System.out.println("Wind.play() " + n);
  }
} ///:~


La clase Music:
//: polymorphism/music/Music.java
// Inheritance & upcasting.
package polymorphism.music;

public class Music {
  public static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
  }
  public static void main(String[] args) {
    Wind flute = new Wind();
    tune(flute); // Upcasting
  }
} /* Output:
Wind.play() MIDDLE_C
*///:~

El método de Music, tune(), acepta una referencia a Instrument, pero también a cualquier cosa que derive de Instrument. Vemos que en main() se pasa una referencia Wind a tune() y no hay que realizar ninguna proyección. La interfaz de Instrument existe en Wind ya que Wind hereda de Instrument. El upcasting o generalización de Wind a Instrument puede estrechar dicha interfaz, pero esa interfaz no puede ser más pequeña que la interfaz completa de Instrument.

Por qué olvidar el tipo de un objeto
Music.java puede parecer extraño. ¿Por qué alguien olvida intencionadamente el tipo de un objeto? Esto es lo que sucede cuando efectuamos una generalización, vamos a ver qué ocurriría si tune() tomara a Wind y a otras clases que deriven de Instrument como argumento. Para ello tendríamos que escribir un método tune() para cada clase derivada de Instrument. Tenemos dos instrumentos más, Stringed (instrumentos de cuerda) y Brass (de metal).
Clase Stringed:
//: polymorphism/music/Music2.java
// Overloading instead of upcasting.
package polymorphism.music;
import static net.mindview.util.Print.*;

class Stringed extends Instrument {
  public void play(Note n) {
    print("Stringed.play() " + n);
  }
}


Clase Brass:
class Brass extends Instrument {
  public void play(Note n) {
    print("Brass.play() " + n);
  }
}


Clase Music2:
public class Music2 {
  public static void tune(Wind i) {
    i.play(Note.MIDDLE_C);
  }
  public static void tune(Stringed i) {
    i.play(Note.MIDDLE_C);
  }
  public static void tune(Brass i) {
    i.play(Note.MIDDLE_C);
  }
  public static void main(String[] args) {
    Wind flute = new Wind();
    Stringed violin = new Stringed();
    Brass frenchHorn = new Brass();
    tune(flute); // No upcasting
    tune(violin);
    tune(frenchHorn);
  }
} /* Output:
Wind.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
*///:~

Fijaos bien, si no utilizamos upcasting o generalización, en la clase Music2 hay que definir un método tune() específico para cada clase derivada de Instrument. Esto requiere un mayor esfuerzo de programación y cada vez que queramos añadir un método tune() o una nueva clase derivada de Instrument, hay que realizar un trabajo adicional. Además el compilador no dará ningún mensaje de error si nos olvidamos de sobrecargar alguno de los métodos, la gestión de tipo se vuelve inmanejable. La herencia no tiene ninguna ventaja, tenemos más posibilidades de equivocarnos y si comparamos el código, el de Music es muchísimo más claro que el de Music2.
Es mucho más fácil escribir un código en el que en un método se tome la clase base como argumento y no ninguna de sus clases derivadas. Es mucho más adecuado olvidarnos de que hay clases derivadas y escribir el código de forma que sólo se entienda con la clase base.
Esto es lo que nos permite hacer el polimorfismo.

Ejercicio 1. Crea una clase Cycle, con subclases Unicycle, Bicycle y Tricycle. Demuestra que se puede generalizar una instancia de cada tipo a Cycle mediante un método ride().

EL SECRETO
En la clase Music, cuando un objeto Wind llama a play() se ejecuta el método adecuado, pero no parece tener sentido que el programa funcione de esa forma. Si examinamos el método tune():
   public static void tune(Instrument i){
      // ...
      i.play(Note.MIDDLE_C);
   }

El método recibe una referencia a Instrument. ¿Cómo puede saber el compilador que una referencia a Instrument apunta a un objeto Wind y no a un objeto Brass o Stringed? El compilador no puede saberlo, para entender esto vamos a ver el tema del acoplamiento.

Acoplamiento de las llamadas a métodos
El conectar una llamada con el cuerpo del método se denomina acoplamiento. Cuando se realiza el acoplamiento antes de ejecutar el programa, se denomina acoplamiento temprano (early binding).
La parte confusa del programa anterior (Music), es la que se refiere al acoplamiento temprano, ya que el compilador no sabe cuál es el método correcto al que hay que llamar cuando sólo disponemos de la referencia Instrument.
La solución es el acoplamiento tardío (late binding), que tiene lugar en tiempo de ejecución. Por eso se le llama también acoplamiento en tiempo de ejecución o acoplamiento dinámico. Si un lenguaje implementa el acoplamiento tardío, debe haber alguna manera de determinar en tiempo de ejecución el tipo de objeto, para llamar al método correcto. El compilador ignora esto pero el mecanismo de invocación del método lo averigua y llama al cuerpo de método correcto. El acoplamiento tardío varía de unos lenguajes a otros, pero lo que está claro es que en todos los objetos debe incorporarse cierta información sobre el tipo de objeto.
Java utiliza el acoplamiento tardío a menos que el método sea estático o de tipo final (los métodos private son implícitamente final). Si tenemos un método final, el compilador generará un código ligeramente más eficiente para las llamadas a los métodos final, aunque en la mayoría de los casos será imperceptible. Si utilizamos final es por decisión de diseño no para mejorar las prestaciones.

Especificación del comportamiento correcto
Ahora que sabemos que Java funciona con acoplamiento tardío, podemos escribir el código de manera que se comunique con la clase base, sabiendo que las clases derivadas funcionarán correctamente con el mismo código. Enviamos un mensaje a un objeto y dejamos que el objeto averigüe qué tiene que hacer.
Un ejemplo clásico en POO es el de las formas. Este ejemplo no significa que la POO sólo sirva para la programación gráfica. En este ejemplo, tenemos la clase base Shape (forma) y varios tipos derivados: Circle (círculo), Square (cuadrado), Triangle (triángulo), etc. Es un ejemplo fácil de entender. El diagrama de herencia es:



La generalización puede tener lugar en una instrucción tan simple como:
   Shape s=new Circle();

Creamos un objeto Circle y la referencia se asigna a un objeto Shape. Un objeto Circle es una forma (Shape), por la herencia.
Si invocamos uno de los métodos de la clase base, que han sido sustituidos en las clases derivadas:
   s.draw();

Se invoca el método adecuado, es decir, Circle.draw() debido al acoplamiento tardío (polimorfismo).
Vamos a ver el ejemplo de las formas de una manera distinta. Primero, crearemos una biblioteca reutilizable de tipos Shape.
Clase Shape:
//: polymorphism/shape/Shape.java
package polymorphism.shape;

public class Shape {
  public void draw() {}
  public void erase() {}
} ///:~


Clase Circle.
/: polymorphism/shape/Circle.java
package polymorphism.shape;
import static net.mindview.util.Print.*;

public class Circle extends Shape {
  public void draw() { print("Circle.draw()"); }
  public void erase() { print("Circle.erase()"); }
} ///:~


Clase Square:
//: polymorphism/shape/Square.java
package polymorphism.shape;
import static net.mindview.util.Print.*;

public class Square extends Shape {
  public void draw() { print("Square.draw()"); }
  public void erase() { print("Square.erase()"); }
} ///:~


Clase Triangle:
//: polymorphism/shape/Triangle.java
package polymorphism.shape;
import static net.mindview.util.Print.*;

public class Triangle extends Shape {
  public void draw() { print("Triangle.draw()"); }
  public void erase() { print("Triangle.erase()"); }
} ///:~


Clase RandomShapeGenerator:
//: polymorphism/shape/RandomShapeGenerator.java
// A "factory" that randomly creates shapes.
package polymorphism.shape;
import java.util.*;

public class RandomShapeGenerator {
  private Random rand = new Random(47);
  public Shape next() {
    switch(rand.nextInt(3)) {
      default:
      case 0: return new Circle();
      case 1: return new Square();
      case 2: return new Triangle();
    }
  }
} ///:~


Clase Shapes:
//: polymorphism/Shapes.java
// Polymorphism in Java.
import polymorphism.shape.*;

public class Shapes {
  private static RandomShapeGenerator gen =
    new RandomShapeGenerator();
  public static void main(String[] args) {
    Shape[] s = new Shape[9];
    // Fill up the array with shapes:
    for(int i = 0; i < s.length; i++)
      s[i] = gen.next();
    // Make polymorphic method calls:
    for(Shape shp : s)
      shp.draw();
  }
} /* Output:
Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()
*///:~

La clase base Shape establece la interfaz común para cualquier clase que herede de ella, representa a todas las formas que pueden dibujarse y borrarse. Cada clase derivada sustituye estas definiciones y así se proporciona un comportamiento distinto para cada tipo específico de forma.
La clase RandomShapeGenerator genera una referencia a un objeto Shape aleatoriamente cada vez que se invoca su método next(). El upcasting se produce en las instrucciones return, el método toma un valor aleatorio y devuelve una referencia a Circle, Square o Triangle dependiendo de este valor, si nos fijamos, el tipo de retorno de este método es Shape.
En el método main() de Shapes, tenemos una matriz de referencias Shape que se rellena con llamadas a RandomShapeGenerator.next(). Una vez rellena la matriz, cuando la recorremos e invocamos el método draw() para cada objeto, tiene lugar el comportamiento correspondiente a cada tipo específico, como por arte de magia.
Al crear las formas aleatoriamente, vemos que el compilador no puede tener los conocimientos adecuados para hacer las llamadas correctas en tiempo de compilación. Las llamadas a draw() tienen lugar mediante el acoplamiento dinámico.

Ejercicio 2. Añade la anotación @Override al ejemplo de procesamiento de formas.
Ejercicio 3. Añade un nuevo método a la clase base de Shapes.java que imprima un mensaje, pero sin sustituirlo en las clases derivadas. Explica lo que sucede. Ahora sustitúyelo en una de las clases derivadas pero no en las otras y mira lo que sucede. Finalmente, sustitúyelo en todas las clases derivadas.
Ejercicio 4. Añade un nuevo tipo de objeto Shape a Shapes.java y verifica que en main() que el polimorfismo funciona para el nuevo tipo al igual que para los tipos anteriores.
Ejercicio 5. Partiendo del Ejercicio 1, añade un método wheels() a Cycle, que devuelva el número de ruedas. Modifica ride() para invocar wheels() y verifica que funciona el polimorfismo.

Ampliabilidad
Vamos ahora con el ejemplo de los instrumentos musicales. Gracias al polimorfismo, podemos añadir nuevos tipos sin modificar el método tune(). En un programa orientado a objetos bien diseñado, la mayoría de los métodos sólo se comunicarán con la interfaz de la clase base.
Ese tipo de programas es extensible (ampliable), ya que se pueden añadir nuevas funcionalidades heredando nuevos tipos de datos a partir de la clase base común. Los métodos que manipulan la interfaz de la clase base no tienen que ser modificados para poder utilizar las nuevas clases.
Si tomamos el ejemplo de los instrumentos y añadimos métodos a la clase base y también clases nuevas. El esquema es el siguiente:



Todas estas nuevas clases funcionan con el antiguo método tune(), no hay necesidad de modificarlo. Incluso si tune() se encontrara en un archivo separado y añadiéramos nuevos métodos a Instrument, tune() seguiría funcionando correctamente, sin necesidad de recompilarlo. Vamos a ver la implementación:
//: polymorphism/music3/Music3.java
// An extensible program.
package polymorphism.music3;
import polymorphism.music.Note;
import static net.mindview.util.Print.*;

class Instrument {
  void play(Note n) { print("Instrument.play() " + n); }
  String what() { return "Instrument"; }
  void adjust() { print("Adjusting Instrument"); }
}

class Wind extends Instrument {
  void play(Note n) { print("Wind.play() " + n); }
  String what() { return "Wind"; }
  void adjust() { print("Adjusting Wind"); }
} 

class Percussion extends Instrument {
  void play(Note n) { print("Percussion.play() " + n); }
  String what() { return "Percussion"; }
  void adjust() { print("Adjusting Percussion"); }
}

class Stringed extends Instrument {
  void play(Note n) { print("Stringed.play() " + n); }
  String what() { return "Stringed"; }
  void adjust() { print("Adjusting Stringed"); }
}

class Brass extends Wind {
  void play(Note n) { print("Brass.play() " + n); }
  void adjust() { print("Adjusting Brass"); }
}

class Woodwind extends Wind {
  void play(Note n) { print("Woodwind.play() " + n); }
  String what() { return "Woodwind"; }
} 

public class Music3 {
  // Doesn't care about type, so new types
  // added to the system still work right:
  public static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
  }
  public static void tuneAll(Instrument[] e) {
    for(Instrument i : e)
      tune(i);
  } 
  public static void main(String[] args) {
    // Upcasting during addition to the array:
    Instrument[] orchestra = {
      new Wind(),
      new Percussion(),
      new Stringed(),
      new Brass(),
      new Woodwind()
    };
    tuneAll(orchestra);
  }
} /* Output:
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C
*///:~

Los nuevos métodos son what(), que devuelve una referencia String con una descripción de la clase y adjust(), para ajustar cada instrumento.
En main(), tenemos una matriz de Instrument, al inicializar cada elemento de esta matriz se produce automáticamente una generalización a Instrument.
El método tune() ignora los cambios de código que se han producido y sigue funcionando correctamente. Esta funcionalidad es la que da el polimorfismo. Los cambios en el código no generan ningún problema en aquellas partes del programa que no deban verse afectadas. Mediante el polimorfismo el programador puede separar las cosas que cambian de las que permanecen.

Ejercicio 6. Modifica Music3.java de modo que what() se convierta en el método toString() del objeto raíz Object. Prueba a imprimir los objetos Instrument utilizando System.out.println() (sin efectuar ninguna proyección de tipo).

Ejercicio 7. Añade un nuevo tipo de objeto Instrument a Music3.java y verifica que el polimorfismo funciona para el nuevo tipo.

Ejercicio 8. Modifica Music3.java para que genere aleatoriamente objetos Instrument de la misma forma que lo hace Shapes.java.

Ejercicio 9. Crea una jerarquía de herencia Rodent: Mouse, Gerbil, Hamster, etc (roedor: ratón, jerbe, hamster, etc.). En la clase base proporciona los métodos que son comunes para todos los roedores, y sustituya estos métodos en las clases derivadas para obtener diferentes comportamientos dependiendo del tipo específico de roedor. Crea una matriz de objetos Rodent, rellénala con diferentes tipos específicos de roedores e invoque los métodos de la clase base para ver lo que sucede.

Ejercicio 10. Crea una clase base con dos métodos. En el primer método, invoca el segundo método. Define una clase que herede de la anterior y sustituya el segundo método. Crea un objeto de la clase derivada, realiza una generalización (upcasting) al tipo base y llama al primer método. Explica lo que sucede.

Error: "sustitución" de métodos private

Vamos a ver un ejemplo de un error que podemos cometer sin querer:
//: polymorphism/PrivateOverride.java
// Trying to override a private method.
package polymorphism;
import static net.mindview.util.Print.*;

public class PrivateOverride {
  private void f() { print("private f()"); }
  public static void main(String[] args) {
    PrivateOverride po = new Derived();
    po.f();
  }
}

class Derived extends PrivateOverride {
  public void f() { print("public f()"); }
} /* Output:
private f()
*///:~

Se podría esperar que la salida fuera "public f()", pero el método f() de PrivateOverride es privado y, por tanto, final, por esta razón está oculto para la clase derivada. La salida es "private f()". El método f() de la clase Derived, es un método nuevo, ni siquiera está sobrecargado, ya que el método f() de PrivateOverride es invisible para Derived.
Los métodos privados no pueden ser sustituidos, así que hay que tener cuidado ya que si intentamos sustituir un método privado el compilador no generará ninguna advertencia y seguramente el programa no hará lo que se espera. Para evitar confusiones, es aconsejable utilizar en la clase derivada un nombre diferente al del método private de la clase base.

Error: campos y métodos static
Una vez conocido el polimorfismo, no hay que pensar que todo ocurre polimórficamente. Las únicas llamadas que pueden ser polimórficas son las llamadas a métodos normales. Por ejemplo, si accedemos a un campo directamente, el acceso se resolverá en tiempo de compilación, vamos a ver un ejemplo:
//: polymorphism/FieldAccess.java
// Direct field access is determined at compile time.

class Super {
  public int field = 0;
  public int getField() { return field; }
}

class Sub extends Super {
  public int field = 1;
  public int getField() { return field; }
  public int getSuperField() { return super.field; }
}

public class FieldAccess {
  public static void main(String[] args) {
    Super sup = new Sub(); // Upcast
    System.out.println("sup.field = " + sup.field +
      ", sup.getField() = " + sup.getField());
    Sub sub = new Sub();
    System.out.println("sub.field = " +
      sub.field + ", sub.getField() = " +
      sub.getField() +
      ", sub.getSuperField() = " +
      sub.getSuperField());
  }
} /* Output:
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
*///:~

Cuando un objeto Sub se generaliza a una referencia Super, los accesos a los campos se resuelven por el compilador, por tanto, no son polimórficos. Hay un espacio de almacenamiento distinto para Super.field y Sub.field. Sub contiene dos campos denominados field, el suyo y el de la clase Super. Cuando hacemos referencia al campo field de Super no se genera de forma predeterminada una referencia a la versión almacenada en Super, para acceder al campo field de Super es necesario escribir super.field.
Esto puede parecer confuso, pero en realidad no llega a ocurrir casi nunca, generalmente se definen los campos private y se accede a ellos a través de métodos. Normalmente tampoco daremos el mismo nombre a un campo de la clase base y a otro de la clase derivada, esto sería muy confuso.
Si un método es static, no se comporta de forma polimórfica:
//: polymorphism/StaticPolymorphism.java
// Static methods are not polymorphic.

class StaticSuper {
  public static String staticGet() {
    return "Base staticGet()";
  }
  public String dynamicGet() {
    return "Base dynamicGet()";
  }
}

class StaticSub extends StaticSuper {
  public static String staticGet() {
    return "Derived staticGet()";
  }
  public String dynamicGet() {
    return "Derived dynamicGet()";
  }
}

public class StaticPolymorphism {
  public static void main(String[] args) {
    StaticSuper sup = new StaticSub(); // Upcast
    System.out.println(sup.staticGet());
    System.out.println(sup.dynamicGet());
  }
} /* Output:
Base staticGet()
Derived dynamicGet()
*///:~

Fijaos, toma el método statiGet() de StaticSuper y el método dynamicGet() de StaticSub. Los métodos estáticos están asociados con la clase y no con objetos individuales.

CONSTRUCTORES Y POLIMORFISMO
Los constructores se comportan diferente con respecto al polimorfismo. No son polimórficos. Debemos entender muy bien el funcionamiento de los constructores para evitar errores desagradables.

Orden de las llamadas a los constructores
El constructor siempre se invoca durante el proceso de construcción de una clase derivada. Esta llamada provoca un desplazamiento hacia arriba en la jerarquía de herencia, invocándose un constructor para todas las clases base. Una clase derivada sólo tiene acceso a sus propios miembros y no a los de la clase base (aquellos miembros típicamente private). Sólo el constructor de la clase base tiene el conocimiento y acceso adecuados para inicializar sus propios elementos. Por eso es esencial que se invoquen todos los constructores, de lo contrario no podría construirse el método completo. Por ello, el compilador impone una llamada al constructor para cada parte de una clase derivada. Si nosotros no especificamos una llamada al constructor de la clase base en la clase derivada, el compilador invocará automáticamente al constructor predeterminado, en caso de que éste no exista, el compilador generará error (en los casos en que una clase no tenga ningún constructor, el compilador sintetizará automáticamente un constructor predeterminado).
Vamos con un ejemplo en el que se muestran los efectos de la composición, la herencia y el polimorfismo sobre el orden de construcción:
//: polymorphism/Sandwich.java
// Order of constructor calls.
package polymorphism;
import static net.mindview.util.Print.*;

class Meal {
  Meal() { print("Meal()"); }
}

class Bread {
  Bread() { print("Bread()"); }
}

class Cheese {
  Cheese() { print("Cheese()"); }
}

class Lettuce {
  Lettuce() { print("Lettuce()"); }
}

class Lunch extends Meal {
  Lunch() { print("Lunch()"); }
}

class PortableLunch extends Lunch {
  PortableLunch() { print("PortableLunch()");}
}

public class Sandwich extends PortableLunch {
  private Bread b = new Bread();
  private Cheese c = new Cheese();
  private Lettuce l = new Lettuce();
  public Sandwich() { print("Sandwich()"); }
  public static void main(String[] args) {
    new Sandwich();
  }
} /* Output:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
*///:~

Tenemos varias clases con un constructor que se anuncia a sí mismo. La clase Sandwich tiene tres niveles de herencia,Lunch hereda de Meal, PortableLunch hereda de Lunch y Sandwich hereda de PortableLunch, también tiene tres objetos miembro, b, c y l. Vemos que cuando se crea un objeto Sandwich primero se ejecuta el constructor de Meal, luego el de Lunch y por último el de PortableLunch. El orden de llamada a los constructores para un objeto complejo es el siguiente:
1. Se invoca al constructor de la clase base (Meal), en segundo lugar al de la clase derivada (Lunch), seguidamente al de la siguiente clase derivada (PortableLunch) y así hasta el nivel más profundo de la jerarquía (PortableLunch).
2. Los inicializadores de los miembros (b, c y l) se invocan según el orden de declaración.
3. Se invoca el cuerpo del constructor de la clase derivada (Sandwich).
El orden de las llamadas a los constructores es importante. Si utilizamos la herencia podemos acceder a los miembros public y protected de la clase base. Todos los demás miembros de la clase base deben ser válidos cuando los encontremos en la clase derivada. En un método normal, el proceso de construcción ya ha tenido lugar, por lo que todos los miembros de todas las partes del objeto habrán sido construidos. Sin embargo, dentro del constructor debemos asegurarnos de que todos los miembros que utilicemos hayan sido construidos. Esto se consigue invocando primero al constructor de la clase base y así cuando estemos en el constructor de la clase derivada, todos los miembros de la clase base que necesitemos habrán sido inicializados. Sabemos que todos los miembros dentro del constructor son válidos, esta es la razón por la que siempre que sea posible se deben inicializar todos los objetos miembro (incluidos mediante composición, en este caso b, c y l) en su punto de definición. Si trabajamos de esta forma, es más fácil garantizar que los miembros de la clase base y objetos miembro del objeto actual hayan sido inicializados. Sin embargo, esto no permite gestionar todos los casos.

Ejercicio 11. Añade una clase Pickle a Sandwich.java.

Herencia y limpieza
Cuando utilizamos herencia y composición para crear una nueva clase, normalmente no nos debemos preocupar por las tareas de limpieza. Cuando haya algún problema relativo a la limpieza, debemos crear un método dispose() en la nueva clase, (se puede utilizar otro nombre en lugar de dispose). Con la herencia, tenemos que sustituir dispose() en la clase derivada si necesitamos realizar alguna tarea de limpieza especial que forme parte de la depuración de memoria. Cuando necesitemos el método dispose() hay que invocar el de la clase base, ya que si no, las tareas de limpieza de esta clase no se llevarán a cabo. Vamos a ver un ejemplo de esto:
//: polymorphism/Frog.java
// Cleanup and inheritance.
package polymorphism;
import static net.mindview.util.Print.*;

class Characteristic {
  private String s;
  Characteristic(String s) {
    this.s = s;
    print("Creating Characteristic " + s);
  }
  protected void dispose() {
    print("disposing Characteristic " + s);
  }
}

class Description {
  private String s;
  Description(String s) {
    this.s = s;
    print("Creating Description " + s);
  }
  protected void dispose() {
    print("disposing Description " + s);
  }
}

class LivingCreature {
  private Characteristic p =
    new Characteristic("is alive");
  private Description t =
    new Description("Basic Living Creature");
  LivingCreature() {
    print("LivingCreature()");
  }
  protected void dispose() {
    print("LivingCreature dispose");
    t.dispose();
    p.dispose();
  }
}

class Animal extends LivingCreature {
  private Characteristic p =
    new Characteristic("has heart");
  private Description t =
    new Description("Animal not Vegetable");
  Animal() { print("Animal()"); }
  protected void dispose() {
    print("Animal dispose");
    t.dispose();
    p.dispose();
    super.dispose();
  }
}

class Amphibian extends Animal {
  private Characteristic p =
    new Characteristic("can live in water");
  private Description t =
    new Description("Both water and land");
  Amphibian() {
    print("Amphibian()");
  }
  protected void dispose() {
    print("Amphibian dispose");
    t.dispose();
    p.dispose();
    super.dispose();
  }
}

public class Frog extends Amphibian {
  private Characteristic p = new Characteristic("Croaks");
  private Description t = new Description("Eats Bugs");
  public Frog() { print("Frog()"); }
  protected void dispose() {
    print("Frog dispose");
    t.dispose();
    p.dispose();
    super.dispose();
  }
  public static void main(String[] args) {
    Frog frog = new Frog();
    print("Bye!");
    frog.dispose();
  }
} /* Output:
Creating Characteristic is alive
Creating Description Basic Living Creature
LivingCreature()
Creating Characteristic has heart
Creating Description Animal not Vegetable
Animal()
Creating Characteristic can live in water
Creating Description Both water and land
Amphibian()
Creating Characteristic Croaks
Creating Description Eats Bugs
Frog()
Bye!
Frog dispose
disposing Description Eats Bugs
disposing Characteristic Croaks
Amphibian dispose
disposing Description Both water and land
disposing Characteristic can live in water
Animal dispose
disposing Description Animal not Vegetable
disposing Characteristic has heart
LivingCreature dispose
disposing Description Basic Living Creature
disposing Characteristic is alive
*///:~

Cada clase tiene dos objetos miembro de tipo Characteristic y Description, que también habrá que borrar. El orden de borrado debe ser inverso al de inicialización, por si alguno de los subobjetos depende del otro. Para los campos, el orden de borrado es el inverso del orden de declaración (ya que se inicializan en orden de declaración). Para las clases base tenemos que realizar las tareas de limpieza primero de las clases derivadas y luego las de la clase base, esto es así porque las clases derivadas pudieran invocar métodos de la clase base que requieran que los componentes de la clase base continúen siendo accesibles. En el ejemplo vemos que se borran todas las partes del objeto Frog en orden inverso al de creación.
No siempre es necesario realizar tareas de limpieza, pero cuando así sea debemos hacerlo con cuidado.

Ejercicio 12. Modifica el Ejercicio 9 para que se muestre el orden de inicialización de las clases base y de las clases derivadas. Ahora añade objetos miembro a las clases base y derivadas, y muestra el orden en que se lleva a cabo la inicialización durante el proceso de construcción.

Ahora bien, la clase Frog tiene sus objetos miembro, los crea y sabe durante cuánto tiempo tienen que existir (tanto como dure el objeto Frog), por tanto, sabe cuándo invocar dispose() para borrar los objetos miembro. Pero, ¿qué ocurre cuando uno de estos objetos miembro es compartido con otros objetos? En este caso el problema es más complejo. Puede ser necesario un recuento de referencias para saber el número de objetos que siguen pudiendo acceder a un objeto compartido. Veamos un ejemplo:
//: polymorphism/ReferenceCounting.java
// Cleaning up shared member objects.
import static net.mindview.util.Print.*;

class Shared {
  private int refcount = 0;
  private static long counter = 0;
  private final long id = counter++;
  public Shared() {
    print("Creating " + this);
  }
  public void addRef() { refcount++; }
  protected void dispose() {
    if(--refcount == 0)
      print("Disposing " + this);
  }
  public String toString() { return "Shared " + id; }
}

class Composing {
  private Shared shared;
  private static long counter = 0;
  private final long id = counter++;
  public Composing(Shared shared) {
    print("Creating " + this);
    this.shared = shared;
    this.shared.addRef();
  }
  protected void dispose() {
    print("disposing " + this);
    shared.dispose();
  }
  public String toString() { return "Composing " + id; }
}

public class ReferenceCounting {
  public static void main(String[] args) {
    Shared shared = new Shared();
    Composing[] composing = { new Composing(shared),
      new Composing(shared), new Composing(shared),
      new Composing(shared), new Composing(shared) };
    for(Composing c : composing)
      c.dispose();
  }
} /* Output:
Creating Shared 0
Creating Composing 0
Creating Composing 1
Creating Composing 2
Creating Composing 3
Creating Composing 4
disposing Composing 0
disposing Composing 1
disposing Composing 2
disposing Composing 3
disposing Composing 4
Disposing Shared 0
*///:~

El contador static long counter cuenta el número de instancias de Shared que son creadas y también crea un valor para id. El tipo de counter es long en vez de int para evitar el desbordamiento (esta es una buena práctica de programación). La variable id es final porque no va a cambiar durante el tiempo de vida del objeto.
Cuando asociamos el objeto compartido a la clase, hay que invocar addRef(), el método dispose() llevará la cuenta del número de referencias y decidirá cuando proceder con las tareas de limpieza. Esta técnica requiere cierta diligencia por nuestra parte.

Ejercicio 13. Añade un método finalize() a ReferenceCounting.java para verificar la condición de terminación (ver Tema 5, Inicialización y limpieza).

Ejercicio 14. Modifica el Ejercicio 12 para que uno de los objetos miembro sea un objeto compartido. Utilice el método de recuento del número de referencias y demuestra que funciona adecuadamente.

Comportamiento de los métodos polimórficos dentro de los constructores

La jerarquía de llamada a constructores plantea un dilema interesante. ¿Qué pasa si dentro de un constructor invocamos un método con acoplamiento dinámico del objeto que está siendo construido?
En un método normal, la llamada con acoplamiento dinámico se resuelve en tiempo de ejecución, ya que el objeto no sabe si pertenece a la clase en que se encuentra el método o a alguna de las derivadas.
Si invocamos un método con acoplamiento dinámico dentro de un constructor, se utiliza también la definición sustituida de dicho método (la definición del método que se encuentra en la clase actual). Pero el efecto de esta llamada puede ser inesperado, el método sustituido será invocado antes de que el objeto haya sido completamente construido. Esto puede acarrear errores difíciles de detectar.
La tarea del constructor es hacer que el objeto comience a existir. En cualquier constructor puede que el objeto completo sólo esté formado parcialmente, lo único seguro que sabemos es que los objetos de la clase base han sido inicializados. Si el constructor es sólo uno de los pasos a la hora de construir un objeto de una clase derivada de la clase correspondiente a dicho constructor, las partes derivadas no habrán sido inicializadas en el momento en que se invoque el constructor actual. Sin embargo, una llamada a un método con acoplamiento dinámico se "adentra" en la jerarquía de herencia invocando un método dentro de una clase derivada. Si esto lo hacemos en un constructor, podríamos estar invocando a un método que manipulara miembros no inicializados. Vamos a ver este problema con un ejemplo:
//: polymorphism/PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect.
import static net.mindview.util.Print.*;

class Glyph {
  void draw() { print("Glyph.draw()"); }
  Glyph() {
    print("Glyph() before draw()");
    draw();
    print("Glyph() after draw()");
  }
} 

class RoundGlyph extends Glyph {
  private int radius = 1;
  RoundGlyph(int r) {
    radius = r;
    print("RoundGlyph.RoundGlyph(), radius = " + radius);
  }
  void draw() {
    print("RoundGlyph.draw(), radius = " + radius);
  }
} 

public class PolyConstructors {
  public static void main(String[] args) {
    new RoundGlyph(5);
  }
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*///:~

El método draw() de la clase Glyph, es sustituido en la clase derivada RoundGlyph. Pero como podemos comprobar el constructor de Glyph invoca este método y la llamada acaba en el método draw() de la clase RoundGlyph. Observamos que cuando el constructor de Glyph invoca draw() el valor del campo radius no es 1 sino que es 0.
Vamos a ver el proceso real de inicialización, importante para entender el ejemplo anterior:
1. El almacenamiento asignado al objeto se inicializa con ceros binarios antes de que suceda nada.
2. Los constructores de las clases base se invocan tal y como hemos visto antes. En el ejemplo se invoca el método sustituido draw()(antes de que llame al constructor de RoundGlyph) y éste ve que el valor de radius es cero debido al paso 1.
3. Los inicializadores de los miembros se invocan según el orden de declaración.
4. Se invoca el cuerpo del constructor de la clase derivada.
Lo bueno de esto es que todo se inicializa al menos con cero (o con lo que cero signifique para el tipo de datos concreto) y no con datos aleatorios. Esto también incluye referencias a objetos, que tendrán el valor null. Así, si nos olvidamos de inicializar esa referencia, tendremos una excepción en tiempo de ejecución. Como todo lo demás tendrá valor cero, servirá de pista al examinar la salida.
Estos errores podrían quedar ocultos, necesitando mucho tiempo para descubrirlos, ya que la salida es errónea pero el compilador no dice nada.
Una buena práctica a la hora de crear constructores es: "Haz lo menos posible para garantizar que el objeto esté en un estado correcto y, siempre que puedas evitarlo, no invoques ningún otro método de esta clase". Los únicos métodos seguros que se pueden invocar dentro de un constructor son los de tipo final en la clase base (esto también se aplica a los métodos privados, que son automáticamente final). Debemos de tratar de cumplir esta directriz.

Ejercicio 15. Añade una clase RectangularGlyph a PolyConstructors.java e ilustra el problema descrito en esta sección.

TIPOS DE RETORNO COVARIANTES

Java SE5 añade tipos de retorno covariantes, esto significa que un método sustituido en una clase derivada puede devolver un tipo derivado del tipo devuelto por el método de la clase base, veamos un ejemplo:
//: polymorphism/CovariantReturn.java

class Grain {
  public String toString() { return "Grain"; }
}

class Wheat extends Grain {
  public String toString() { return "Wheat"; }
}

class Mill {
  Grain process() { return new Grain(); }
}

class WheatMill extends Mill {
  Wheat process() { return new Wheat(); }
}

public class CovariantReturn {
  public static void main(String[] args) {
    Mill m = new Mill();
    Grain g = m.process();
    System.out.println(g);
    m = new WheatMill();
    g = m.process();
    System.out.println(g);
  }
} /* Output:
Grain
Wheat
*///:~

En versiones anteriores de Java SE5 se obligaría a que el método process() de Wheat devolviera "Grain" en lugar de "Wheat", aunque Wheat derivara de Grain. Los tipos de retorno covariantes permiten utilizar el tipo de retorno Wheat más específico.

DISEÑO DE SISTEMAS CON HERENCIA

Una vez que conocemos el polimorfismo, podríamos pensar en utilizar siempre la herencia, el polimorfismo es una herramienta tan inteligente. Pero no podemos pensar de esta manera, si utilizamos la herencia como primera opción para utilizar una clase existente y formar otra nueva, las cosas se pueden complicar innecesariamente.
Lo correcto es tratar de utilizar primero la composición, sobre todo cuando no sepamos cuál de los dos mecanismos utilizar. La composición no hace que el diseño deba adoptar una jerarquía de herencia. Asimismo, la composición es más flexible ya que permite seleccionar dinámicamente un tipo (y por tanto un comportamiento), la herencia exige que se conozca un tipo exacto en tiempo de compilación. Lo vemos en el siguiente ejemplo:
//: polymorphism/Transmogrify.java
// Dynamically changing the behavior of an object
// via composition (the "State" design pattern).
import static net.mindview.util.Print.*;

class Actor {
  public void act() {}
}

class HappyActor extends Actor {
  public void act() { print("HappyActor"); }
}

class SadActor extends Actor {
  public void act() { print("SadActor"); }
}

class Stage {
  private Actor actor = new HappyActor();
  public void change() { actor = new SadActor(); }
  public void performPlay() { actor.act(); }
}

public class Transmogrify {
  public static void main(String[] args) {
    Stage stage = new Stage();
    stage.performPlay();
    stage.change();
    stage.performPlay();
  }
} /* Output:
HappyActor
SadActor
*///:~

Un objeto Stage contiene una referencia a un objeto Actor y se inicializa para que apunte a un objeto HappyActor. El método performPlay() produce un comportamiento concreto. Como una referencia puede dirigirse a un objeto distinto en tiempo de ejecución, podríamos almacenar una referencia a un objeto SadActor en actor, así el comportamiento producido por performPlay() variaría. De esta forma tenemos una mayor flexibilidad dinámica en tiempo de ejecución (esto se llama patrón de diseño basado en estados, consultar Thinking in Pattern (with Java) en www.MindView.net). Si pensamos en la herencia no podemos realizar la herencia de forma diferente en tiempo de ejecución ya que debe ésta debe estar determinada en tiempo de compilación.
Una regla general dice:"Utilizar la herencia para expresar las diferencias en comportamiento y los campos para expresar las variaciones en el estado". En el ejemplo anterior tenemos los dos mecanismos. Se definen mediante herencia dos clases distintas, SadActor y HappyActor, para expresar las diferencias en el comportamiento mediante el método act() y Stage utiliza la composición para permitir que su estado sea modificado. Este cambio de estado produce un cambio de comportamiento.

Ejercicio 16. Siguiendo el ejemplo de Transmogrify.java, crea una clase Starship que contenga una referencia AlertStatus que pueda indicar tres estados distintos. Incluye métodos para verificar los estados.

Sustitución y extensión

Podríamos pensar que la forma más limpia de crear una jerarquía de herencia sería adoptar un enfoque "puro", sólo los métodos establecidos en la clase base serán sustituidos en la clase derivada:



Se trata de un tipo de relación "es-un" ya que la interfaz de la clase base establece lo que dicha clase es. La herencia garantiza que cualquier clase derivada tendrá la interfaz de la clase base y nada más. Las clases derivadas no tendrán nada más que lo que la interfaz de la clase base tenga.
Esto podría considerarse como una "sustitución pura" ya que podemos sustituir un objeto de la clase base o un objeto de la clase derivada y no necesitamos ninguna información adicional sobre las subclases a la hora de utilizarlas:



La clase base puede recibir cualquier mensaje que enviemos a la clase derivada, las dos tienen la misma interfaz. Debido a esto, lo que tenemos que hacer es generalizar a partir de la clase derivada, sin preocuparnos de cuál es es el tipo del objeto que estemos utilizando. Todo se maneja mediante polimorfismo.
Puede parecer que las relaciones "es-un" son la forma más lógica de implementar las cosas y que cualquier otro diseño resultará confuso. Si pensamos así estamos en un error, ya que si miramos a nuestro alrededor podemos ver que ampliar la interfaz (mediante extends) puede ser la solución perfecta para un problema concreto. Este tipo de relación se denomina "es-como-un", ya que la clase derivada es como la clase base: tiene la misma interfaz y además añade otras características:


Este enfoque tiene una desventaja, la parte ampliada de la clase derivada no está disponible en la clase base, por tanto, cuando hagamos una generalización no podremos invocar nuevos métodos:



Mientras no se hagan generalizaciones no hay problema, pero en muchas ocasiones necesitaremos conocer el tipo exacto del objeto para acceder a los métodos ampliados de dicho tipo, lo vemos en la siguiente sección.

Especialización e información de tipos en tiempo de ejecución
Como perdemos la información específica del tipo mediante generalización (upcast), la extracción de información de tipos, es decir, descender por la jerarquía de herencia, será mediante especialización (downcast). La generalización es siempre segura, ya que una clase base no puede tener una interfaz más amplia que la clase derivada. Con la especialización, sin embargo, no sabemos realmente si, por ejemplo, una forma es un círculo, o un triángulo, o un cuatrado u otra forma.
Debe de haber alguna manera de garantizar que la especialización se haga de forma correcta, de forma que no hagamos una proyección sobre el tipo inadecuado y enviemos un mensaje que el objeto no pueda aceptar. Si no garantizamos la especialización nuestro programa no será muy seguro.
En Java, todas las proyecciones de tipos se comprueban en tiempo de ejecución para garantizar que se trate del tipo que creemos que es. Si no lo es, obtendremos una excepción ClassCastException. Esta comprobación de tipos en tiempo de ejecución se denomina información de tipos en tiempo de ejecución (RTTI, runtime type information). Vamos a ver un ejemplo de comportamiento de RTTI:
//: polymorphism/RTTI.java
// Downcasting & Runtime type information (RTTI).
// {ThrowsException}

class Useful {
  public void f() {System.out.println("Useful.f()");}
  public void g() {System.out.println("Useful.g()");}
}

class MoreUseful extends Useful {
  public void f() {System.out.println("MoreUseful.f()");}
  public void g() {System.out.println("MoreUseful.g()");}
  public void u() {System.out.println("MoreUseful.u()");}
  public void v() {System.out.println("MoreUseful.v()");}
  public void w() {System.out.println("MoreUseful.w()");}
} 

public class RTTI {
  public static void main(String[] args) {
    Useful[] x = {
      new Useful(),
      new MoreUseful()
    };
    x[0].f();
    x[1].g();
    // Compile time: method not found in Useful:
    //! x[1].u();
    ((MoreUseful)x[1]).u(); // Downcast/RTTI
    ((MoreUseful)x[0]).u(); // Exception thrown
  }
} ///:~

MoreUseful amplía la interfaz de Useful, al ser una clase heredada también puede generalizarse a Useful, de hecho podemos ver esta generalización en la inicialización de la matriz x en main(). Ambos objetos de la matriz son de la clase Useful, por tanto, podemos enviar los métodos f() y g() a ambos, pero si tratamos de invocar el método u() que sólo existe en MoreUseful, obtendremos un mensaje de error en tiempo de compilación (x[1].u()).
Como en la matriz tenemos objetos Useful, para acceder a la interfaz ampliada de un objeto MoreUseful, podemos intentar realizar una especialización. Si se trata del tipo correcto, la operación tendrá éxito, si no, obtendremos una excepción ClassCastException. Para esta excepción no hay que escribir ningún código especial, se trata de un error del programador que puede producirse en cualquier parte del programa.
El mecanismo RTTI es más complejo de lo que este ejemplo muestra. Por ejemplo, hay una forma de ver cuál es el tipo con el que estamos tratando antes de ver la especializacion. En el Tema 14, Información de tipos, veremos diferentes aspectos de la información de tipos en tiempo de ejecución.

Ejercicio 17
. Utilizando la jerarquía Cycle del Ejercicio 1, añade un método balance() a Unicycle y Bicycle, pero no a Tricycle. Crea instancias de los tres tipos y generalícelas para formar una matriz de objetos Cycle. Trata de invocar balance() en cada elemento de la matriz y observa los resultados. Realice una especialización e invoca balance() y observa lo que sucede.

2 comentarios:

  1. Muy buen trabajo, muy juicioso además, sin embargo como consejo te recomendaría aclarar un poco más los conceptos y ejemplos, el libro no es el más claro al explicar. De cualquier forma te felicito es un muy buen trabajo.

    SoftMAS
    www.soft-mas.com

    ResponderEliminar
    Respuestas
    1. Hola Manuel:

      Gracias por tu consejo, lo tendré en cuenta en los próximos temas que publique.

      Muchas gracias.

      Eliminar