• No se han encontrado resultados

polimorfismo

N/A
N/A
Protected

Academic year: 2021

Share "polimorfismo"

Copied!
25
0
0

Texto completo

(1)

4.1 Definicion de polimorfismo

Polimorfismo

El polimorfismo es un concepto de la programación orientada a objetos que nos permite programar en forma general, en lugar de hacerlo en forma específica. En general nos sirve para programar objetos con características comunes y que todos estos compartan la misma superclase en una jerarquía de clases, como si todas fueran objetos de la superclase. Esto nos simplifica la programación. Recuerde el ejemplo del ecosistema, en donde todos los objetos de las

distintas especies heredaban de una superclase llamada Animal, que brindaba la información general de cualquier animal, independiente de su especie. Sin embargo, cada especie hace un uso particular de cada uno de los métodos u operaciones de la clase Animal. El método comer() no se ejecutará de la misma manera en un León() o en un Pavo(). Lo mismo ocurre para métodos moverse() en objetos de tipo Tiburón() o Gallina(), aunque todas las especies realicen estos métodos. A la sobrescritura o implementación específica de métodos es la clave del polimorfismo.

Para poner en práctica se hará un ejemplo bastante sencillo. Se hará una librería de clases que represente figuras tridimensionales y bidimensionales, y su respectiva jerarquía de clases. Las clases deben ser capaces de tener funcionamiento bastante básico, como obtener áreas, volúmenes y perímetros de la figura correspondiente.

La representación de la jerarquía sería como ésta:

La superclase de dicha jerarquía podría ser muy parecida a ésta: public abstract class figura {

protected String nombre; protected int color;

(2)

protected int grosorBorde; public String getNombre(){ return this.nombre;

}

public void setNombre(String n){ this.nombre=n;

}

public int getColor(){ return this.color; }

public void setColor(int c){ this.color=c;

}

public int getGrosorBorde(){ return this.grosorBorde; }

public void setGrosorBorde(int g){ this.grosorBorde=g;

}

public abstract void dibujar(); }

Las siguientes clases en el nivel de la jerarquía podrían quedar muy parecidas a éstas:

public abstract class figura2D extends figura { public abstract int calcularArea();

public abstract int calcularPerimetro(); }

(3)

public abstract class figura3D extends figura { public abstract int calcularVolumen();

}

Se le pide que forme las clases de la parte inferior de la jerarquía y que representarían los objetos a instanciarse.

Además, debe de realizar una implementación de esta librería, en donde el usuario pueda crear nuevas figuras y que éstas se almacenen en un arreglo de figuras.

Enlace dinámico

En el lenguaje C, los identificadores de la función están asociados siempre a direcciones físicas antes de la ejecución del programa, esto se conoce como enlace temprano o estático. Ahora bien, el lenguaje C++ y Java permiten decidir a que función llamar en tiempo de ejecución, esto se conoce como enlace tardío o dinámico. Vamos a ver un ejemplo de ello.

Podemos crear un array de la clase base Figura y guardar en sus elementos los valores devueltos por new al crear objetos de las clases derivadas.

Figura[] fig=new Figura[4];

fig[0]=new Rectangulo(0,0, 5.0, 7.0); fig[1]=new Circulo(0,0, 5.0); fig[2]=new Circulo(0, 0, 7.0); fig[3]=new Rectangulo(0,0, 4.0, 6.0); La sentencia fig[i].area();

¿a qué función area llamará?. La respuesta será, según sea el índice i. Si i es cero, el primer elemento del array guarda una referencia a un objeto de la clase Rectangulo, luego llamará a la función miembro area de Rectangulo. Si ies uno, el segundo elemento del array guarda una referencia un objeto de la clase Circulo, luego llamará también a la función area de Circulo, y así sucesivamente. Pero podemos introducir el valor del índice i, a través del teclado, o seleccionando un control en un applet, en el momento en el que se ejecuta el programa. Luego, la decisión sobre qué función area se va a llamar se retrasa hasta el tiempo de ejecución.

