lunes, 4 de junio de 2012

4.4 Definición, uso y aplicaciones de las variables polimórficas


Variables polimórficas
En Java, las variables que contienen objetos son variables polimórficas. El término «polimórfico» (literalmente: muchas formas) se refiere al hecho de que una misma variable puede contener objetos de diferentes tipos (del tipo declarado o de cualquier subtipo del tipo declarado). El polimorfismo aparece en los lenguajes orientados a objetos en numerosos contextos, las variables polimórficas constituyen justamente un primer ejemplo.
Observemos la manera en que el uso de una variable polimórfica nos ayuda a simplificar nuestro método listar. El cuerpo de este método es

for (Elemento elemento : elementos)
elemento.imprimir();

En este método recorremos la lista de elementos (contenida en un ArrayList mediante la variable elementos), tomamos cada elemento de la lista y luego invocamos su método imprimir. Observe que los elementos que tomamos de la lista son de tipo CD o DVD pero no son de tipo Elemento. Sin embargo, podemos asignarlos a la variable elemento (declarada de tipo Elemento) porque son variables polimórficas. La variable elemento es capaz de contener tanto objetos CD como objetos DVD porque estos son subtipos de Elemento.

Enmascaramiento de tipos (Casting)
Algunas veces, la regla de que no puede asignarse un supertipo a un subtipo es más restrictiva de lo necesario. Si sabemos que la variable de un cierto supertipo contiene un objeto de un subtipo, podría realmente permitirse la asignación. Por ejemplo:

Vehiculo v;
Coche a = new Coche();
v = a; // Sin problemas
a = v; // Error, según el compilador
Obtendremos un error de compilación en a = v.

El compilador no acepta esta asignación porque como a (Coche) tiene mas atributos que v (Vehículo) partes del objeto a quedan sin asignación. El compilador no sabe que v ha sido anteriormente asignado por un coche.
Podemos resolver este problema diciendo explícitamente al sistema, que la variable v contiene un objeto Coche, y lo hacemos utilizando el operador de enmascaramiento de tipos, en una operación también conocida como casting.
a = (Coche)v; // correcto
En tiempo de ejecución, el Java verificará si realmente v es un Coche. Si fuimos cuidadosos, todo estará bien; si el objeto almacenado en v es de otro tipo, el sistema indicará un error en tiempo de ejecución (denominado ClassCastException) y el programa se detendrá.
El compilador no detecta (Naturalmente) errores de enmascaramiento en tiempo de compilación. Se detectan en ejecución y esto no es bueno.
El enmascaramiento debiera evitarse siempre que sea posible, porque puede llevar a errores en tiempo de ejecución y esto es algo que claramente no queremos. El compilador no puede ayudamos a asegurar la corrección de este caso.
En la práctica, raramente se necesita del enmascaramiento en un programa orientado a objetos bien estructurado. En la mayoría de los casos, cuando se use un enmascaramiento en el código, debiera reestructurarse el código para evitar el enmascaramiento, y se terminará con un programa mejor diseñado. Generalmente, se resuelve el problema de la presencia de un enmascaramiento reemplazándolo por unmétodo polimórfico (Un poquito de paciencia).

La clase Object
Todas las clases tienen una superclase. Hasta ahora, nos puede haber parecido que la mayoría de las clases con que hemos trabajado no tienen una superclase, excepto clases como DVD y CD que extienden otra clase. En realidad, mientras que podemos declarar una superclase explícita para una clase dada, todas las clases que no tienen una declaración explícita de superclase derivan implícitamente de una clase de nombre Object.

Object es una clase de la biblioteca estándar de Java que sirve como superclase para todos los objetos. Es la única clase de Java sin superclase. Escribir una declaración de clase como la siguiente

public class Person{} es equivalente a public class Person extends Object{}
Tener una superclase sirve a dos propósitos.
. Podemos declarar variables polimórficas de
tipo Object que pueden contener cualquier objeto
(esto no es importante)
. Podemos usar Polimorfismo (Ya lo vemos) y esto si es importante.

