Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Conceptos y mecanismos de la
programación orientada a objetos
15
16 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS
de la abstracción “punto en el plano”. Todos los objetos que puedan ser identificados como
puntos en el plano estarán caracterizados por un estado, determinado por sus coordenadas
(abscisa y ordenada), y un comportamiento, fijado por las operaciones que pueden ser aplica-
das sobre esos objetos (trasladar un punto, calcular la distancia entre dos puntos, obtener la
abscisa u ordenada de un punto, cambiar la abscisa u ordenada, etc.).
El listado 2.1 muestra la descripción en Java de la clase Punto, caracterizada por un es-
tado (las variables x e y) y un comportamiento (las operaciones trasladar, distancia,
abscisa y ordenada). Definiciones similares en otros lenguajes como Smalltalk, C++ e Eif-
fel, pueden consultarse en los listados 2.2, 2.3 y 2.4, respectivamente2 . Obsérvese que, aunque
la sintaxis es diferente (especialmente la utilizada en el lenguaje Smalltalk), los elementos des-
critos en todos los casos son los mismos. Por un lado, encontramos las variables que definen el
estado de los objetos (denominadas variables de instancia en Smalltalk y Java, datos miembro en
C++ y atributos en Eiffel), las operaciones que determinan su comportamiento (denominadas
métodos en Smalltalk y Java, funciones miembro en C++ y rutinas en Eiffel) y las operaciones que
definen la construcción de objetos (denominadas constructores).
Es importante hacer notar, sobre todo a los programadores habituados a lenguajes estruc-
turados (no orientados a objetos), que el número de argumentos de los métodos suele ser uno
menos en los lenguajes orientados a objetos. Ası́, la operación trasladar posee dos argumentos
(los incrementos de las coordenadas), y el método distancia sólo uno, en vez de tener tres y
dos argumentos, respectivamente, como ocurrirı́a en un lenguaje no orientado a objetos. Esto
se debe a que uno de los argumentos coincide con el objeto representado por la clase defini-
da: el método distancia calcula el espacio entre el punto representado por la clase, que recibe el
mensaje, y el punto pasado como argumento. Esto se entenderá mejor en la sección 2.2.
Con objeto de representar clases independientemente del lenguaje en el que se implemen-
ten, utilizaremos el lenguaje unificado de modelado (Unified Model Language –UML), que es
una notación diagramática de comprensión intuitiva. A modo de ejemplo, la clase Punto se re-
presentarı́a según se muestra en la figura 2.1. Aunque no sea objetivo de esta sección, iremos
introduciendo algunas caracterı́sticas de la notación UML. Las clases se representan median-
te cajas divididas (habitualmente) en tres partes verticalmente: la parte superior que contiene
el nombre de la clase, la parte intermedia para contener las variables que definen el estado
y la parte inferior que contiene los métodos que caracterizan el comportamiento. Tanto las
variables como los métodos pueden venir precedidos por un sı́mbolo, indicando el nivel de
protección de la información: público (+), protegido (#), a nivel de paquete (˜) y privado (-).
Volveremos sobre el tema de la ocultación de información en las secciones siguientes.
De este modo, una clase no es más que la abstracción de un grupo de objetos que exhiben
un comportamiento común. Los objetos correspondientes a una misma clase difieren entre
sı́ por su identidad y, posiblemente, por los valores de sus variables de instancia (es decir, su
estado).
La creación de objetos (también denominados instancias) a partir de una clase depende
de las construcciones que el lenguaje proporcione para ello. Existen distintas variaciones al
respecto: desde el uso de una primitiva especial (make instance, new) aplicada a clases, como
ocurre en CLOS, hasta el uso de una primitiva similar (create) aplicada a objetos, como ocurrı́a
en las primeras versiones de Eiffel, pasando por la solución de Smalltalk (adoptada por la
2 Lo que nos interesa de estos códigos es dar una idea intuitiva de cómo aparecen los distintos conceptos de la POO
en los distintos lenguajes. No nos preocupamos de momento de algunas de las particularidades de estos lenguajes,
como las palabras reservadas public o private de Java para establecer niveles de visibilidades, uso de funciones
matématicas como pow o sqrt, etc.
18 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS
class Punto {
/ / Coordenadas d e l punto
p r i v a t e double x , y ;
/ / Devuelve e l v a l o r de l a coordenada x
public double a b s c i s a ( ) {
return x ;
}
/ / Devuelve e l v a l o r de l a coordenada y
public double ordenada ( ) {
return y ;
}
/ / Cambia e l v a l o r de l a coordenada x
public void a b s c i s a ( double nuevaX ) {
x = nuevaX ;
}
/ / Devuelve e l v a l o r de l a coordenada y
public void ordenada ( double nuevaY ) {
y = nuevaY ;
}
}
O b j e c t s u b c l a s s : # Punto
instanceVariableNames : ’ x y ’
classVariableNames : ’ ’
poolDictionaries : ’ ’
! Class methods
new
” M étodo de c l a s e para c r e a r un punto con coordenadas ( 0 , 0 ) ”
ˆ ( super new) a b s c i s a : 0 ordenada : 0
x : unNumero y : otroNumero
” M étodo de c l a s e para c r e a r un punto con coordenadas i n i c i a l e s ”
ˆ ( s e l f new) a b s c i s a : unNumero ordenada : otroNumero
! I n s t a n c e methods
abscisa
” Devuelve l a a b s c i s a d e l punto ”
ˆx
ordenada
” Devuelve l a ordenada d e l punto ”
ˆy
a b s c i s a : unNumero
” Cambia l a a b s c i s a d e l punto ”
x : = unNumero
ordenada : unNumero
” Cambia l a ordenada d e l punto ”
y : = unNumero
t r a s : unNumero l a d a r : otroNumero
” Incrementa l a s coordenadas d e l punto ”
x : = x + unNumero .
y : = y + otroNumero
d i s t a n c i a : unPunto
” C a l c u l a l a d i s t a n c i a e n t r e e s t e punto y e l argumento ( unPunto ) ”
ˆ ( ( x − unPunto a b s c i s a ) squared +
( y − unPunto ordenada ) squared ) ) s q r t
class Punto {
private :
/ / Coordenadas d e l punto
double x , y ;
public :
/ / Devuelve e l v a l o r de l a coordenada x
double a b s c i s a ( ) {
return x ;
}
/ / Devuelve e l v a l o r de l a coordenada y
double ordenada ( ) {
return y ;
}
/ / Cambia e l v a l o r de l a coordenada x
void a b s c i s a ( double nuevaX ) {
x = nuevaX ;
}
/ / Cambia e l v a l o r de l a coordenada y
void ordenada ( double nuevaY ) {
y = nuevaY ;
}
}
c r e a t i o n −− C o n s t r u c t o r e s
origen ;
nuevo ;
f e a t u r e −− R u t i n a s
origen is
−− C o n s t r u c t o r para i n i c i a l i z a r a 0 ambas coordenadas
abscisa := 0;
ordenada : = 0 ;
end ;
t r a s l a d a r ( a : REAL, b : REAL) i s
−− Incrementa l a s coordenadas d e l punto
do
x := x + a ;
y := y + b ;
end ;
d i s t a n c i a ( p t o : PUNTO) : REAL i s
−− C a l c u l a l a d i s t a n c i a e n t r e e s t e punto y e l argumento
do
Result : = s q r t ( ( x − p t o . x ) ˆ 2 + ( y − p t o . y ) ˆ 2 ) ;
end ;
a b s c i s a ( nuevaX :REAL) i s
−− Cambia e l v a l o r de l a coordenada x
do
x : = nuevaX ;
end ;
end −− c l a s e PUNTO
ONML
HIJK
(1, 3)
última versión de Eiffel, que consiste en considerar, dentro de la descripción de la clase, una
familia especial de métodos para crear instancias, denominados métodos de clase en Smalltalk
y rutinas de creación en Eiffel. Algo similar se da en lenguajes como C++ y Java, pero con la
restricción de que dichos métodos (denominados constructores) se han de denominar como las
clases, y la creación de objetos ha de realizarse invocándolos mediante el operador new. Ası́,
atendiendo a las diferentes versiones de la clase Punto la creación de un objeto se realizarı́a en
Java del modo siguiente:
Punto p = new Punto(1, 3);
donde en la misma instrucción declaramos una variable p de tipo Punto, creamos una instancia
de esta clase con coordenadas 1 y 3 y dejamos a p referenciando a dicho objeto. En C++ se harı́a
de forma muy similar, mientras que en Eiffel la secuencia de instrucciones serı́a:
p: PUNTO;
!!p.nuevo(1, 3);
y en Smalltalk:
p := Punto abscisa: 1 ordenada: 3
Gráficamente, la situación después de cualquiera de estas instrucciones podrı́a represen-
tarse como se muestra en la figura 2.2.
p p
_ _−1)_ _ _/ HIJK
ONML ONML
HIJK
_ _ _ _ trasladar
_ _ _ (1, (1, 3) (2, 2)
Figura 2.3: Mensaje trasladar a un punto (izqda.) y cambio de estado resultante (dcha.).
C++ lo normal es hablar de dato y función miembros. En lo que sigue utilizaremos indistin-
tamente cualquiera de estas formas para hacer referencia a los componentes básicos de una
clase.
En general, a la hora de tratar la forma en que se invocan las operaciones de una clase dada,
se utiliza la metáfora del paso de mensajes. Ası́, la invocación de un método de una clase se en-
tiende como la emisión de un mensaje adecuado sobre un objeto de la clase, pero el significado
real no difiere mucho de la llamada a procedimiento, caracterı́stica de cualquier lenguaje de
programación imperativo. Una de las consecuencias del uso de esta metáfora es que el número
de argumentos de un método en un lenguaje orientado a objetos es uno menos que el número
de argumentos de una operación equivalente en un lenguaje estructurado no orientado a obje-
tos: el argumento adicional se corresponde con el receptor del mensaje. Por ejemplo, mientras
que en un lenguaje como C o Pascal, el cálculo de la distancia entre dos puntos se realiza con
una invocación como distancia (pto1,pto2), en un lenguaje como Java, la misma acción se realizarı́a
enviando a uno de los puntos un mensaje del modo siguiente: pto1.distancia (pto2).
El acceso a los datos y operaciones que agrupa una clase puede o no estar protegido. La
forma y criterios para ocultar información depende del lenguaje que se considere. Ası́, por
ejemplo, mientras en Smalltalk el criterio que se sigue está predeterminado3 , otros lenguajes,
como C++, Eiffel o Java, establecen diversos mecanismos de privatización y exportación de
rutinas, que limitan el conjunto de mensajes admisibles por los objetos de la clase, y permiten
una mejor ocultación de información.
El comportamiento de un objeto está totalmente determinado por la respuesta de los men-
sajes que acepta, y es independiente de la representación de datos utilizada para definir sus
variables de instancia. Además, el único conocimiento que el resto de objetos tiene del mis-
mo viene dado por los mensajes que puede recibir. La metáfora del paso de mensajes en la
programación orientada a objetos facilita la abstracción a dos niveles: los emisores tienen una
visión abstracta de los receptores (dada por la interfaz de la clase a la que éstos pertenecen) y,
al mismo tiempo, éstos no tienen conocimiento explı́cito de aquéllos.
Las operaciones de un objeto comparten su estado, de manera que todos los cambios que
un mensaje produce en las variables de instancia afectarán a mensajes posteriores. Ası́, las
variables de instancia no son locales a las operaciones, pero sı́ al objeto. Por ejemplo, si el
punto p previamente creado recibe un mensaje del modo siguiente:
p.trasladar(1, -1);
sus coordenadas cambiarán a (2, 2). La figura 2.3 ilustra el cambio en el estado del objeto al
que referencia p ante la recepción del dicho mensaje.
Los mensajes que se envı́an a un objeto deben corresponder a alguno de los métodos defini-
dos en la clase a la que éste pertenece. Esta correspondencia se debe reflejar en la signatura del
método: nombre (o selector) del método, número de argumentos y sus tipos. En los lenguajes
3 En Smalltalk las variables de instancia se mantienen privadas a la clase, de forma que el acceso y modificación de
éstas sólo se puede realizar a través de métodos, que siempre se consideran públicos.
24 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS
2.3. Herencia
La herencia permite reutilizar el comportamiento de una clase en la definición de otras nue-
vas. Las subclases de una clase heredan las operaciones y variables de ésta y pueden añnadir
nuevas operaciones y nuevas variables de instancia. La herencia es una relación entre clases,
no entre objetos, y caracterizan la programación orientada a objetos.
La herencia juega un papel muy importante en los lenguajes orientados a objetos debido a
que captura una forma de abstracción de nivel superior al de la abstracción de datos, comple-
mentándolo. Esta afirmación no contradice sin embargo la opinión de muchos autores que no
reconocen en este mecanismo el aspecto más relevante de la orientación a objetos.
La herencia puede expresar distintas relaciones entre comportamientos: clasificación, espe-
cialización, generalización y aproximación.
Por ejemplo, a partir de la clase Punto considerada en la sección anterior es posible definir
una nueva clase Partı́cula , que represente objetos que incluyen las propiedades de los puntos
en el plano, añadiendo además nuevas caracterı́sticas como, por ejemplo, la posesión de masa
y la posibilidad de calcular la fuerza de atracción entre partı́culas. Se puede observar la des-
cripción en UML de esta clase en la figura 2.4. Entre los nuevos elementos de la notación UML
que aparecen, obsérvese que la relación de herencia se representa mediante una flecha sólida
orientada desde la clase heredera hacia la heredada, y las variables estáticas se representan
subrayándolas.
El listado 2.5 muestra la declaración en Java de una clase como la descrita4 . Definiciones
similares en C++, Smalltalk y Eiffel se ofrecen en los listados 2.6, 2.7 y 2.8, respectivamente.
Conceptualmente, la relación de herencia responde a una relación de especialización entre
clases de objetos, de forma que los objetos de la clase heredera (subclase o hija) se pueden
considerar también instancias de la clase heredada (superclase o padre). En efecto, en el ejemplo
toda partı́cula es también un punto, y hereda su comportamiento; es decir, una partı́cula posee
coordenadas y es posible calcular la distancia entre dos partı́culas. La diferencia radica en que,
además, poseen masa y es posible calcular la fuerza de atracción entre dos partı́culas.
4 Obsérvese que se ha definido una variable etiquetada como final y estática para representar la constante de gravi-
tación universal. En posteriores capı́tulos se definirán los conceptos de entidad estática (o de clase) y final. Ahora nos
basta saber que una variable final y estática es una constante.
D UR ÁN , G UTI ÉRREZ Y P IMENTEL 25
/ / Masa de l a p a r t ı́ c u l a
p r i v a t e double masa ;
/ / C a l c u l a l a f u e r z a de a t r a c c i ó n e n t r e e s t a p a r t ı́ c u l a ( t h i s )
/ / y l a p a r t ı́ c u l a que se pasa como argumento
public double a t r a c c i ó n ( P a r t ı́ c u l a p a r t ) {
r e t u r n G∗masa∗ p a r t . masa / Math . pow ( t h i s . d i s t a n c i a ( p a r t ) , 2 ) ;
}
/ / Devuelve e l v a l o r de l a masa
public double masa ( ) {
r e t u r n masa ;
}
}
/ / Masa de l a p a r t ı́ c u l a
double masa ;
public :
/ / C o n s t r u c t o r para i n i c i a l i z a r a 0 coordenadas y masa
P a r t i c u l a ( ) : Punto ( ) {
masa = 0 ;
}
/ / C a l c u l a l a f u e r z a de a t r a c c i ó n e n t r e dos p a r t ı́ c u l a s
double a t r a c c i o n ( P a r t i c u l a p a r t ) {
r e t u r n G∗masa∗ p a r t . masa / pow ( t h i s . d i s t a n c i a ( p a r t ) , 2 ) ;
}
/ / Devuelve e l v a l o r de l a masa
double masa ( ) {
r e t u r n masa ;
}
}
Punto s u b c l a s s : # P a r t i c u l a
instanceVariableNames : ’ masa ’
classVariableNames : ’G ’
poolDictionaries : ’ ’
! Class methods
! I n s t a n c e methods
masa : unNumero
” M o d i f i c a l a masa de l a p a r t ı́ c u l a ”
masa : = unNumero
masa
” Devuelve l a masa de l a p a r t ı́ c u l a ”
ˆ masa
atraccion : unaParticula
” C a l c u l a l a f u e r z a de a t r a c c i ó n e n t r e dos p a r t ı́ c u l a s ”
ˆ G∗masa ∗ ( u n a P a r t i c u l a masa ) / ( s e l f d i s t a n c i a : u n a P a r t i c u l a ) squared
c r e a t i o n −− C o n s t r u c t o r e s
cero ;
nuevo ;
f e a t u r e −− R u t i n a s
cero i s
−− C o n s t r u c t o r para i n i c i a l i z a r a 0 coordenadas y masa
do
super . o r i g e n ;
masa : = 0 ;
end ;
a t r a c c i o n ( p a r t : PARTICULA ) : REAL i s
−− C a l c u l a l a f u e r z a de a t r a c c i ó n e n t r e dos p a r t ı́ c u l a s
do
Result : = G∗masa∗ p a r t . masa / Current . d i s t a n c i a ( p a r t ) ˆ 2 ;
end ;
end −− c l a s e PARTICULA
realice de forma adecuada y garantizando que las coordenadas no superan los lı́mites estable-
cidos. En la implementación en Java de la clase PuntoAcotado que se muestra en el listado 2.9 se
ha considerado una redefinición del método que considera la zona rectangular homeomorfa a
un toro; es decir, trasladar un punto más allá de alguna de las paredes que lo acotan hace que
éste aparezca por la pared opuesta.
/ / Devuelve e l ancho d e l r e c t á n g u l o
double ancho ( ) {
r e t u r n esquinaD . a b s c i s a ( ) − e s q u i n a I . a b s c i s a ( ) ;
}
/ / Devuelve e l a l t o d e l r e c t á n g u l o
double a l t o ( ) {
r e t u r n esquinaD . ordenada ( ) − e s q u i n a I . ordenada ( ) ;
}
anterior se resuelven definiendo una clase que herede de forma simultánea de distintas clases
(figura 2.6), y creando instancias de la clase resultante.
La herencia múltiple constituye un mecanismo muy importante en la programación orien-
tada a objetos, debido al interés que suscita sobre todo en el campo de la Inteligencia Artificial
por su gran poder de clasificación, y gracias a que facilita la modularidad, reutilización y di-
seño incremental. A pesar de todo, plantea muchos problemas, tanto su implementación como
su interpretación conceptual. Ello se debe a la variedad de formas en que se puede combinar el
comportamiento de clases distintas. Los inconvenientes más importantes de la herencia múlti-
ple se presentan en presencia de lo que se denominan conflictos y herencia repetida de atributos.
Los conflictos se presentan a la hora de decidir qué versión de una operación se da cuando
una clase hereda dos o más versiones de una rutina con una misma signatura proveniente de
distintos ancestros. Ası́, por ejemplo, en la clase PuntoColoreado, ilustrada en la figura 2.3.3, apa-
rece una ambigüedad que debe ser resuelta pues el método distancia es heredado de las clases
Punto y Color. En efecto, es necesario decidir qué versión del método distancia es heredada en la
clase PuntoColoreado. En este caso, parece claro que es más interesante considerar la versión de
Punto.
Se han propuesto muchas y variadas soluciones para resolver el problema de los conflictos
en herencia múltiple; desde fijar estrategias de búsqueda en el grafo de herencia (primero en
profundidad o en amplitud), hasta proporcionar mecanismos explı́citos para evitarlos. Aun-
que lo más cómodo, desde el punto de vista del programador, es la primera solución, ésta
no resulta adecuada de forma general —fijada una estrategia de búsqueda siempre es posi-
ble encontrar situaciones que no se resuelven adecuadamente—, por lo que la tendencia de
los lenguajes orientados a objetos actuales es la de evitar la producción de conflictos. Ası́, por
ejemplo, Eiffel introduce un mecanismo de renombramiento de rutinas y de herencia selec-
tiva, mientras que C++ exige al programador el uso de un operador de ámbito para evitar
ambigüedades. En Eiffel, el método distancia procedente de la clase Color se podrı́a heredar
renombrándolo como distanciaCromática, eliminando de esta forma la ambigüedad an-
terior y manteniendo ambas versiones del método heredado. Alternativamente, Eiffel permite
utilizar herencia selectiva para seleccionar cuál de las versiones desea heredar (por ejemplo,
32 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS
el de la clase Punto) desechando las demás. De forma similar, C++ permite la coexistencia de
ambas versiones de distancia en el contexto de la clase PuntoColoreado, pero cualquier invocación
al método debe ir precedida por el identificador de la clase de la que procede: Punto::distancia
y Color :: distancia . Otros lenguajes optan por eliminar el problema permitiendo sólo herencia
simple (Smalltalk, Java).
Otro problema existente en lenguajes que permiten herencia múltiple se da cuando se pro-
duce herencia repetida; es decir, una clase hereda de otra a través de caminos distintos. El
problema sobreviene en estos casos cuando la creación de objetos no tiene en cuenta este tipo
de situaciones (generalmente, por motivos de eficiencia) y las variables de instancia correspon-
dientes a la clase heredada por diferentes caminos son duplicadas. Este problema se produce
en C++ cuando, por ejemplo, se definen jerarquı́as de herencia como las de la figura 2.7, donde
la clase Pixel representa puntos en una pantalla que tienen asociado un determinado color. Al
crearse una instancia de esta clase en C++ se reserva memoria para las variables de instancia
heredadas de cada una de sus dos superclases directas (PuntoAcotado y PuntoColoreado, por ejem-
plo, las esquinas que delimitan el rectángulo —pantalla—), pero también para dos copias de
las variables x e y heredadas de Punto. El problema radica en que estas dos copias pueden llegar
a almacenar valores inconsistentes. Lenguajes como Eiffel no presentan este inconveniente,
y en C++ es posible resolverlo con una variante de la herencia que implementa por defecto,
denominada herencia virtual, y que es algo menos eficiente.
Obsérvese que mientras que el problema de los conflictos por ambigüedad es conceptual,
el ocasionado por la herencia repetida está motivado por una decisión de implementación, que
en el caso de C++ se toma por la constante búsqueda de la eficiencia en tiempo de ejecución.
En cualquier caso, hemos de resaltar que la herencia múltiple, aunque en útil en casos muy
especı́ficos, no es esencial como mecanismo lingüı́stico, y siempre es posible encontrar una
alternativa de diseño combinando la herencia simple y la composición (ver sección 2.4).
D UR ÁN , G UTI ÉRREZ Y P IMENTEL 33
La herencia de clases es esencialmente una forma estática de herencia. De ese modo, las cla-
ses heredan nuevas propiedades cuando son definidas, y no en tiempo de ejecución. Una vez
que la clase se ha definido, las propiedades de sus instancias están totalmente determinadas.
Existen sistemas, sobre todo en el contexto de las bases de datos orientadas a objetos, en los
que es posible la modificación de clases en tiempo de ejecución. Eso hace que las instancias
de una clase puedan presentar diferentes propiedades en distintos momentos. No obstante,
esta posibilidad no puede confundirse con algún tipo de herencia dinámica, debido a que la
redefinición de clases no es una operación que actúe sobre los objetos, del mismo modo que
la modificación del esquema de una base de datos no puede considerarse una transacción de
la base de datos. Esto significa que los cambios dinámicos de la jerarquı́a de herencia deben
realizarse actuando sobre los objetos.
Figura 2.8: Diagrama UML que define la clase Vector a partir de la clase Punto
class V e c t o r {
/ / Extremo d e l v e c t o r
p r i v a t e Punto extremo ;
/ / C o n s t r u c t o r para i n i c i a l i z a r e l extremo
public V e c t o r ( Punto p t o ) {
extremo = p t o ;
}
/ / C a l c u l a e l m ódulo d e l v e c t o r
public double m ódulo ( ) {
r e t u r n (new Punto ( 0 , 0 ) ) . d i s t a n c i a ( extremo ) ;
}
/ / Devuelve l a p r o y e c c i ó n sobre e l e j e X
public double componenteX ( ) {
r e t u r n extremo . a b s c i s a ( ) ;
}
/ / Devuelve l a p r o y e c c i ó n sobre e l e j e Y
public double componenteY ( ) {
r e t u r n extremo . ordenada ( ) ;
}
/ / Devuelve e l v e c t o r o r t o g o n a l a e s t e v e c t o r
public V e c t o r o r t o g o n a l ( ) {
r e t u r n new V e c t o r (−componenteY ( ) , componenteX ( ) ) ;
}
En esto consiste el polimorfismo sobre los datos: una variable declarada inicialmente para
almacenar puntos en el plano, puede también almacenar partı́culas o puntos limitados a un
rectángulo. Aunque en el ejemplo las distintas referencias se producen como consecuencia de
una asignación explı́cita, efectos similares se pueden obtener mediante el paso de parámetros o
la devolución de un resultado por parte de una función. En efecto, la función distancia definida
en la clase Punto con un parámetro formal de tipo Punto podrı́a ser invocada con una partı́cula
como parámetro real:
Punto p t o = new Punto ( ) ;
P a r t ı́ c u l a p a r t = new P a r t ı́ c u l a ( 1 , −1, 100) ;
double d i s t = p t o . d i s t a n c i a ( p a r t ) ;
PuntoAcotado y el resultado de la traslación hace que el punto vuelva a su posición inicial (0, 0),
después de rebotar sobre la pared derecha del rectángulo.
Como hemos mencionado, en lenguajes como Java, Smalltalk e Eiffel la vinculación dinámi-
ca es el mecanismo que se utiliza por defecto para resolver las llamadas a los métodos, respon-
diendo a la forma natural del comportamiento de los objetos. Otros lenguajes, como C++,
aplican por defecto vinculación estática, y necesitan declarar las funciones de una forma es-
pecial (funciones virtuales) para que se resuelvan mediante vinculación dinámica. El problema
es que esta decisión se debe tomar en la raı́z de la jerarquı́a (en el ejemplo, en la clase Punto)
cuando aún el diseñador puede no conocer la existencia de las clases herederas ( Partı́cula o
PuntoColoreado); esto es lo que en la literatura se conoce como el problema de la clarividencia en
C++.
a b s t r a c t class Magnitud {
/ / M étodo a b s t r a c t o para comparar magnitudes .
a b s t r a c t public boolean menorQue ( Magnitud magn ) ;
tación de los métodos que no son abstractos, como mayorQue, no serı́a posible si la clase no
incluyese una declaración (aunque fuese sin implementación) del método menorQue, pues el
compilador no aceptarı́a la expresión this.menorQue(magn).
Otra alternativa al uso de métodos abstractos serı́a incluirlos en la clase e implementarlos
con el cuerpo vacı́o. Por ejemplo:
public boolean menorQue ( Magnitud magn ) { }
En este caso el problema radicarı́a en la imposibilidad de garantizar que las clases Fecha
y Hora incluyan una redefinición adecuada de los métodos abstractos. Por el contrario, la eti-
queta abstract permite que el compilador avise de situaciones en las que olvidemos dar una
implementación de estos métodos en las clases herederas. Toda clase declarada subclase de
una clase heredera ha de proporcionar implementaciones de todos los métodos abstractos, o
en su defecto ser declarada abstracta.
Gracias al polimorfismo sobre los datos (capacidad que tiene una variable tipada para hacer
referencia a instancias de distintas clases en tiempo de ejecución), la existencia de clases abs-
tractas adquiere una utilidad extraordinaria en los lenguajes orientados a objetos. Por ejemplo,
serı́a posible definir un método (en alguna clase) para comprobar si un array de magnitudes
está ordenado, sin necesidad de saber si el array es de fechas, horas o vectores:
public boolean est áOrdenado ( Magnitud [ ] magnitudes ) {
boolean ordenado = t r u e ;
int i = 0;
while ( i < magnitudes . l e n g t h && ordenado )
ordenado = magnitudes [ i ] . menorIgualQue ( magnitudes [++ i ] ) ;
r e t u r n ordenado ;
}
Obsérvese que de no estar definida la clase abstracta Magnitud habrı́amos tenido que imple-
mentar una versión del método para cada tipo de magnitud. Estas situaciones, y otras que
surgirán en capı́tulos posteriores hacen de las clases abstractas una herramienta muy útil en el
diseño orientado a objetos.
La noción de interfaz de Java lleva la idea de clase abstracta al lı́mite, de forma que una
interfaz puede considerarse como una clase “completamente” abstracta; es decir, con todos sus
métodos abstractos. El beneficio de tratar con interfaces radica en que es posible definir clases
que implementen (proporcionen una definición de) los métodos de múltiples interfaces. Esto
solventa, en cierta medida, la deficiencia expresiva de la herencia simple frente a la herencia
múltiple.
class I n t e r v a l o {
/ / V a r i a b l e s que d e f i n e n e l i n i c i o y e l f i n a l d e l i n t e r v a l o
Object i n i c i o , f i n ;
/ / C o n s t r u c t o r que i n i c i a l i z a l o s extremos d e l i n t e r v a l o
public I n t e r v a l o ( O b j e c t i , O b j e c t f ) {
inicio = i ;
fin = f ;
}
/ / Devuelve e l i n i c i o d e l i n t e r v a l o
Object i n i c i o ( ) {
return i n i c i o ;
}
/ / Devuelve e l f i n a l d e l i n t e r v a l o
Object f i n ( ) {
return f i n ;
}
deberı́a permitir parametrizar una clase que especifique el tipo abstracto de datos Lista con el
tipo base de los elementos que vaya a contener. Esta parametrización puede ser explı́cita cuan-
do el lenguaje permite la definición de clases parametrizadas (Eiffel, C++ —templates— o Java
desde la versión 1.5), o simulada mediante el uso de interfaces suficientemente genéricos.
Para ilustrar la noción de clase genérica, consideremos el listado 2.12 donde se define una
clase Intervalo cuyos objetos representan intervalos que van desde un inicio hasta un final , sin
determinar el tipo de éstos. La forma de generalizar el tipo de los extremos del intervalo es
utilizar en la declaración de las variables la clase más general posible: la clase Object, de la que
todas las clases heredan.
Esto permitirı́a definir intervalos de distintos tipos (fechas, horas, puntos, . . . ). El inconve-
niente de esta forma de implementar la genericidad es que no existe manera de distinguir el
tipo de un intervalo de fechas de uno de horas, por ejemplo. Esto implica que una secuencia
de instrucciones como la siguiente:
Hora nueveAM , medioDı́a , s a l i d a N i ñ o s D e l C o l e ;
...
I n t e r v a l o r e u n i ó n = new I n t e r v a l o ( nueveAM , medioDı́a ) ;
...
boolean l l e g o T a r d e = ( r e u n i ó n . f i n ( ) ) . mayorQue ( s a l i d a N i ñ o s D e l C o l e ) ;
no sea correcta desde el punto de vista del compilador, a pesar de que todo hace pensar que
D UR ÁN , G UTI ÉRREZ Y P IMENTEL 41
class I n t e r v a l o <T> {
/ / V a r i a b l e s que d e f i n e n e l i n i c i o y e l f i n a l d e l i n t e r v a l o
T inicio , fin ;
/ / C o n s t r u c t o r que i n i c i a l i z a l o s extremos d e l i n t e r v a l o
public I n t e r v a l o ( T i , T f ) {
inicio = i ;
fin = f ;
}
/ / Devuelve e l i n i c i o d e l i n t e r v a l o
T inicio () {
return i n i c i o ;
}
/ / Devuelve e l f i n a l d e l i n t e r v a l o
T fin () {
return f i n ;
}
sı́ lo es. Ello es debido a que la expresión reunión. fin () , según la definición de la clase Intervalo ,
es de tipo Object, y el mensaje mayorQue sólo puede ser recibido por una magnitud 6 . Este
inconveniente podrı́a resolverse parcialmente si en vez de la clase Object se hubiese utilizado
la clase abstracta Magnitud. El problema, en este caso, serı́a la imposibilidad de crear intervalos
de objetos que no fuesen magnitudes; por ejemplo, puntos.
Una forma alternativa de implementar este tipo de comportamiento genérico es el uso de
clases parametrizadas (disponibles en Eiffel, C++ y Java desde la versión 1.5). En el listado 2.13
se muestra una versión parametrizada de la clase Intervalo . En este caso, la forma de generalizar
el tipo de los extremos del intervalo es introducir en la definición de la clase un parámetro for-
mal (T en el ejemplo) y utilizarlo como si fuese el nombre de una clase: para definir variables,
declarar argumentos, devolver resultados, etc.
Los parámetros formales en las clases parametrizadas pueden instanciarse cuando se de-
claran variables de la clase genérica. Por ejemplo, podrı́amos definir intervalos horarios, en
cuyo caso el parámetro formal es instanciado con la clase Hora, o intervalos temporales, utili-
zando la clase Fecha, o segmentos en el plano, en cuyo caso los extremos serı́an objetos de la
clase Punto:
6 Aunque en tiempo de ejecución el objeto devuelto por la expresión reunión.fin() es medioDı́a, objeto de la
clase Hora y, por lo tanto, una magnitud, en tiempo de compilación esta información no se conoce.
42 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS
I n t e r v a l o <Hora> r e u n i ó n ;
I n t e r v a l o <Fecha> semanaSanta ;
I n t e r v a l o <Punto> segmento ;
Ahora sı́, una expresión como reunión. fin () es un objeto de tipo Hora, y como tal puede recibir
el mensaje mayorQue.
La instanciación de clases genéricas permite que el compilador garantice el buen uso de las
variables. Por ejemplo, una vez definida la variable reunión como un intervalo horario, sólo
es posible cambiar el inicio o el final del intervalo utilizando horas como argumentos. En otras
palabras, el compilador no admitirı́a un mensaje como:
r e u n i ó n . c a m b i a r F i n a l (new Punto ( 1 , 2 ) ) ;
debido a que el parámetro formal del método cambiarFinal en el contexto del objeto reunión tiene
como tipo la clase Hora.
Sin embargo, como uno se puede imaginar, no es ésta la única utilidad de este tipo de
clases. En efecto, como se verá en el capı́tulo 6 las ventajas ofrecidas por las clases genéricas
van mucho más allá.
Bibliografı́a
[US87] D. Ungar and R.B. Smith. Self: The power of simplicity. In N. Meyrowitz, editor, ACM
Conference on Object-Oriented Systems, volume 21 of SIGPLAN Notices, pages 227–242.
ACM Press/Addison-Wesley, 1987.
43