(4)

Concepto

Hay ocasiones, cuando se desarrolla una jerarquía de clases en que algún comportamiento está presente en todas ellas pero se materializa de forma distinta para cada una. Por ejemplo, pensemos en una estructura de clases para manipular figuras geométricas. Podríamos pensar en tener una clase genérica, que podría llamarse FiguraGeometrica y una serie de clases que extienden a la anterior que podrían ser Circulo, Poligono, etc. Podría haber un método dibujar dado que sobre todas las figuras puede llevarse a cabo esta acción, pero las operaciones concretas para llevarla a cabo dependen del tipo de figura en concreto (de su clase). Por otra parte la acción dibujar no tiene sentido para la clase genérica Figura Geometrica, porque esta clase representa una abstracción del conjunto de figuras posibles.

Para resolver esta problemática Java proporciona las clases y métodos abstractos. Un método abstracto es un método declarado en una clase para el cual esa clase no proporciona la implementación (el código). Una clase abstracta es una clase que tiene al menos un método abstracto. Una clase que extiende a una clase abstracta debe implementar los métodos abstractos (escribir el código) o bien volverlos a declarar como abstractos, con lo que ella misma se convierte también en clase abstracta

Las clases abstractas en java

Una de las características más útiles de cualquier lenguaje orientado a objetos es la

posibilidad de declarar clases que definen como se utiliza solamente, sin tener que implementar método. Esto en Java se hace mediante interfaces y con clases abstractas.

Una clase abstracta es una clase de la que no se puede crear objetos. La utilidad de estas clases estriba en que otras clases hereden de ésta, por lo que con ello conseguiremos reutilizar código. Para declarar una clase como abstracta utilizamos la palabra clave abstract.

Los métodos para los que no aporte una implementación serán declarados a su vz abstractos. Si una clase tiene un método abstract es obligatorio que la clase sea abstract. Todas las

subclases que hereden de una clase abstracta tendrán que redefinir los métodos abstractos dándoles una implementación. En el caso de que no implementen alguno de esos métodos la clase hija también será abstracta y tendrá que declararse como tal (tanto la clase como los métodos que siguen siendo abstractos).

En método abstract no pude ser static, ya que estos no pueden ser redifinidos por las subclases.

(5)

INTERFACES Introducción

Las interfaces Java son expresiones puras de diseño. Se trata de auténticas conceptualizaciones no implementadas que sirven de guía para definir un determinado concepto (clase) y lo que debe hacer, pero sin desarrollar un mecanismo de solución.

Se trata de declarar métodos abstractos y constantes que posteriormente puedan ser implementados de diferentes maneras según las necesidades de un programa.

Por ejemplo una misma interfaz podría ser implementada en una versión de prueba de manera poco óptima, y ser acelerada convenientemente en la versión definitiva tras conocer más a fondo el problema.

Declaración

Para declarar una interfaz se utiliza la sentencia interface, de la misma manera que se usa la sentencia class:

interface MiInterfaz { int CONSTANTE = 100;

int metodoAbstracto( int parametro ); }

Se observa en la declaración que las variables adoptan la declaración en mayúsculas, pues en realidad actuarán como constantes final. En ningún caso estas variables actuarán como variables de instancia.

Por su parte, los métodos tras su declaración presentan un punto y coma, en lugar de su cuerpo entre llaves. Son métodos abstractos, por tanto, métodos sin implementación

Implementación de una interfaz

Como ya se ha visto, las interfaces carecen de funcionalidad por no estar implementados sus métodos, por lo que se necesita algún mecanismo para dar cuerpo a sus métodos.

La palabra reservada implements utilizada en la declaración de una clase indica que la clase implementa la interfaz, es decir, que asume las constantes de la interfaz, y codifica sus métodos:

(6)