Autoboxing y clases «envoltorio»

Hemos visto que, con una parametrización adecuada, las colecciones pueden almacenar objetos de cualquier tipo; queda un problema, Java tiene algunos tipos que no son objetos.
Como sabemos, los tipos primitivos tales como int, boolean y char están separados de los tipos objeto. Sus valores no son instancias de clases y no derivan de la clase Object. Debido a esto, no son suptipos de Object y normalmente, no es posible ubicarlos dentro de una colección.
Este es un inconveniente pues existen situaciones en las que quisiéramos crear, por ejemplo, una lista de enteros (int) o un conjunto de caracteres (char). ¿Qué hacer?
La solución de Java para este problema son las clases envoltorio. En Java, cada tipo simple o primitivo tiene su correspondiente clase envoltorio que representa el mismo tipo pero que, en realidad, es un tipo objeto. Por ejemplo, la clase envoltorio para el tipo simple int es la clase de nombre Integer.
La siguiente sentencia envuelve explícitamente el valor de la variable ix de tipoprimitivo int, en un objeto Integer:
Integer ienvuelto = new Integer(ix);

y ahora ienvuelto puede almacenarse fácilmente por ejemplo, en una colección de tipo
ArrayList<Integer>. Sin embargo, el almacenamiento de valores primitivos en un objeto colección se lleva a cabo aún más fácilmente mediante una característica del compilador conocida como autoboxing.
En cualquier lugar en el que se use un valor de un tipo primitivo en un contexto que requiere un tipo objeto, el compilador automáticamente envuelve al valor de tipo primitivo en un objeto con el envoltorio adecuado. Esto quiere decir que los valores de tipos primitivos se pueden agregar directamente a una colección:

private ArrayList<Integer> listaDeMarcas;
public void almacenarMarcaEnLista (int marca){
listaDeMarcas.agregar(marca);
}
La operación inversa, unboxing, también se lleva a cabo automáticamente, de modo que el acceso a un elemento de una colección podría ser:

int primerMarca = listaDeMarcas.remove(0);

El proceso de autoboxing se aplica en cualquier lugar en el que se pase como parámetro un tipo primitivo a un método que espera un tipo envoltorio, y cuando un valor primitivo se almacena en una variable de su correspondiente tipo envoltorio.
De manera similar, el proceso de unboxing se aplica cuando un valor de tipo envoltorio se pasa como parámetro a un método que espera un valor de tipo primitivo, y cuando se almacena en una variable de tipo primitivo.

Tipo estático y tipo dinámico

