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.
Por lo tanto, el
uso de herencia en este ejemplo ha eliminado la necesidad de escribir dos ciclos
en el método listar. La herencia evita la duplicación de código no sólo en las
clases servidoras sino también en las clases clientes de aquellas.
Nota: Si UD.
codifica lo que estamos detallando descubrirá que el método imprimir tiene un
problema: no imprime todos los detalles. Esto es así porque estamos invocando el
método imprimir() de la clase Elemento, que no imprime atributos de CD o DVD.
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 un
mé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 es Sobrescribir
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 sentencia
elemento.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 void
DemoBaseDedatos() 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 imprime
System.out.println(db.toString()) lo hace el método del objeto
responsable de ello, como manda la POO.
No hay comentarios:
Publicar un comentario