class ImplementaInterfaz implements MiInterfaz{ int multiplicando=CONSTANTE;

int metodoAbstracto( int parametro ){ return ( parametro * multiplicando ); }

}

En este ejemplo se observa que han de codificarse todos los métodos que determina la interfaz (metodoAbstracto()), y la validez de las constantes (CONSTANTE) que define la interfaz durante toda la declaración de la clase. Una interfaz no puede implementar otra interfaz, aunque sí extenderla (extends) ampliándola.

Herencia múltiple

Java es un lenguaje que incorpora herencia simple de implementación pero que puede aportar herencia múltiple de interfaz. Esto posibilita la herencia múltiple en el diseño de los programas Java.

Una interfaz puede heredar de más de una interfaz antecesora. interface InterfazMultiple extends Interfaz1,Interfaz2{ }

Una clase no puede tener más que una clase antecesora, pero puede implementar más de una interfaz:

class MiClase extends SuPadre implements Interfaz1,Interfaz2{ }

El ejemplo típico de herencia múltiple es el que se presenta con la herencia en diamante:

Imagen 6: Ejemplo de herencia múltiple

Para poder llevar a cabo un esquema como el anterior en Java es necesario que las clases A, B y C de la figura sean interfaces, y que la clase D sea una clase (que recibe la herencia múltiple):

interface A{ }

interface B extends A{ } interface C extends A{ } class D implements B,C{ }

Colisiones en la herencia múltiple

En una herencia múltiple, los identificadores de algunos métodos o atributos pueden coincidir en la clase que hereda, si dos de las interfaces padres tienen algún método o atributo que coincida en nombre. A esto se le llama colisión.

(7)

Esto se dará cuando las clases padre (en el ejemplo anterior B y C) tienen un atributo o método que se llame igual. Java resuelve el problema estableciendo una serie de reglas.

Para la colisión de nombres de atributos, se obliga a especificar a qué interfaz base pertenecen al utilizarlos.

Para la colisión de nombres en métodos:

 Si tienen el mismo nombre y diferentes parámetros: se produce sobrecarga de métodos permitiendo que existan varias maneras de llamar al mismo.

 Si sólo cambia el valor devuelto: se da un error de compilación, indicando que no se pueden implementar los dos.

 Si coinciden en su declaración: se elimina uno de los dos, con lo que sólo queda uno.

Envolturas de los tipos simples

Los tipos de datos de Java no forman parte de la jerarquía de objetos. Sin embargo a veces es necesario crear una representación como objeto de alguno de los tipos de datos simples de Java.

La API de Java contiene un conjunto de interfaces especiales para modificar el comportamiento de los tipos de datos simple. A estas interfaces se las conoce comoenvolturas de tipo simple.

Todas ellas son hijas de la clase abstracta Number y son:  Double: Da soporte al tipo double.

 Float: Da soporte al tipo float.

 Integer: Da soporte a los tipos int, short y byte.  Long: Da soporte al tipo long.

 Character: Envoltura del tipo char.  Boolean: Envoltorio al tipo boolean.

Los métodos abstractos son útiles cuando se quiere que cada implementación de la clase parezca y funcione igual, pero necesita que se cree una nueva clase para utilizar los métodos abstractos. Los interfaces proporcionan un mecanismo para abstraer los métodos a un nivel superior, lo que permite simular la herencia múltiple de otros lenguajes.

Un interfaz sublima el concepto de clase abstracta hasta su grado más alto. Un interfaz podrá verse simplemente como una forma, es como un molde, solamente permite declarar nombres de métodos, listas de argumentos, tipos de retorno y adicionalmente miembros datos (los cuales podrán ser únicamente

(8)

tipos básicos y serán tomados como constantes en tiempo de compilación, es decir, static y final).

Un interfaz contiene una colección de métodos que se implementan en otro lugar. Los métodos de una clase son public, static y final.