Volvemos sobre un problema inconcluso: el método imprimir de DoME, no muestra todos los datos de los elementos.
El intento de resolver el problema de desarrollar un método imprimir completo y polimórfico nos conduce a la discusión sobre tipos estáticos y tipos dinámicos y sobre despacho de métodos. Comencemos desde el principio.
Necesitamos ver más de cerca los tipos. Consideremos la siguiente sentencia:
Elemento e1 = new CD();
¿Cuál es el tipo de e1?
Depende de qué queremos decir con «tipo de e1».
El tipo de la variable e1 es Elemento; (tipo estático)
El tipo del objeto almacenado en ees CD. (tipo dinámico)
Entonces el tipo estático de e1 es Elemento y su tipo dinámico es CD.
En el momento de la llamada e1.imprimir(); el tipo estático de la variable elemento
es Elemento mientras que su tipo dinámico puede ser tanto CD como DVD. No sabemos cuál es su tipo ya que asumimos que hemos ingresado tanto objetos CD como objetos DVD en nuestra base de datos.
Y en que clase debe estar codificado el método imprimir()?
En tiempo de compilación necesitamos de la existencia de imprimir() en la clase Elemento, el compilador trabaja con tipo estático.
En tiempo de ejecución necesitamos de la existencia de un método imprimir() adecuado a los datos del objeto CD o DVD.
En definitiva, necesitamos de imprimir() en las tres clases. Aunque no será lo mismo lo que se imprima en cada uno de ellos. Lo que debemos hacer entonces esSobrescribir el método
Veamos el método imprimir en cada una de las clases.
public class Elemento{
public void imprimir(){
System.out.print(titulo + " (" + duracion + " minutos) " );
if (loTengo){System.out.println("*");
}
else {System.out.println();}
System.out.println(" " + comentario);
}
}
public class CD extends Elemento{
public void imprimir(){
System.out.println(" " + interprete);
System.out.println(" temas: " + numeroDeTemas);
}
}
public class DVD extends Elemento{
public void imprimir(){
System.out.println(" director: " + director);
}
}
Este diseño funciona mejor: compila y puede ser ejecutado, aunque todavía no está perfecto. Proporcionamos una implementación de este diseño mediante el proyecto dome-v3.
La técnica que usamos acá se denomina sobrescritura (algunas veces también se hace referencia a esta técnica como redefinición). La sobrescritura es una situación en la que un método está definido en una superclase (en este ejemplo, el método imprimir de la clase  Elemento) y un método, con exactamente la misma signatura, está definido en la subclase.
En esta situación, los objetos de la subclase tienen dos métodos con el mismo nombre y la misma signatura: uno heredado de la superclase y el otro propio de la subclase.
¿Cuál de estos dos se ejecutará cuando se invoque este método?
Búsqueda dinámica del método (Despacho dinámico)
Si ejecutamos el método listar de la BaseDeDatos, podremos ver que se ejecutarán los métodos imprimir de CD y de DVD pero no el de Elemento, y entonces la mayor parte de la información, la común contenida en Elemento, no se imprime.
Que está pasando? Vimos que el compilador insistió en que el método imprimir esté en la clase Elemento, no le alcanzaba con que los métodos estuvieran en las subclases. Este experimento ahora nos muestra que el método de la clase Elemento no se ejecuta para nada, pero sí se ejecutan los métodos de las subclases.
Ocurre que el control de tipos que realiza el compilador es sobre el tipo estático, pero en tiempo de ejecución los métodos que se ejecutan son los que corresponden al tipo dinámico.
Saber esto es muy importante pero todavía insuficiente.
Para comprenderla mejor, veamos con más detalle cómo se invocan los métodos. Este procedimiento se conoce como búsqueda de método, ligadura de método o despacho de método. En este libro, nosotros usamos la terminología «búsqueda de método».
Comenzamos con un caso bien sencillo de búsqueda de método. Suponga que tenemos un objeto de clase DVD almacenado en una variable v1 declarada de tipo DVD (Figura 9.5). La clase DVD tiene un método imprimir y no tiene declarada ninguna superclase.
Esta es una situación muy simple que no involucra herencia ni polimorfismo.
Ejecutamos v1.imprimir{). Esto requiere de las siguientes acciones:
l. Se accede a la variable v1.
2. Se encuentra el objeto almacenado en esa variable (siguiendo la referencia).
3. Se encuentra la clase del objeto (siguiendo la referencia «es instancia de»).
4. Se encuentra la implementación del método imprimir en la clase y se ejecuta.

Hasta aquí, todo es muy simple.
A continuación, vemos la búsqueda de un método cuando hay herencia. El escenario es similar al anterior, pero esta vez la clase DVD tiene una superclase, Elemento, y el método imprimir está definido sólo en la superclase Ejecutamos la misma sentencia. La invocación al método comienza de manera similar: se ejecutan nuevamente los pasos 1 al 3 del escenario anterior pero luego continúa de manera diferente:

4. No se encuentra ningún método imprimir en la clase DVD.
5. Se busca en la superclase un método que coincida. Y esto se hace hasta encontrarlo, subiendo en la jerarquía hasta Object si fuera necesario. Tenga en cuenta que, en tiempo de ejecución, debe encontrarse definitivamente un método que coincida, de lo contrario la clase no habría compilado.
6. En nuestro ejemplo, el método imprimir es encontrado en la clase

