Está en la página 1de 10

TEMA # 3: Polimorfismo

CONFERENCIA: Polimorfismo

 Contenido
 Polimorfismo.
 Ligadura temprana y ligadura tardía
 Métodos final
 Arreglos polimórficos
 Clases Abstractas y clase concretas

 Bibliografía
 Bud Timothy. “Introducción a la Programación orientada a Objeto”.
 Bruce Eckel. “Thinking in Java.” Third Edition.
 Stephen Stelting, Olav Maassen “Applied Java Patterns”
 Craig Larman “UML y Patrones”

 Objetivos
 Que los estudiantes conozcan las formas de establecer la ligadura.
 Que los estudiantes conozcan en qué consiste el polimorfismo y reconozcan las situaciones donde
éste debe implementarse.
 Que los estudiantes conozcan cómo especificar en Java que la ligadura sea estática.
 Que los estudiantes reconozcan la utilidad de los arreglos polimórficas.
 Que los estudiantes conozcan qué es una clase abstracta y los detalles de su implementación en
Java.

Introducción

Polimorfismo es una palabra de origen griego que significa aproximadamente “muchas formas”, (poly =
muchas, morphos = forma).

El uso de la herencia trae consigo un gran número de beneficios. El polimorfismo es uno de ellos,
veamos pues qué es polimorfismo.

En la POO este concepto se introduce en las jerarquías de clases, cuando la clase base y la derivada
declaran funciones con el mismo nombre, el mismo tipo y los mismos parámetros, o sea, funciones
polimórficas, las cuales son invocadas, en tiempo de ejecución, teniendo en cuenta el tipo de la instancia
desde donde se invoca, aún cuando ésta es tratada como clase base.

Retomemos, el ejemplo de la conferencia anterior. En este ejemplo se consideró que el salario se


calcula como el salario básico más un plus por cargo ocupado. Sin embargo, como es de suponer, el
salario de los docentes tiene en cuenta también su categoría. Los docentes cobran por encima de su
salario básico y el plus por cargo ocupado, un por ciento de su salario básico que se define como sigue:

Instructores: 10 %
Asistentes: 12%
Auxiliares: 15%
Titulares: 20%

¿Cómo modificar la jerarquía de clases definida para tener en cuenta las nuevas consideraciones?
Nótese que en la jerarquía definida anteriormente las clases Teacher y Admin heredan el método pay(),
definido en Worker, lo cual era suficiente para las condiciones dadas en aquel entonces. Sin embargo,
teniendo en cuenta que el pago a los profesores se calcula de manera distinta sería conveniente poder
redefinir dicho método en la clase profesor.

public class Worker extends Person{

CEIS, CUJAE 1
private String dpto;
protected float salary;
protected float plusCharge;

public float pay(){
return salary + plusCharge;
}
}

public class Teacher extends Worker {


private String category;

public float pay(){


float totalSalary = super.pay();
if(category.equalsIgnoreCase("Instructor"))
totalSalary += salary * 0.10;
if(category.equalsIgnoreCase("Asistente"))
totalSalary += salary * 0.12;
if(category.equalsIgnoreCase("Auxiliar"))
totalSalary += salary * 0.15;
if(category.equalsIgnoreCase("Titular"))
totalSalary += salary * 0.20;
return totalSalary;
}
}

Observe que la primera sentencia del método pay() de la clase Teacher, invoca al método pay() de la
clase Worker. Para ello se ha utilizado la sintaxis:
super.<nombre del método heredado(lista de argumentos)>
la misma sintaxis puede utilizarse para resolver conflictos de nombre al acceder a un campo heredado.
O sea, en el caso en que en la clase derivada exista algún campo de igual nombre de un campo de clase
base: super.<nombre del campo heredado>

Analicemos el comportamiento en Java, cuando un método redefinido en una clase derivada se invoca
desde una instancia de la clase derivada, tratada como clase base, como por ejemplo en:

Worker p = new Teacher();


float salary= p.pay();

siempre se invocará al método correcto según la clase de la instancia, o sea, si es un profesor, se


invocará al método pay() definido en Teacher, mientras que si se trata de un administrador, se invocará
al de Worker, pues Admin lo hereda de ésta.

Este mecanismo permite implementar una responsabilidad en la clase PersonCollection que


devuelva el monto a pagar a los trabajadores, de la siguiente manera:

public class PersonCollection {


private static final int DEFAULT_CAPACITY = 5;
private Person[] person;
int count;
...
public float totalPay(){
float sum = 0;
for(int i= 0; i < count ;i++)
if(person[i] instanceof Worker)
sum += (Worker)person[i].pay();

CEIS, CUJAE 2
return sum;
}

Nótese que es necesario asegurarse que se trate de una instancia de Worker, antes de invocar al
método pay(), pues dicho método no está definido para los estudiantes y podría provocarse un error al
invocarlo desde una instancia de Student. Por otra parte, cuando los objetos sean profesores y
administradores siempre se invocará al método correcto según la clase de la instancia, o sea, si es un
profesor, se invocará al método pay() definido en Teacher, mientras que si se trata de un administrador,
se invocará al de Worker, pues Admin lo hereda de ésta.

Pero, cómo el compilador sabe cual es el método que debe invocar en cada caso. Veamos a
continuación el mecanismo que desarrollan los compiladores para poder ofrecer dicho comportamiento y
veremos algunas diferencias de su implementación en lenguajes como C++ y Object Pascal.

Ligadura temprana y ligadura tardía

Se le llama ligadura al casamiento o enlace que se establece entre el punto donde se invoca a una
función y el código que debe ejecutarse. En el caso de la POO, como la invocación se hace desde un
objeto, influye en el resultado el objeto desde donde se invoca.

La ligadura puede hacerse en tiempo de compilación (early binding) o en tiempo de ejecución (late
binding). La ligadura temprana también se conoce como ligadura estática en contraposición con la
ligadura dinámica o tardía. Veamos cada una a través de un ejemplo:

EA 5.1 Unregistered Trial Version EA 5.1 Unregistered Trial Version


cd Cilindro Hueco

EA 5.1 Unregistered TrialCylinder


Version EA 5.1 Unregistered Trial Version
- length: float

EA 5.1 Unregistered Trial Version EA 5.1 Unregistered Trial Version


# PI: float = 3.1416f
# radius: float

+ baseArea() : float
EA 5.1 Unregistered
+ Trial Version
Cylinder(float, float) EA 5.1 Unregistered Trial Version
+ getLength() : float
+ getRadius() : float
EA 5.1 Unregistered Trial Version EA 5.1 Unregistered Trial Version
+ volume() : float

EA 5.1 Unregistered Trial Version EA 5.1 Unregistered Trial Version

EA 5.1 Unregistered Trial Version EA 5.1 Unregistered Trial Version


Hollow Cylinder
EA 5.1 Unregistered Trial Version
- innerRadius: float EA 5.1 Unregistered Trial Version
+ baseArea() : float
EA 5.1 Unregistered Trial Version
+ getInnerRadius() : float EA 5.1 Unregistered Trial Version
+ HollowCylinder(float, float, float)

EA 5.1 Unregistered Trial Version EA 5.1 Unregistered Trial Version

public class
EA 5.1 Unregistered Cylinder {
Trial Version EA 5.1 Unregistered Trial Version
protected static final float PI= 3.1416f;
protected float radius;
EA 5.1 Unregisteredprivate
Trial Version EA 5.1 Unregistered Trial
float length; Version

EA 5.1 Unregistered Trial Version EA 5.1 Unregistered Trial Version


CEIS, CUJAE 3
EA 5.1 Unregistered Trial Version EA 5.1 Unregistered Trial Version
public Cylinder(float radius, float length) {
this.radius = radius;
this.length = length;
}
public float getLength() {
return length;
}
public float getRadius() {
return radius;
}
public float baseArea(){
return PI * radius * radius;
}
public float volume(){
return baseArea() * length;
}
}

El volumen de un cilindro se calcula multiplicando el área de la base por su longitud. Nótese que este
comportamiento es el mismo para cualquier tipo de cilindro, por lo que ese método no es necesario
redefinirlo en la clase HollowCylinder. En qué se diferencia, entonces, el volumen del cilindro hueco. La
diferencia de comportamiento está en la forma de calcular el área de la base, por lo que sólo se necesita
redefinir el método baseArea().

public class HollowCylinder extends Cylinder {


private float innerRadius;

public HollowCylinder(float radius, float length, float innerRadius) {


super(radius, length);
if (innerRadius >= radius)
this.innerRadius = innerRadius;
else
this.innerRadius = radius;
}
public float getInnerRadius() {
return innerRadius;
}
public float baseArea(){
float r= radius - innerRadius;
return PI * r * r;
}
}

Si la ligadura es temprana, el comportamiento por defecto de la mayoría de los compiladores, la


invocación a baseArea() dentro del volumen se enlaza con el código del método correspondiente de la
clase Cylinder, como se muestra en la figura, por lo que cualquier invocación a volume(), ya sea desde
una instancia de cilindro como desde una instancia de cilindro hueco, se ejecutará calculando el área de
un círculo de radio radius.

CEIS, CUJAE 4
Cylinder.volume() Cylinder.baseArea()

HollowCylinder.baseArea()
HollowCylinder.volume()

Cylinder c= new HollowCylinder(10,10,5);


HollowCylinder c1= new HollowCylinder(10,9,3);
float v= c.volume();
float v1= c1.volume();

Si la ligadura fuera temprana, en cualquiera de los dos ejemplos de invocación se obtendría un resultado
incorrecto. Cuando se realiza la llamada al método volume(), el compilador busca su implementación en
la jerarquía para ejecutarlo, encontrándolo en la clase Cylinder. Dentro de este método se realiza una
llamada al método baseArea() que está definido en la clase Cylinder y redefinido en HollowCylinder.
Debido a la ligadura temprana, el método volume() de Cylinder se enlazó con el método baseArea() de
Cylinder por lo que se accederá al mismo y no al de la clase derivada HollowCylinder como se muestra
en la figura. Por tanto, no se obtendrán los resultados esperados.

Para que haya un comportamiento polimórfico, la “ligadura” entre la función y el objeto que la invoca
debe ser tardía, o sea, en tiempo de ejecución, de manera que se pueda determinar qué tipo de
instancia está invocando la función y enlazar con la función correcta. De esta manera siempre se
invocará la función correspondiente según la clase de instancia que la invoque. Véase la figura siguiente:

Cylinder.volume() Cylinder.baseArea()

HollowCylinder.volume() HollowCylinder.baseArea()

Cuando se realiza la llamada al método volume(), el compilador busca su implementación en la


jerarquía, encontrándolo en la clase Cylinder. Dentro de este método se realiza una llamada al método
baseArea() que está definido en la clase Cylinder y redefinido en HollowCylinder. Debido a la ligadura
tardía, en este caso se accede al método baseArea() de la clase HollowCylinder y no al de la clase
padre como se muestra en la figura. Por tanto, se obtienen los resultados esperados.

El compilador de Java realiza todas las ligaduras tardías, es decir, en tiempo de ejecución. Por esta
razón, tanto en la invocación a pay() como a volume() en las jerarquías de personas y cilindros
respectivamente se ejecutarán las funciones correctas. Veamos como ocurre la ligadura en el método
que calcula el monto total del pago a los trabajadores.

CEIS, CUJAE 5
worker

est. Eva pay()

Teacher Admin
prof. Juan admin Ana
pay()

public float totalPay(){


float sum = 0;
for(int i= 0; i < count ;i++)
if(person[i] instanceof Worker)
sum = (Worker)person[i].pay();
return sum;
}

CEIS, CUJAE 6
worker

est. Eva pay()

Teacher Admin
prof. Juan admin Ana
pay()

public float totalPay(){


float sum = 0;
for(int i= 0; i < count ;i++)
if(person[i] instanceof Worker)
sum = (Worker)person[i].pay();
return sum;
}

Sin embargo, es necesario resaltar que, la mayoría de los compiladores tienen un comportamiento
estático por defecto, por lo que cuando se desea que realice una ligadura dinámica será necesario
especificarlo explícitamente. En lenguajes como C++ y Object Pascal, los métodos que se desean ligar
dinámicamente deben ser declarados como virtuales (virtual). Para ello se crea una tabla de métodos
virtuales donde se guarda con quién se debe enlazar cada invocación según el objeto desde donde se
realiza.

Métodos final-

Como en Java, por defecto, se implementa la ligadura tardía, se requiere un mecanismo que permita
especificar si en ciertos casos se desea una ligadura temprana. Para ello se utiliza la palabra reservada
final en la declaración del método. O sea, que deshabilita la ligadura dinámica o tardía, indicándole al
compilador que esta ligadura es innecesaria. Cuando se declara un método como final, éste no puede
ser redefinido en ninguna clase hija, lo cual evita la sobrescritura.

Arreglos polimórficos

En virtud de la compatibilidad de tipos para la asignación, las clases derivadas pueden ser tratadas como
clase base, o sea, donde cabe un padre cabe cualquiera de sus hijos. Debido a ello, como ya hemos
visto anteriormente, es posible tener listas de diferentes tipos de instancias, siempre que ellas
pertenezcan a la misma jerarquía y todas sean tratadas como clase base.

Cuando las jerarquías de clases implementan polimorfismo (métodos que se ligan dinámicamente) y se
crean colecciones de objetos, de clases derivadas tratados como clase base, decimos que estamos en
presencia de arreglos polimórficos. Sobre estos arreglos se pueden implementar construcciones como la
vista anteriormente para calcular el monto total del pago a los trabajadores.

CEIS, CUJAE 7
Clases Abstractas y clase concretas

Existen clases cuya única razón de ser es proporcionar código (miembros de datos y funciones
miembros) para ser reusado por otras clases derivadas y de las cuales nunca existirán instancias. Tal es
el caso, por ejemplo, de Person. En el caso de estudio visto en la conferencia anterior todas las
personas que existen son estudiantes, profesores o administradores, por lo que cualquier instancia
siempre será de una de éstas específicamente y nunca será necesario construir instancias de Person. A
las clases como Person, se les conoce como clases abstractas en contraposición con el término usado
para clases que se pueden instanciar como es el caso de Student, Teacher y Admin, a las cuales se les
conoce como clases concretas.

En ocasiones las clases abstractas no tienen suficiente conocimiento para implementar determinadas
funciones, cuyas implementaciones sólo tienen sentido en el marco de instancias derivadas. En estos
casos es conveniente declarar la función en la clase base, sólo para garantizar que se pueda producir
una ligadura dinámica con la(s) función(es) correspondiente.

Para declarar que una clase es abstracta, se utiliza la palabra clave abstract en la declaración de la
clase.

public abstract class Person {


. . .
}

El compilador prohíbe la instanciación de una clase abstracta; por tanto, si se intenta instanciar una clase
abstracta el compilador dará un error y no compilará el programa.

Una clase abstracta puede contener métodos abstractos, esto es, métodos que no tienen
implementación. De esta forma, una clase abstracta puede definir un comportamiento de programación
completo, proporcionando a sus subclases la declaración de todos los métodos necesarios para
implementar la lógica de programación. Sin embargo, las clases abstractas pueden dejar algunos
detalles o toda la implementación de aquellos métodos a sus subclases.

Por ejemplo, imaginemos el siguiente criterio hipotético de valoración de la importancia de las


publicaciones para el ejercicio de la biblioteca visto en la clase práctica pasada. Existen tres valoraciones
posibles: alta, media y normal. Los libros son de importancia alta si se trata de un ejemplar único, media
si existen entre 2 y 20 ejemplares y normal si existen más de 20. Las revistas son de importancia alta si
son de antes de 1800, media si fueron publicadas entre 1801 y 1900 y normal las publicadas posterior al
1900, mientras que los artículos son todos de valoración normal.

Como puede apreciarse, sólo las instancias podrán responder a una pregunta sobre su valoración ya que
ésta depende del tipo de la instancia y la clase Publication no tiene cómo saber cuál es el mecanismo
para determinar la valoración de la instancia concreta de que se trate. Sería necesario entonces hacer
las siguientes modificaciones a la jerarquía:

public enum Importance {Alta, Media, Normal}

public abstract class Publication {


...
public abstract Importance importance() ;

public class Magazine extends Publication {


...
public Importance importance (){
if (year < 1800)

CEIS, CUJAE 8
return Importance.Alta;
else
if (year >= 1801 && year <= 1900)
return Importance.Media;
return Importance.Normal;
}

public abstract class PublicationAuthor extends Publication {


...
public abstract Importance importance() ;

public class Book extends PublicationAuthor {


...
public Importance importance () {
if (super.getCountCopy() == 1)
return Importance.Alta;
else
if (super.getCountCopy() >= 2 && super.getCountCopy()<=20)
return Importance.Media;
return Importance.Normal;
}

public class Article extends PublicationAuthor {


...
public Importance importance() {
return Importance.Normal;
}
}

Dada la definición del método importance()en toda a jerarquía, será posible la siguiente construcción
en un método de la clase PublicationCollection:

int p = find("AB123F35");
if (p != -1)
return publications[p].importance();

Una clase abstracta no necesita contener un método abstracto. Pero todas las clases que contengan un
método abstracto o no proporcionen implementación para cualquier método abstracto declarado en sus
superclases debe ser declarada como una clase abstracta. En el ejemplo la clase PublicationAuthor
también es abstracta, ya que no proporciona implementación para un método abstracto de su clase
base.

Los métodos abstractos deben tener la misma firma a lo largo de toda la jerarquía.

CEIS, CUJAE 9
Estudio independiente

El Complejo Lácteo de Ciudad de La Habana fabrica y comercializa quesos en tres formas distintas:
queso esférico, queso cilíndrico y queso en forma de cilindro hueco, como se muestra en la siguiente
figura:

Dicha entidad requiere un sistema que procese de forma automática su actividad comercial,
esencialmente el cálculo de la factura que cada cliente debe pagar conociendo de todos los quesos el
precio base, el precio unitario y la medida de su radio. Si el tipo de queso es cilíndrico se conoce su
longitud y si es del tipo cilindro hueco se conoce además el radio interior. El cálculo del precio total de la
factura se realiza mediante la fórmula:
PrecioTotal = Costo Base + CostoUnitario * Volumen

Precio total es igual a la suma de los queso pasdos por parámetro en un arraylist

Además, la factura contiene los datos del cliente y de cada unidad de queso que se ha solicitado. De los
clientes se conoce el nombre, dirección y teléfono.

Diseñe las clases necesarias para modelar la situación anterior utilizando lenguaje UML.

Implemente métodos para:

a) Calcular el volumen de cada unidad de queso, conociendo que:


Esfera: 4/3 *  * radio3
Cilindro:  * radio2 * longitud
Cilindro hueco:  * longitud * (radioe2 – radioi2) radioe > radioi
b) Calcular el precio total de la factura.
c) Obtener todos los quesos de cada tipo y la cantidad de ellos.
d) Obtener el precio del queso esférico de mayor volumen de una factura.

CEIS, CUJAE 10

También podría gustarte