La principal diferencia entre interface y abstract es que un interfaz proporciona un mecanismo de encapsulación de los protocolos de los métodos sin forzar al usuario a utilizar la herencia. Por ejemplo:

public interface VideoClip {

// comienza la reproduccion del video void play();

// reproduce el clip en un bucle void bucle();

// detiene la reproduccion void stop();

}

Las clases que quieran utilizar el interfaz VideoClip utilizarán la palabra implements y proporcionarán el código necesario para implementar los métodos que se han definido para el interfaz:

class MiClase implements VideoClip { void play() { <código> } void bucle() { <código> } void stop() { <código> }

Al utilizar implements para el interface es como si se hiciese una acción decopiar-y-pegar del código del interface, con lo cual no se hereda nada, solamente se pueden usar los métodos.

La ventaja principal del uso de interfaces es que una clase interface puede ser implementada por cualquier número de clases, permitiendo a cada clase compartir el interfaz de programación sin tener que ser consciente de la implementación que hagan las otras clases que implementen el interface.

class MiOtraClase implements VideoClip { void play() {

(9)

} void bucle() { <código nuevo> } void stop() { <código nuevo> }

Es decir, el aspecto más importante del uso de interfaces es que múltiples objetos de clases diferentes pueden ser tratados como si fuesen de un mismo tipo común, donde este tipo viene indicado por el nombre del interfaz.

Aunque se puede considerar el nombre del interfaz como un tipo de prototipo de referencia a objetos, no se pueden instanciar objetos en sí del tipo interfaz. La definición de un interfaz no tiene constructor, por lo que no es posible invocar el operador new sobre un tipo interfaz.

Un interfaz puede heredar de varios interfaces sin ningún problema. Sin embargo, una clase solamente puede heredar de una clase base, pero puede implementar varios interfaces. También, el JDK ofrece la posibilidad de definir un interfaz vacío, como es el caso de Serialize, que permite serializar un objeto. Un interfaz vacío se puede utilizar como un flag, un marcador para marcar a una clase con una propiedad determinada.

La aplicación java514.java, ilustra algunos de los conceptos referentes a los interfaces. Se definen dos interfaces, en uno de ellos se definen dos constantes y en el otro se declara un método put() y un método get(). Las constantes y los métodos se podrían haber colocado en la misma definición del interfaz, pero se han separado para mostrar que una clase simple puede implementar dos o más interfaces utilizando el separador coma (,) en la lista de interfaces.

También se definen dos clases, implementando cada una de ellas los dos interfaces. Esto significa que cada clase define el método put() y el métodoget(), declarados en un interfaz y hace uso de las constantes definidas en el otro interfaz. Estas clase se encuentran en ficheros separados por exigencias del compilador, los ficheros son Constantes.java y MiInterfaz.java, y el contenido de ambos ficheros es el que se muestra a continuación:

public interface Constantes { public final double pi = 6.14;

public final int constanteInt = 125; }

(10)

void put( int dato ); int get();

}

Es importante observar que en la definición de los dos métodos del interfaz, cada clase los define de la forma más adecuada para esa clase, sin tener en cuenta cómo estará definidos en las otras clases.

Una de las clases también define el método show(), que no está declarado en el interfaz. Este método se utiliza para demostrar que un método que no está declarado en el interfaz no puede ser accedido utilizando una variable referencia de tipo interfaz.

El método main() en la clase principal ejecuta una serie de instanciaciones, invocaciones de métodos y asignaciones destinadas a mostrar las características de los interfaces descritos anteriormente. Si se ejecuta la aplicación, las sentencias que se van imprimiendo en pantalla son autoexplicactivas de lo que está sucediendo en el corazón de la aplicación. Los interfaces son útiles para recoger las similitudes entre clase no relacionadas, forzando una relación entre ellas. También para declarar métodos que forzosamente una o más clases han de implementar. Y también, para tener acceso a un objeto, para permitir el uso de un objeto sin revelar su clase, son los llamados objetos anónimos, que son muy útiles cuando se vende un paquete de clases a otros desarrolladores.

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();

(11)

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

(12)

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

(13)

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.

(14)

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);