Este ejemplo ilustra la manera en que los objetos heredan los métodos. Cualquier método que se encuentre en la superclase puede ser invocado sobre un objeto de la subclase y será correctamente encontrado y ejecutado.
Ahora llegamos al escenario más interesante: la búsqueda de métodos con una variable polimórfica y un método sobrescrito. Los cambios:
. El tipo declarado de la variable v1 ahora es Elemento, no DVD.
. El método imprimir está definido en la clase Elemento y redefinido (o sobrescrito) en la clase DVD.
Este escenario es el más importante para comprender el comportamiento de nuestra aplicación DoME y para encontrar una solución a nuestro problema con el método imprimir.
Los pasos que se siguen para la ejecución del método son exactamente los mismos pasos 1 al 4, primer caso
Observaciones:
. No se usa ninguna regla especial para la búsqueda del método en los casos en los que el  tipo dinámico no sea igual al tipo estático.
. El método que se encuentra primero y que se ejecuta está determinado por el tipo dinámico, no por el tipo estático. La instancia con la que estamos trabajando es de la clase DVD, y esto es todo lo que cuenta.
Los métodos sobrescritos en las subclases tienen precedencia sobre los métodos de las superclases. La búsqueda de método comienza en la clase dinámica de la instancia, esta redefinición del método es la que se encuentra primero y la que se ejecuta.
Esto explica el comportamiento que observamos en nuestro proyecto DoME. Los métodos imprimir de las subclases (CD y DVD) sólo se ejecutan cuando se imprimen los elementos, produciendo listados incompletos. Como podemos solucionarlo?
Llamada a super en métodos
Ahora que conocemos detalladamente cómo se ejecutan los métodos sobrescritos podemos comprender la solución al problema de la impresión. Es fácil ver que lo que queremos lograr es que, para cada llamada al método imprimir de, digamos un objeto CD, se ejecuten para el mismo objeto tanto el método imprimir de la clase Elemento como el de la clase CD. De esta manera se imprimirán todos los detalles.
Cuando invoquemos al método imprimir sobre un objeto CD, inicialmente se invocará al método imprimir de la clase CD. En su primera sentencia, este método se convertirá en una invocación al método imprimir de la superclase que imprime la información general del elemento. Cuando el control regrese del método de la superclase, las restantes sentencias del método de la subclase imprimirán los campos distintivos de la clase CD.

public void imprimir(){ // Método imprimir de la clase CD
super.imprimir();
System.out.println(" " + interprete);
System.out.println(" temas: ") + numeroDeTemas);
}

Detalles sobre diferencias del super usado en constructores:
El nombre del método de la superclase está explícitamente establecido. Una llamada a super en un método siempre tiene la forma super.nombre-del-método(parámetros);
La llamada a super en los métodos puede ocurrir en cualquier lugar dentro de dicho método. No tiene por qué ser su primer sentencia.
La llamada a super no se genera, es completamente opcional.

Método polimórfico
Lo que hemos discutido en las secciones anteriores, desde Tipo estático y tipo dinámico hasta ahora, es lo que se conoce como despacho de método polimórfico (o mas simplemente, Polimorfismo).
Recuerde que una variable polimórfica es aquella que puede almacenar objetos de diversos tipos (cada variable objeto en lava es potencialmente polimórfica). De manera similar, las llamadas a métodos en lava son polimórficas dado que ellas pueden invocar diferentes métodos en diferentes momentos. Por ejemplo, la sentenciaelemento.imprimir(); puede invocar al método imprimir de CD en un momento dado y al método imprimir de DVD en otro momento, dependiendo del tipo dinámico de la variable elemento.
Bueno, no hay mucho más por ver en herencia y polimorfismo. Claro que para consolidar  esto necesitamos verlo funcionando.
Para hacer mas completo el demo de polimorfismo, vamos a incorporar un elemento más:
Libro, que extiende directamente Elemento, sin incorporarle ningún atributo adicional.

