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 e1 es
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