(15)

} }

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».

(16)

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

(17)

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

(18)

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; }

}

(19)

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;

(20)

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);

(21)

// 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.

4.5 Reutilización del código

Lo primero que se les viene a la cabeza a los estudiantes (y a muchos profesionales) cuando se les menciona la reutilización del código es el famoso copiar y pegar al que se han acostumbrado en la programación estructurada, y de hecho muchos lo hacen en poo, lo cual es una de las practicas que más encarece el desarrollo de software. Como todo en Java, el problema se resuelve con las clases. Para reutilizar el código creamos nuevas clases pero, en lugar de partir de cero, partimos de clases, relacionadas con nuestra clase, que

(22)

han sido ya creadas y depuradas. El truco está en usar las clases sin ensuciar el

código existente.

Una forma de hacer esto es crear objetos de nuestras clases existentes dentro de la nueva clase. Esto se conoce como composición porque la nueva clase está compuesta de objetos de clases existentes. Estamos reutilizando la

funcionalidad del código, y no la forma.

Otra forma es crear una nueva clase como un tipo de una clase ya existente. Tomamos la forma de la clase existente y añadimos código a la nueva, sin modificar la clase existente. Esta forma de crear nuevos objetos se llamada herencia, y lo que hacemos es extender la clase en la que nos basamos para crear la nueva.

Composición:

Hasta ahora hemos usado la composición de cierta manera, ej. Cuando hacemos una interfaz gráfica de usuario, nuestra clase de interfaz gráfica esta compuesta por un frame, unos panel, botones, etc. todos estos objetos componen el objeto de interfaz gráfica. Es decir que la composición consiste en poner manejadores de objetos dentro de nuestra clase, estos manejadores de objetos no serán otra cosa que instancias de las clases en las que nos estamos basando para crear la nueva clase.

Recordemos que la forma para determinar cuándo usar composición es cuando podemos decir que nuestra nueva clase “tiene un” elemento de otro tipo de objetos, por ejemplo un cronómetro tiene: horas, minutos y segundos, es decir que una clase Cronometro está compuesta por otras clases llamadas: Horas, Minutos y Segundos.

Veamos como seria esta clase: public class Cronometro { Horas h; Minutos m; Segundos s; String cadena; int seg,min,hor; public Cronometro() { seg=0;

(23)

min=0; hor=0;

h = new Horas(); m = new Minutos(); s = new Segundos();

cadena = new String("0 : 0 : 0"); }

public String avanzar(){ seg = s.forward(); if(seg==0){ min=m.forward(); if(min==0){ hor=h.forward(); } }

cadena = hor + " : " + min + " : " + seg; return cadena;

}

public String reset(){ seg = s.reset();

min = m.reset(); hor = h.reset();

cadena = hor + " : " + min + " : " + seg; return cadena;

} }

Nuestra clase Cronometro está compuesta entre otras cosas por objetos del tipo Horas, Minutos y Segundos y a través del constructor de nuestra clase creamos las instancias de cada una de ellas.

Herencia:

(24)

presente, ya que todas las clases que creemos heredan de la clase Object, por eso es válido decir que en java todo es un objeto. La sintaxis para la composición es obvia pero, para realizar la herencia, hay una forma claramente distinta. Cuando heredamos, estamos diciendo "Esta nueva clase es como esa clase antigua", por ejemplo es decir que la clase Horas “es una” Unidad De Tiempo. Afirmamos esto en el código dando el nombre de la clase como siempre pero, antes de la apertura del límite cuerpo de clase, pondremos la palabra clave "extends" seguida por el nombre de la clase base. Cuando hagamos esto, obtendremos automáticamente todos los datos miembros y métodos de la clase base.