import java.util.ArrayList;
public class BaseDeDatos{
private ArrayList<Elemento> elementos;
protected String auxStr;
public BaseDeDatos(){ // constructor
elementos = new ArrayList<Elemento>();
}
public void agregarElemento (Elemento elElemento){
elementos.add(elElemento);
}
public String toString(){ // Cadena con todos los elementos contenidos
auxStr = "Contenidos BaseDeDatos\n";
auxStr+=elementos.toString();
return auxStr;
}
}
package dome;
public class Elemento{
private String titulo;
private int duracion;
private boolean loTengo;
private String comentario;
public Elemento(String elTitulo, int tiempo){
titulo = elTitulo;
duracion = tiempo;
loTengo = false;
comentario = "";
}
public String toString(){
String aux = titulo + " (" + duracion + " minutos) ";
if (loTengo)aux += "*";
aux += " " + comentario+"\n";
return aux;
}
}
package dome;
public class CD extends Elemento{
private String interprete;
private int numeroDeTemas;
public CD(String elTitulo, String elInterprete, int temas, int tiempo){
super(elTitulo, tiempo);
interprete = elInterprete;
numeroDeTemas = temas;
}
public String toString(){
String aux = super.toString();
aux+= " interprete (CD): " + interprete+"\n";
aux+= " temas: " + numeroDeTemas+"\n";
return aux;
}
}
package dome;
public class DVD extends Elemento{
private String director;
public DVD(String elTitulo, String elDirector, int time){
super(elTitulo, time);
director = elDirector;
}
public String toString(){
String aux = super.toString();
aux+= " director (DVD): " + director+"\n";
return aux;
}
}
package dome;
public class Libro extends Elemento{
public Libro(String elTitulo, int time){
super(elTitulo, time);
}
}
package dome;
// @author
public class Main {
private BaseDeDatos db;
public void DemoBaseDedatos(){
System.out.println("Demo inicia");
db = new BaseDeDatos();
Elemento elem;
// Incluyo 2 CDs
elem = new CD("Pajaros en la Cabeza","Amaral",14,35);
db.agregarElemento(elem);
elem = new CD("One chance","Paul Pots",10,30);
db.agregarElemento(elem);
// Incluyo 2 DVDs
elem = new DVD("Soy Leyenda","Francis Lawrence",120);
db.agregarElemento(elem);
elem = new DVD("Nada es Para Siempre","Robert Redford",105);
db.agregarElemento(elem);
// Incluyo dos libros
elem = new Libro("El Señor de los Anillos",5000);
db.agregarElemento(elem);
elem = new Libro("El Don Apacible",10000);
db.agregarElemento(elem);
// veamos que hemos hecho
System.out.println(db.toString());
System.out.println("Demo terminado");
}
public static void main(String[] args) {
Main demo = new Main();
demo.DemoBaseDedatos();
}
}
La sentencia System.out.println(db.toString()), método public voidDemoBaseDedatos() es la que se ejecuta inicialmente. Esta sentencia:
- Incorpora en la cadena el resultado de elementos.toString. Como elementos es una instancia de ArrayList, usa el toString() de esta clase (De ahí los corchetes de cierre y las comas separadoras).
elementos contiene 6 instancias de la variable polimórfica Elemento:
- las dos primeras tienen tipo dinámico CD. Entonces, en la ejecución del toString() propio invocan super.toString() (el de Elemento) y luego completan con los datos específicos de CD.
- Las dos siguientes tienen tipo dinámico DVD. Proceden exactamente lo mismo que CD.
- Las dos últimas instancias tienen tipo dinámico Libro. Como no tienen toString() propio, el despacho dinámico encuentra el de Elemnto y este es el que se ejecuta.
Complicado o facil? En todo caso, la programación es muy sintética, nada de sobreescritura, cada parte del armado de la cadena que imprimeSystem.out.println(db.toString()) lo hace el método del objeto responsable de ello, como manda la POO.

No hay comentarios:

Publicar un comentario