Primero veamos como seria la clase UnidadDeTiempo: public class UnidadDeTiempo {

int valor; int tope;

public int forward(){ if(valor == tope) valor=0; else valor++; return valor; }

public int reset(){ valor=0;

return valor; }

}

Y nuestra clase Horas:

public class Horas extends Unidad De Tiempo{ public Horas() {

this.valor=0; this.tope=23;

(25)

} }

De esta manera sin necesidad de tener que escribir nuevamente todos el código de Unidad De Tiempo lo tememos disponible en la clase Horas, pero que partes tenemos disponibles?, todos los atributos y los métodos de la clase padre están disponibles en la clase hija pero dependiendo de los modificadores de acceso o visibilidad de estos, por ejemplo los atributos y métodos de tipo friendly solo estarán disponibles para las clases hijas que heredan de una clase padre en el mismo paquete, los atributos y métodos de tipo public estarán disponibles para todas las clases que hereden de la clase padre sin importar que se halle o no en el mismo paquete; los miembros protected también son accesibles desde las clases hijas.

El código de nuestra clases hijas no tienen porque limitarse solo al código heredado, de hecho casi siempre la herencia se hace para extender la funcionalidad de las clases heredadas añadiendo nuevos métodos y atributos.

La composición y la herencia:

Tanto la composición como la herencia permiten poner sub-objetos dentro de tu nueva clase. Podríamos preguntarnos cuál es la diferencia entre los dos, y cuándo elegir uno en lugar del otro. La composición es generalmente usada cuando deseamos las características de una clase existente dentro de una nueva clase, pero no su interfaz. Es decir, ponemos un para poder usarlo para implementar características de nuestra nueva clase, pero el usuario de esa nueva clase verá el interfaz que hemos definido en lugar del interfaz del

objeto insertado.

Los objetos miembros usan la implementación ocultándose a sí mismos, por lo que esto es una cosa segura a hacer y, cuando el usuario sabe que estamos uniendo un conjunto de partes, hace que el interfaz sea más fácil de entender. Cuando heredamos, estamos cogiendo una clase existente y creando una versión especial de esa clase. En general, esto significa que estamos tomando una clase de propósito general, especializándola para un caso o necesidad particular. Pensando un poco, podrá entender que no tendría sentido construir un coche usando un objeto vehículo (un coche no contiene un vehículo, ¡es un vehículo!). La relación es- un viene expresada por la herencia, y la relación tiene un viene expresada por la composición.

Referencias

Documento similar

La Normativa de evaluación del rendimiento académico de los estudiantes y de revisión de calificaciones de la Universidad de Santiago de Compostela, aprobada por el Pleno or-

PLAN DE NEGOCIOS DE UN RESTAURANTE QUE POSTERIORMENTE SIRVA COMO BASE PARA LA CREACIÓN DE UNA FRANQUICIA COLOMBIANA, COMERCIALIZADORA DE ALITAS DE POLLO A DOMICILIO Y EN PUNTO

laborales más afectadas por las olas de calor son aquellas más precarizadas, peor remuneradas y con menor consideración social, aunque se trate de trabajos esenciales para la

Establecer un arancel de 98% para la importación de leche y nata (crema) clasificada por la partida arancelaria 04.02, por tal motivo estos productos no estarán sujetos al mecanismo

Gestionar un producto cultural es un arte que requiere habilidades especiales para combinar la dimensión creativa del producto cultural (en cualquiera de sus versiones,

Se presenta un panorama epidemiológico de la lactancia en México, los principales constituyentes de la leche, los beneficios de ama- mantar, tanto para el bebé como para la madre,

La oferta existente en el Departamento de Santa Ana es variada, en esta zona pueden encontrarse diferentes hoteles, que pueden cubrir las necesidades básicas de un viajero que

Tras haber conseguido trasladar la importancia del drama de la despoblación a toda la sociedad, este año 4GATOS pretende escapar del victimismo y la lamentación y abordar la