SlideShare una empresa de Scribd logo
1 de 26
Descargar para leer sin conexión
Resumen en Español:
CAPÍTULO 10
“El paradigma Orientado a Objetos”
LIBRO:
Programming Languajes:
Principles and Paradigms
by
Maurizio Gabrielli and Simone Martini
AUTOR:
Cristian Arciniegas
Chapter 10.- The Object-Oriented Paradigm (10.2, 10.3)
10.2 CONCEPTOS FUNDAMENTALES
- Los siguientes cuatro requisitos se satisfacen con el paradigma orientado a objetos y podemos
tomarlos como características esenciales de este paradigma que separa un lenguaje orientado
a objetos de otro que no lo es.
a. Permiten la encapsulación y ocultar información.
b. Equipados con un mecanismo que, bajo ciertas condiciones, soporta la herencia de la
implementación de ciertas operaciones de otras, construcciones análogas.
c. Se enmarcan en una noción de compatibilidad definida en términos de las operaciones
admisibles para una determinada construcción.
d. Permiten la selección dinámica de operaciones en función del “tipo efectivo” de los
argumentos a los que se aplican.
- Por tanto, los tomaremos en el siguiente orden:
1. Encapsulación y Abstracción.
2. Subtipos, que es una relación de compatibilidad basada en la funcionalidad de un
objeto.
3. Herencia, que es la posibilidad de reutilizar la implementación de un método
previamente definido en otro objeto o para otra clase.
4. Selección de método dinámico.
- Estos son mecanismos distintos, cada uno exhibido por sí mismo por otros paradigmas. Como
se verá, es su interacción lo que hace que el paradigma orientado a objetos sea tan atractivo
para el diseño de software, incluso de grandes sistemas.
10.2.1 OBJETOS
- La construcción principal de los lenguajes orientados a objetos es (claramente) la de objeto.
Una cápsula que contiene tanto los datos como las operaciones que los manipulan y que
proporciona una interfaz para el mundo exterior a través de la cual se puede acceder al objeto.
- La idea metodológica que comparten los objetos con los ADT(Tipo de Dato Abstracto) es que
los datos deben “permanecer juntos” con las operaciones que los manipulan.
- Existe, entonces, una gran diferencia conceptual entre los objetos y los ADT. Aunque, en la
definición de un ADT, los datos y las operaciones están juntos, cuando declaramos una variable
de tipo abstracto, esa variable representa solo los datos que se pueden manipular mediante las
operaciones. Cada objeto, por otro lado, es un contenedor que (al menos conceptualmente)
encapsula tanto los datos como las operaciones. Un ejemplo es el objeto Counter c.
- La figura sugiere que podemos imaginar un objeto como un registro. Algunos campos
corresponden a datos (modificables), por ejemplo, el campo c; otros campos corresponden a
las operaciones que están permitidas para manipular los datos (es decir, reset, get e inc).
- Las operaciones se denominan métodos (o campos funcionales o funciones miembro) y
pueden acceder a los elementos de datos contenidos en el objeto, que se denominan variables
de instancia (o miembros o campos de datos). La ejecución de una operación se realiza
enviando al objeto un mensaje que consta del nombre del método a ejecutar, posiblemente
junto con los parámetros.
- Para representar tales invocaciones, la notación Java y C++: object.method(parameters)
- Un aspecto que no siempre está claro es que el objeto que recibe el mensaje es, al mismo
tiempo, también un parámetro (n implícito) del método invocado. Además, los miembros de
datos son accesibles utilizando el mismo mecanismo (si no están ocultos por cápsula,
obviamente). Si un objeto, o, tiene una variable de instancia, v, con o.v, solicitamos o por el
valor de v.
- Si bien la notación sigue siendo uniforme, observamos que acceder a los datos es un
mecanismo distinto al de invocar un método. La invocación de un método implica (o puede
implicar) la selección dinámica del método que se va a ejecutar, mientras que el acceso a un
elemento de datos es estático, aunque existen excepciones.
- El nivel de opacidad de la cápsula se define cuando se crea el objeto. Los datos pueden ser
más o menos directamente accesibles desde el exterior (por ejemplo, los datos podrían ser
directamente inaccesibles, con el objeto, en cambio, proporcionando "observadores"), algunas
operaciones pueden ser visibles en todas partes, otras visibles para algunos objetos, mientras
que, finalmente, otros son completamente privados, que solo están disponibles dentro del
propio objeto.
- Junto con los objetos, los lenguajes de este paradigma también ponen a disposición
mecanismos organizativos para los objetos. Estos mecanismos permiten agrupar objetos con
la misma estructura (o con estructura similar). Sin un principio organizativo, que haga explícita
la similitud de todos estos objetos, el programa perdería claridad expositiva y sería más difícil
de documentar. Además, desde el punto de vista de la implementación, está claro que sería
apropiado que el código de cada método se almacenara una sola vez, en lugar de ser copiado
dentro de cada objeto con estructura similar.
- Para resolver estos problemas, todo lenguaje orientado a objetos se basa en algunos principios
organizativos. Entre estos principios, el que es de lejos el más conocido es el de las clases,
aunque existe toda una familia de lenguajes orientados a objetos que carecen de clases.
10.2.2 CLASES
- Una clase es un modelo para un conjunto de objetos. Establece cuáles serán sus datos (tipo
junto con su visibilidad) y fija nombre, firma, visibilidad e implementación para cada uno de sus
métodos. En un lenguaje con clases, cada objeto pertenece (al menos) a una clase. Por
ejemplo, un objeto como el objeto Counter podría ser una instancia de la siguiente clase:
class Counter{
private int x;
public void reset() {
x = 0;
}
public void get() {
return x;
}
public void inc() {
x = x+1;
}
}
- Esta clase contiene la implementación de tres métodos los cuales son declarados public
(visibles para todos), mientras que la variable de instancia x es private(inaccesible desde fuera
del propio objeto pero accesible en la implementación de los métodos de esta clase).
- Los objetos se crean dinámicamente mediante una instancia de su clase. Se asigna un objeto
específico cuya estructura está determinada por su clase. Esta operación difiere de manera
fundamental de un lenguaje a otro y depende del estatus lingüístico de las clases. En C ++ y
Java las clases corresponden a un tipo y todos los objetos que instancian una clase A son
valores de tipo A. Tomando este punto de vista en Java, con su modelo basado en referencias
para variables, podemos crear un objeto Counter: Counter c = new Counter();
- El nombre c, de tipo Counter, está vinculado a un nuevo objeto que es una instancia de
Counter. Podemos crear un nuevo objeto, distinto al anterior pero con la misma estructura, y
vincularlo a otro nombre: Counter d = new Counter(); A la derecha del símbolo de asignación,
podemos ver dos operaciones distintas: la creación de los objetos (asignación del
almacenamiento necesario, new constructor) e inicialización (invocación del constructor de la
clase, representado por el nombre de la clase y paréntesis.
- Podemos suponer que el código de los métodos se almacena una sola vez en su clase y que
cuando un objeto necesita ejecutar un método específico, este código se buscará en la clase
de la que es una instancia. Para que esto suceda, el código del método debe acceder
correctamente a las variables de instancia que son diferentes para cada objeto y, por lo tanto,
no todas están almacenadas juntas en la clase sino dentro de la instancia.
- Los métodos de la clase Counter se refieren a sus variables de instancia con el nombre this.
Hemos visto que cuando un objeto recibe un mensaje solicitando la ejecución de un método, el
objeto en sí es un parámetro de método implícito. Cuando se hace una referencia a una
variable de instancia en el cuerpo de un método, hay una referencia implícita al objeto que
actualmente ejecuta el método.
- Desde el punto de vista lingüístico, el objeto actual generalmente se denota con un nombre
único, generalmente self o this. Por ejemplo, la definición del método inc podría escribirse de la
siguiente manera. Aquí, la referencia implícita al objeto actual se hace explícita:
public void inc() {
this.x = this.x+1;
}
- En el caso de una llamada a un método a través de this, el vínculo entre el método y el código
al que se refiere se determina dinámicamente. Este es un aspecto importante de este
paradigma.
- Algunos lenguajes permiten que algunas variables y métodos se asocien con clases (y no con
sus instancias). Se denominan variables y métodos de clase o estáticos. Las variables
estáticas se almacenan juntas en la clase y los métodos estáticos no pueden, obviamente,
referirse por this en su cuerpo porque no tienen un objeto actual.
----------------------------------------------------------------------------------------------------------------------------
Lenguajes basados en delegación
- También existen lenguajes orientados a objetos que carecen de clases. En estos lenguajes,
que se basan en la delegación (o en prototipos), el principio organizador no es la clase, sino la
delegación; es decir, el principio de que un objeto puede pedir a otro objeto (su padre) que
ejecute un método para él. Entre estos lenguajes, el progenitor es Self, que se desarrolló en
Xerox PARC y Stanford a finales de la década de 1980. Otros lenguajes basados en la
delegación son Dylan, que fue diseñado para distribuirse en una de las primeras versiones de
Netscape.
- Un objeto se crea a partir de la nada, o más a menudo, copiando (o clonando) otro objeto (que
se llama su prototipo). Los prototipos juegan el papel metodológico de clases. Proporcionan el
modelo para la estructuración y el funcionamiento de otros objetos, pero no son objetos
especiales. Lingüísticamente, son objetos ordinarios que, sin embargo, no se utilizan para
calcular, sino como modelo. Cuando se clona un prototipo, la copia mantiene una referencia al
prototipo como padre.
- Cuando un objeto recibe un mensaje pero no tiene un campo con ese nombre, pasa el mensaje
a su padre y el proceso se repite. Cuando el mensaje llega a un objeto que lo entiende, se
ejecuta el código asociado al método. El lenguaje asegura que la referencia a self se resuelva
correctamente como una referencia al objeto que originalmente recibió el mensaje. En general,
los datos del objeto se consideran iguales a los métodos.
- El mecanismo de delegación (y la unificación de código y datos) hace que la herencia sea
más poderosa. Es posible y natural crear objetos que comparten porciones de datos (en un
lenguaje basado en clases, esto solo es posible usando datos estáticos asociados con clases;
sin embargo, tal plantilla es demasiado “estática” para ser usada de manera rentable).
Además, un objeto puede cambiar la referencia a su padre de forma dinámica y, por lo tanto,
cambiar su propio comportamiento.
----------------------------------------------------------------------------------------------------------------------------
Objetos en el montón y en la pila. Todo lenguaje orientado a objetos permite la creación
dinámica de objetos. El lugar donde se crean los objetos depende del lenguaje.
La solución más común es asignar objetos en el montón y acceder a ellos usando referencias
(que serán punteros reales en los lenguajes que lo permitan, pero serán variables si el lenguaje
admite un modelo de referencia). Algunos lenguajes permiten la asignación y desasignación
explícita de objetos en el montón (C++ es uno); otros, probablemente la mayoría, optan por un
recolector de basura.
- La opción de crear objetos en la pila como variables ordinarias no es muy común. C ++ es uno
de esos lenguajes. Cuando se elabora una declaración de una variable de tipo clase, se realiza
la creación e inicialización de un objeto de ese tipo. El objeto se asigna como valor a la
variable. Las dos operaciones que contribuyen a la creación de un objeto (asignación e
inicialización) ocurren implícitamente en los casos en que no se llama explícitamente al
constructor. Algunos lenguajes, finalmente, permiten crear objetos en la pila y dejarlos sin
inicializar.
- En nuestro pseudolenguaje, asumimos que la creación de objetos ocurre explícitamente en el
montón y tiene un modelo de variable de referencia.
10.2.3 ENCAPSULACIÓN
- La encapsulación y la ocultación de información representan dos de los puntos cardinales de la
abstracción de datos. Todos los lenguajes permiten la definición de objetos ocultando alguna
parte de ellos (ya sean datos o métodos). En cada clase hay, por lo tanto, al menos dos vistas:
partes privadas y públicas. En la vista privada, todo es visible: es el nivel de acceso posible
dentro de la propia clase (por sus métodos). Sin embargo, a la vista del público, solo la
información exportada explícitamente es visible. Decimos que la información pública es la
interfaz de una clase, por analogía con los ADTs(Tipo de Dato Abstracto).
10.2.4 SUBTIPOS
- Se puede hacer que una clase se corresponda, de forma natural, con el conjunto de objetos
que son instancias de esa clase. Este conjunto de objetos es el tipo asociado con esa clase. En
los lenguajes tipados, esta relación es explícita. Una definición de clase también introduce la
definición de un tipo cuyos valores son las instancias de esa clase. En los lenguajes sin tipo
(como Smalltalk), la correspondencia es sólo convencional e implícita.
- Entre los tipos así obtenidos, se define una relación de compatibilidad en términos de las
operaciones posibles sobre valores del tipo. El tipo asociado a la clase T es un subtipo de S
cuando todo mensaje entendido (es decir, el que se puede recibir sin generar un error) de los
objetos de S también es entendido por los objetos de T. Si un objeto se representa como un
registro que contiene datos y funciones, la relación de subtipo corresponde al hecho de que T
es un tipo de registro que contiene todos los campos de S, así como posiblemente otros
campos. Más precisamente, teniendo en cuenta el hecho de que algunos campos de los dos
tipos podrían ser privados, podríamos decir que T es un subtipo de S cuando la interfaz de S es
un subconjunto de la interfaz de T (nótese la inversión: un subtipo es obtenido cuando tenemos
una interfaz más grande).
- Algunos lenguajes (como C++ y Java) utilizan una equivalencia de tipos basada en el nombre
que no se extiende adecuadamente a una relación de compatibilidad completamente
estructural. En tales lenguajes, no es, por tanto, la única propiedad estructural entre interfaces
que define la relación de subtipo, pero debe ser introducida explícitamente por el programador.
Este es el papel de la definición de subclases, o clases derivadas, que en nuestro
pseudolenguaje se denotarán usando la construcción neutral extending:
class NamedCounter extending Counter {
private String name;
public void set_name(String n) {
name = n;
}
public String get_name() {
return name;
}
}
- La clase NamedCounter es una subclase de Counter (que, a su vez, es una superclase de
NamedCounter), es decir, el tipo NamedCounter es un subtipo de Counter. Las instancias de
NamedCounter contienen todos los campos de Counter (incluso sus campos privados, pero son
inaccesibles en la subclase), además de tener nuevos campos introducidos por la definición. De
esta manera, se puede garantizar la compatibilidad estructural (una subclase se deriva
explícitamente de su superclase) pero esto se indica explícitamente en el programa.
Redefinición de un método
- En el ejemplo simple de NamedCounter, las subclases se limitan a extender la interfaz de la
superclase. Una característica fundamental del mecanismo de subclase es la capacidad de una
subclase para modificar la definición (la implementación) de un método presente en su
superclase. Este mecanismo se denomina anulación de método. Así, se puede definir una
subclase de Counter como:
class NewCounter extending Counter {
private int num_reset = 0;
public void reset() {
x = 0;
num_reset = num_reset + 1;
}
public int howmany_resets() {
return num_reset;
}
}
- La clase NewCounter amplía simultáneamente la interfaz de Counter con nuevos campos y
redefine el método de reinicio. Un método de reinicio enviado a una instancia de NewCounter
provocará la invocación de la nueva implementación.
Shadowing(Ocultamiento) Además de modificar la implementación de un método, una
subclase también puede redefinir una variable de instancia (o campo) definida en una
superclase. Este mecanismo se llama shadowing. Por razones de implementación, el
shadowing es significativamente diferente del overriding. Por el momento, simplemente
notamos que, en una subclase, una variable de instancia se puede redefinir con el mismo
nombre y el mismo tipo que en la superclase. Podríamos, por ejemplo, modificar los contadores
extendidos usando la siguiente subclase de NewCounter donde, por alguna (extraña) razón, el
valor inicial de num_reset se inicializa a 2 y se incrementa en 2 cada vez:
class EvenNewCounter extending NewCounter {
private int num_reset = 2;
public void reset() {
x = 0;
num_reset = num_reset + 2;
}
public int howmany_resets() {
return num_reset;
}
}
- Usando la noción habitual de visibilidad en lenguajes estructurados en bloques, cada referencia
a num_reset dentro de EvenNewCounter se refiere a la variable local (inicializada en 2) y no a
la declarada en NewCounter. Un mensaje reset enviado a una instancia de EvenNewCounter
provocará la invocación de la nueva implementación para reset que utilizará el nuevo campo
num_reset. Sin embargo, existe una gran diferencia, tanto a nivel semántico como a nivel de
implementación, entre overriding y shadowing.
Clases abstractas Para simplificar, hemos introducido el tipo asociado con una clase como el
conjunto de sus instancias. Sin embargo, muchos lenguajes permiten la definición de clases
que no pueden tener instancias porque la clase carece de la implementación de algún método.
En tales clases, solo existe el nombre y el tipo (es decir, la firma) de uno o más métodos; se
omite su implementación.
Las clases de este tipo se denominan clases abstractas. Las clases abstractas sirven para
proporcionar interfaces y pueden recibir implementaciones en subclases que redefinen (o,
define por primera vez) el método que carece de implementación. Las clases abstractas
también corresponden a tipos y el mecanismo que proporciona implementaciones para sus
métodos también genera subtipos.
La relación de subtipo En general, los lenguajes prohíben los ciclos en la relación de subtipo
entre clases, es decir, no podemos tener tanto A subtipo B como B subtipo A, a menos que A y
B coincidan. La relación de subtipo es, por tanto, un orden parcial. En muchos lenguajes, esta
relación tiene un elemento máximo: el tipo del cual todos los demás tipos definidos por clases
son subtipos. Denotaremos tal tipo con el nombre Object. Los mensajes que acepta una
instancia de Object son bastante limitados (es un objeto de máxima generalidad). En general,
encontramos un método de clonación que devuelve una copia del objeto, un método de
igualdad y algunos otros. Además, algunos de estos métodos pueden ser abstractos en Object
y, por lo tanto, deben ser redefinidos antes de poder usarse.
Se puede ver que no se garantiza que, dado un tipo A, exista un tipo B único que es el
supertipo inmediato de A. En el siguiente ejemplo:
abstract class A {
public int f();
}
abstract class B {
public int g();
}
class C extending A, B {
private x = 0;
public int f() {
return x;
}
public int g() {
return x+1;
}}
- El tipo C es un subtipo tanto de A como de B y no hay otro tipo incluido entre C y los otros dos.
La jerarquía de subtipos no es un árbol en general, sino sólo un gráfico orientado acíclico.
Constructores. Ya hemos visto que un objeto es una estructura compleja que incluye datos y
código, todos juntos. La creación de un objeto es, por tanto, también una operación de cierta
complejidad que consta de dos acciones distintas: primero, la asignación de la memoria
necesaria (en el montón o en la pila) y la inicialización adecuada de los datos del objeto. Esta
última acción la realiza el constructor de la clase. Es decir, por código asociado a la clase y
que el lenguaje garantiza que se ejecutará exactamente al mismo tiempo que se crea la
instancia. El mecanismo del constructor es de cierta complejidad porque los datos de un
objeto consisten no sólo en los declarados explícitamente en la clase cuya instancia se está
creando, sino también en los datos declarados en sus superclases. Además, a menudo se
permite más de un constructor para una clase.
Tenemos, por tanto, una serie de preguntas sobre constructores que se pueden resumir en las
siguientes:
- Selección de constructor. Cuando el lenguaje lo permite, hay más de un constructor para una
clase, entonces, ¿cómo se elige qué clase usar al crear un objeto específico?
En algunos lenguajes (por ejemplo, C++ y Java), el nombre del constructor es el mismo que el
de la clase. Múltiples constructores tienen todos el mismo nombre y deben distinguirse por su
tipo o el número de argumentos o ambos (por lo tanto, están sobrecargados, con resolución
estática). Dado que C++ permite la creación implícita de objetos en la pila, existen mecanismos
específicos para seleccionar el constructor apropiado a utilizar en cada caso. Otros lenguajes
permiten al programador elegir libremente los nombres de los constructores (pero los
constructores permanecen sintácticamente distintos de los métodos ordinarios) y requieren que
cada operación de creación esté siempre asociada con un constructor.
- Encadenamiento de constructores. ¿Cómo y cuándo se inicializa la parte del objeto que
pertenece a la superclase?
Algunos lenguajes se limitan a ejecutar el constructor de la clase cuya instancia están
construyendo. Si el programador tiene la intención de llamar a constructores de superclase,
debe hacerlo explícitamente. Otros lenguajes (entre ellos C++ y Java) garantizan, por otro lado,
que cuando se inicializa un objeto, el constructor de su superclase será invocado
(encadenamiento de constructor) antes de que el constructor específico de la subclase realice
cualquier otra operación.
Una vez más, hay muchas cuestiones que cada lenguaje tiene que resolver. Entre estos, los
dos más importantes son determinar qué constructor de superclase invocar y cómo determinar
sus argumentos.
10.2.5 HERENCIA
- Hemos visto que una subclase puede redefinir los métodos de su superclase. Pero, ¿qué pasa
cuando la subclase no los redefine? En tales casos, la subclase hereda métodos de la
superclase, en el sentido de que la implementación del método en la superclase está disponible
para la subclase. Por ejemplo, NewCounter hereda de Counter el elemento de datos x y los
métodos inc y get (pero no reset, que es redefinido).
- De manera más general, podemos caracterizar la herencia como un mecanismo que permite la
definición de nuevos objetos basados en la reutilización de los preexistentes. La herencia
permite la reutilización del código en un contexto ampliable. Al modificar la implementación de
un método, una clase automáticamente pone la modificación a disposición de todas sus
subclases, sin que se requiera intervención por parte del programador.
- Es importante comprender la diferencia entre la relación de herencia y la de subtipo. El
concepto de subtipo tiene que ver con la posibilidad de utilizar un objeto en otro contexto. Es
una relación entre las interfaces de dos clases. El concepto de herencia tiene que ver con la
posibilidad de reutilizar el código que manipula un objeto. Es una relación entre las
implementaciones de dos clases.
- Estamos tratando con dos mecanismos que son completamente independientes. Tanto C++
como Java tienen construcciones que pueden introducir simultáneamente ambas relaciones
entre las dos clases, pero esto no significa que los conceptos sean los mismos.
- En la literatura, a veces vemos la distinción entre herencia de implementación (herencia) y
herencia de interfaz (nuestra relación de subtipo).
Herencia y visibilidad Hemos visto que hay dos visiones de cada clase: la privada y la pública,
esta última compartida entre todos los clientes de la clase. Una subclase es un cliente particular
de la superclase. Utiliza los métodos de la superclase para ampliar la funcionalidad de la
superclase, pero a veces tiene que acceder a algunos datos no públicos. Por tanto, muchos
lenguajes introducen una tercera vista de una clase: una para las subclases. Tomando el
término de C++, podemos referirnos a él como la vista protegida de una clase.
- Si la subclase tiene acceso a algunos detalles de la implementación de la superclase, la
subclase depende mucho más de la superclase. Cada modificación de la superclase requerirá
una modificación de la subclase. Desde el punto de vista pragmático, esto es razonable sólo si
las dos clases están "próximas" entre sí, por ejemplo, si pertenecen al mismo paquete. Cuanto
más fuerte sea el emparejamiento de las dos clases, más difícil será modificar y mantener el
sistema resultante. Sin embargo, hacer coincidir las interfaces públicas y protegidas puede
resultar demasiado restrictivo. Al poder acceder a la representación de las estructuras de datos,
la subclase podrá implementar sus propias operaciones de una manera más eficiente.
Herencia única y múltiple En algunos lenguajes, una clase puede heredar de una única
superclase inmediata. La jerarquía de herencia es, por tanto, un árbol y decimos que el
lenguaje tiene herencia única (o simple). Otros lenguajes, por otro lado, permiten que una clase
herede métodos de más de una superclase inmediata; la jerarquía de herencia en tal caso es
un grafo orientado acíclico y el lenguaje tiene herencia múltiple.
- Hay solo unos pocos lenguajes que soportan herencia múltiple (entre los que se encuentran
C++ y Eiffel), porque esto presenta problemas que no tienen una solución elegante ni a nivel
conceptual ni de implementación. Los problemas fundamentales se relacionan con los
conflictos de nombres. Tenemos un conflicto de nombres cuando una clase C hereda
simultáneamente de A y B, que proporcionan implementación para métodos con la misma
firma. El siguiente es un ejemplo simple:
class A {
int x;
int f() {
return x;
}
}
class B {
int y;
int f() {
return y;
}
}
class C extending A, B {
int h() {
return f();
}
}
----------------------------------------------------------------------------------------------------------------------------
Herencia y subtipos en Java
- La relación de subtipo se introduce en Java utilizando la cláusula extends (para definir
subclases) o la cláusula implements cuando una clase se declara subtipo de una o más
interfaces (para Java, una interfaz es un tipo de clase abstracta incompleta en la que solo los
nombres y firmas de métodos se incluyen - no incluyen implementaciones). La relación de
herencia se introduce con la cláusula extends siempre que la subclase no redefina un método
y, por lo tanto, usa una implementación de la superclase. Cabe señalar que nunca hay herencia
de una interfaz porque la interfaz no tiene nada que heredar. El lenguaje restringe cada clase a
tener una sola superclase inmediata (es decir, una sola superclase puede ser nombrada con
una extends), pero permite que una sola clase (o interfaz) implemente más de una interfaz:
interface A {
int f();
}
interface B {
int g();
}
class C {
int x;
int h() {
return x+2;
}
}
class D extends C implements A, B {
int f() {
return x;
}
int g() {
return x+1;
}
}
- Por tanto, Java tiene herencia única. La jerarquía de herencia es un árbol organizado por
cláusulas extends. Además, la relación de herencia es, en Java, siempre una sub jerarquía de
la jerarquía de subtipos.
- En el ejemplo, cuando al mismo tiempo tenemos herencia de superclase e implementación de
alguna interfaz abstracta, a menudo se denomina herencia mixta (porque los nombres de los
métodos abstractos en la interfaz se mezclan con las implementaciones heredadas). En
muchos manuales (e incluso en la definición oficial de Java...), se dice que Java tiene una
herencia única para las clases, pero una herencia múltiple para las interfaces. Según nuestra
terminología, no existe una verdadera herencia en lo que respecta a las interfaces.
----------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------------
Herencia y subtipos en C++
- Los mecanismos de C++ que permiten la definición de la relación de herencia, y los
responsables de la subtipificación, no son independientes. La definición de clases derivadas (el
término de C++ para subclase) introduce la relación de herencia. También introduce una
relación de subtipo cuando la clase derivada declara su clase base (que es la superclase) como
public; cuando la clase base no es pública, la clase derivada hereda de la clase base pero no
existe una relación de subtipo.
class A {
public:
void f() {...}
…
}
class B : public A {
public:
void g() {...}
…
}
class C : A {
public:
void h() {...}
…
}
- Ambas clases, B y C, heredan de A, pero solo B es un subtipo de A.
- Dado que la relación de subtipo sigue a la de subclase, con las herramientas vistas hasta
ahora, no podríamos introducir un subtipo de interfaz, es decir, una clase derivada de una
clase base que no proporciona implementaciones sino que solo corrige una interfaz. Es con
este fin que C++ introduce el concepto de clase base abstracta, en la que algunos métodos no
necesitan tener una implementación. En tales casos, como ocurre con las interfaces Java,
podemos tener subtipos sin herencia.
----------------------------------------------------------------------------------------------------------------------------
- ¿Cuál de los dos métodos llamados f se hereda en C? Podemos solucionar este problema de
tres formas diferentes, ninguna de las cuales es totalmente satisfactoria:
1. Prohibir los choques sintácticos de nombres.
2. Exija que cualquier conflicto sea resuelto por el programador calificando
apropiadamente cada referencia a un nombre que esté en conflicto. Por ejemplo, el
cuerpo de h en la clase C, debe escribirse como B :: f () o como A :: f (), que es la
solución adoptada por C++.
3. Decidir sobre una convención para resolver el conflicto, por ejemplo, favoreciendo la
primera clase nombrada en la cláusula extending
- En lo que respecta a la solución explícita, se puede ver que el conflicto podría observarse no en
C sino en una de sus subclases (si no se llama a f dentro de C). Por tanto, el diseñador debe
conocer la jerarquía de clases con cierta precisión. Sin embargo, en casos como este, es
metodológicamente mejor redefinir f en C. Por ejemplo:
class C extending A, B {
int f() {
return A :: f();
}
int h() {
return this.f();
}
}
de modo que en las subclases de C, no hay conflictos de nombres.
- Los problemas más interesantes de la herencia múltiple están presentes en el llamado
problema del diamante. Este caso surge cuando una clase hereda de dos superclases, cada
una de las cuales hereda de la misma superclase única. Una situación simple de este tipo es la
siguiente (la jerarquía de herencia en forma de diamante):
class Top {
int w;
int f() {
return w;
}
}
class A extending Top {
int x;
int g() {
return w+x;
}
}
class B extending Top {
int y;
int f() {
return w+y;
}
int k() {
return y;
}
}
class Bottom extending A,B {
int z;
int h() {
return z;
}
}
- En este caso, también, tenemos el problema habitual del conflicto de nombres, pero es
fundamentalmente la cuestión de implementación la que es más relevante. Es decir, idear una
técnica que permita la selección correcta y eficiente del código para f cuando se invoca este
método en una instancia de la clase Bottom.
- En conclusión, la herencia múltiple es una herramienta muy flexible para la combinación de
abstracciones correspondientes a distintas funcionalidades. Algunas de las situaciones en las
que trabaja se expresan mejor en realidad utilizando relaciones de subtipo (“heredando” de
clases abstractas). No existen soluciones simples, inequívocas y elegantes a los problemas que
plantea la herencia múltiple. El balance de costo-beneficio entre la herencia única y múltiple es
ambiguo.
10.2.5 BÚSQUEDA DE MÉTODO DINÁMICO
- La búsqueda (o envío) de métodos dinámicos es el corazón del paradigma orientado a objetos.
En esto, las características que ya se han discutido se combinan para formar una nueva
síntesis. En particular, la búsqueda de métodos dinámicos permite que coexistan la
compatibilidad de subtipos y la abstracción. Esto es, en particular, algo que se consideró
problemático para los tipos de datos abstractos.
- Conceptualmente, el mecanismo es muy simple, ya hemos visto que un método definido para
un objeto se puede redefinir (anular-overridden) en objetos que pertenecen a subtipos del
objeto original. Por lo tanto, cuando se invoca un método, m, sobre un objeto, o, puede haber
muchas versiones de m posibles para o. La selección de qué implementación para m se
utilizará en una invocación es una función del tipo de objeto que recibe el mensaje.
o.m (parámetros);
Tenga en cuenta que lo relevante es el tipo de objeto que recibe el mensaje, no el tipo de
referencia (o nombre) a ese objeto (que en cambio es información estática).
- Vamos a dar un ejemplo en nuestro pseudo-lenguaje con las clases y contadores vistos en
secciones anteriores, y en ese contexto ejecutamos el siguiente fragmento:
NewCounter n = new NewCounter();
Counter c;
c = n;
c.reset();
class Counter {
private int x;
public void reset() {
x = 0;
}
public int get() {
return x;
}
public void inc() {
x = x+1;
}
}
class NewCounter extending Counter {
private int num_reset = 0;
public void reset() {
x = 0;
num_reset = num_reset + 1;
}
public int howmany_resets() {
return num_reset;
}
}
- El tipo (estático) del nombre c es Counter pero se refiere (dinámicamente) a una instancia de
NewCounter. Por lo tanto, será el método reset de NewCounter el que se invoque. Tanto
Counter como NewCounter se almacenan en una estructura de datos cuyo tipo es su supertipo:
Counter V[100];
Ahora aplicamos el método reset a cada uno:
for (int i=1; i<100; i=i+1)
V[i].reset();
- La búsqueda dinámica nos asegura que se invocará el método correcto en cualquier contador.
En general, un compilador no podrá decidir cuál será el tipo de objeto cuyo método será
invocado, de ahí la dinámica de este mecanismo.
- El lector debería haber notado una cierta analogía entre la sobrecarga y la búsqueda de
métodos dinámicos. En ambos casos el problema es el mismo: el de resolver una situación
ambigua en la que un solo nombre puede tener más de un significado.
- Sin embargo, bajo sobrecarga, la ambigüedad se resuelve estáticamente, según el tipo de
nombres involucrados. En la búsqueda de métodos, por otro lado, la solución del problema está
en tiempo de ejecución y hace uso del tipo dinámico del objeto y no de su nombre. Sin
embargo, no es un error pensar en la búsqueda de métodos como una operación de
sobrecarga en tiempo de ejecución en la que el objeto que recibe el mensaje se considera el
primer argumento (implícito) del método cuyo nombre debe resolverse.
- Es importante observar explícitamente que la búsqueda de métodos dinámicos funciona incluso
cuando un método en un objeto invoca un método en el mismo objeto, como sucede en el
siguiente fragmento:
class A {
int a = 0;
void f() {g();}
void g() {a=1;}
}
class B extending A {
int b = 0;
void g() {b=2;}
}
- Supongamos ahora que tenemos un objeto b que es una instancia de B y queremos invocar en
b el método f heredado por b de A. Ahora, f llama a g: ¿cuál de las dos implementaciones de g
se ejecutará?
Recuerde que el objeto que recibe el mensaje también es un parámetro importante del método.
La llamada de g en el cuerpo de f se puede escribir más explícitamente como this.g(), donde,
recuerde, this es una referencia al objeto actual. El objeto actual es b y, por lo tanto, el método
que se invocará es el de la clase B. Se puede ver que, de esta manera, una llamada a un
método como this.g () en el cuerpo de f puede referirse a ( implementaciones de) métodos que
aún no están escritos y que estarán disponibles sólo más adelante a través de la jerarquía de
clases. Este mecanismo, a través del cual el nombre this se vincula dinámicamente al objeto
actual, se denomina vinculación tardía de sí mismo (o de this).
- Notemos explícitamente que, a diferencia del overriding, shadowing es un mecanismo
completamente estático. Considere, por ejemplo, el código:
class A {
int a = 1;
int f() {return -a;}
}
class B extending A {
int a = 2;
int f() {return a;}
}
B obj_b = new B(); A obj_a = obj_b;
print(obj_b.f()); print(obj_a.f());
print(obj_b.a); print(obj_a.a);
----------------------------------------------------------------------------------------------------------------------------
Envío dinámico en C ++
- En lenguajes como Java o Smalltalk, la invocación de cada método se realiza mediante el envío
dinámico. C++ tiene como objetivos de diseño, eficiencia de ejecución y compatibilidad con C,
lo que significa que el uso de una construcción C en un programa C++ debe ser eficiente como
en C.
- En C++, tenemos, por lo tanto, un método de envío estático (análogo a la llamada de función),
así como una forma de envío dinámico que se realiza mediante funciones virtuales. Cuando se
define un método (que es una función miembro en terminología C++), es posible especificar si
es una función virtual o no. Solo se permite overriding de funciones virtuales. El envío
dinámico se realiza en funciones de miembros virtuales.
- Observemos, dicho sea de paso, que no está prohibido definir, en alguna clase, una función del
mismo nombre y firma que una función no virtual definida en la superclase. En tal caso, no
tenemos redefinición, sino sobrecarga, y esto puede ser resuelto estáticamente por el
compilador usando el tipo de nombre usado para referirse a ese objeto. En el ejemplo que
sigue declaramos una clase A y una subclase B:
class A {
public:
void f() {printf("A");}
virtual void g() {printf("A");}
}
class B : public A {
public:
void f() {printf("B");}
virtual void g() {printf("B");}
}
Si, ahora, tenemos un puntero a de tipo A*, que apunta a una instancia de B, la invocación de
la función a-> f () imprime A, mientras que la invocación de la función virtual a-> g () imprime B.
----------------------------------------------------------------------------------------------------------------------------
- donde print denota un método que genera el valor entero pasado como argumento. Las dos
primeras invocaciones de print producen el valor 2 ‘dos veces’, como debería ser obvio. La
tercera llamada también imprimirá el valor 2, dado que, como ya se vio anteriormente, el
método f se ha redefinido en la clase B. De hecho, el objeto creado (con new B()) es una
instancia de B y, por lo tanto, cada acceso a él, incluso aquellos a través de una variable de tipo
A (como en el caso de obj_a), utiliza el método redefinido (que obviamente es utilizando los
campos redefinidos en la clase B). La última aparición de print, en cambio, produce el valor 1.
En este caso, de hecho, dado que no estamos tratando con la invocación de un método sino
accediendo a un campo, es el tipo de referencia actual el que determina qué campo es para ser
considerado. Dado que obj_a es de tipo A, cuando escribimos obj_a.a, el campo en cuestión es
el de la clase A (inicializado a uno).
- Esta distinción entre overring y shadowing, se explica a nivel de implementación utilizando el
hecho de que el objeto de instancia de la clase B también contiene todos los campos de las
superclases de B, como aclararemos en la siguiente sección.
----------------------------------------------------------------------------------------------------------------------------
Lenguajes multimétodo
- En los lenguajes que hemos considerado hasta ahora, la invocación de un método tiene la
forma: o.m(parámetros);
- La búsqueda de métodos dinámicos usa el tipo de tiempo de ejecución del objeto que recibe el
método, mientras que usa los tipos estáticos de los parámetros. En algunos otros lenguajes
(por ejemplo, CLOS), esta asimetría se elimina. Los métodos ya no están asociados con clases,
sino que son nombres globales. Cada nombre de método está sobrecargado por una serie de
implementaciones (que en este caso se denominan métodos múltiples) y el objeto en el que
se invoca el método se pasa a cada método múltiple. Al invocar un método múltiple, el código
que debe ejecutarse se elige dinámicamente en función del tipo (dinámico) tanto del receptor
como de todos los argumentos.
- En estos lenguajes, hablamos de envío múltiple en lugar de envío único en lenguajes donde
existe un receptor privilegiado.
- En lenguajes con despacho múltiple, la analogía entre despacho (múltiple) dinámico y
“sobrecarga dinámica” es más evidente; El envío múltiple consiste en la resolución (en tiempo
de ejecución) de una sobrecarga sobre la base de información de tipo dinámico.
----------------------------------------------------------------------------------------------------------------------------
10.3 ASPECTOS DE IMPLEMENTACIÓN
- Cada lenguaje tiene su propio modelo de implementación que está optimizado para las
características específicas que proporciona el lenguaje. Sin embargo, podemos señalar algunas
líneas comunes que indican los principales problemas y sus soluciones.
Objetos. Un objeto de instancia de la clase A puede usarse como el valor de cualquier
superclase de A. En particular, es posible acceder no solo a las variables de instancia (campos
de datos) explícitamente definidas en A, sino también a las definidas en sus superclases.
Un objeto se puede representar como si fuera un registro, con tantos campos como variables
haya en la clase de la que es instancia, además de todas las que aparecen en sus superclases.
- En un caso de shadowing, o más bien cuando se usa un nombre de una variable de instancia
de la clase con el mismo tipo en una subclase, el objeto contiene campos adicionales, cada uno
correspondiente a una declaración diferente (a menudo, el nombre usado en la superclase no
es accesible en la subclase, si no se utilizan calificaciones particulares, por ejemplo super). La
representación también contiene un puntero al descriptor de la clase de la que es una instancia.
- En el caso de un lenguaje con un sistema de tipos estáticos, esta representación permite la
implementación simple de compatibilidad de subtipos (en el caso de herencia única). Para cada
variable de instancia, el desplazamiento desde el inicio del registro se registra estáticamente. Si
el acceso a un objeto de instancia de una clase se realiza utilizando una referencia (estática)
que tiene como tipo una de las superclases de ese objeto, la verificación de tipo estático
asegura que el acceso solo se puede realizar a un campo definido en la superclase, que está
asignado en la parte inicial del registro.
Clases y herencia. La representación de clases es la clave de la máquina abstracta de
orientación a objetos. La implementación más simple e intuitiva es la que representa la
jerarquía de clases mediante una lista vinculada. Cada elemento representa una clase y
contiene (apunta a) la implementación de los métodos que están definidos o redefinidos en esa
clase. Los elementos se vinculan mediante un puntero que apunta desde la subclase a la
superclase inmediata. Cuando se invoca un método m de un objeto o que es una instancia de
una clase C, el puntero almacenado en o se usa para acceder al descriptor de C y determinar si
C contiene una implementación de m. Si no es así, el puntero se establece en la superclase y
se repite el procedimiento. Por ejemplo: (una implementación simple de herencia)
Enlace tardío de sí mismo. Un método se ejecuta de forma similar a una función. Un registro
de activación para las variables locales del método, los parámetros y toda la otra información se
inserta en la pila. Sin embargo, a diferencia de una función, un método también debe acceder a
las variables de instancia del objeto en el que se llama; la identidad del objeto no se conoce en
el momento de la compilación.
- Sin embargo, la estructura de dicho objeto es conocida (depende de la clase) y, por lo tanto, el
desplazamiento de cada variable de instancia en la representación de un objeto es
estáticamente conocido (está sujeto a condiciones que dependen del lenguaje). Un puntero al
objeto que recibió el método también se pasa como parámetro cuando se invoca un método.
Durante la ejecución del cuerpo del método, este puntero es el método de this. Cuando se
invoca el método en el mismo objeto que lo invoca, this todavía se pasa como parámetro.
Durante la ejecución del método, cada acceso a una variable de instancia utiliza el
desplazamiento de este puntero (en lugar de tener un desplazamiento de un puntero en los
registros de activación, como es el caso de las variables locales de funciones).
- Desde un punto de vista lógico, podemos suponer que el puntero this se pasa a través del
registro de activación normalmente con todos los demás parámetros, pero esto causaría un
acceso doblemente indirecto para cada variable de instancia (uno para acceder al puntero
usando el puntero del registro de activación y uno para acceder a la variable usando this). De
manera más eficiente, la máquina abstracta mantendrá el valor actual de this en un registro.
10.3.1 HERENCIA ÚNICA
- Bajo la hipótesis de que el lenguaje tiene un sistema de tipos estáticos, la implementación que
utiliza listas enlazadas puede ser reemplazada por otra más eficiente, en la que la selección del
método requiere un tiempo constante (en lugar de un tiempo lineal en la profundidad de la
jerarquía de clases).
- Si los tipos son estáticos, el conjunto de métodos que cualquier objeto puede invocar se conoce
en tiempo de compilación. La lista de estos métodos se guarda en el descriptor de clase. La
lista contiene no sólo los métodos que se definen o redefinen explícitamente en la clase, sino
también todos los métodos heredados de sus superclases.
- Siguiendo la terminología de C ++, usaremos el término vtable (tabla de funciones virtuales)
para referirnos a esta estructura de datos. Cada definición de clase tiene su propia vtable y
todas las instancias de la misma clase comparten la misma vtable. Cuando se define una
subclase, B, de la clase A, la vtabla de B se genera copiando la de A, reemplazando todos los
métodos redefinidos en B y agregando los nuevos métodos que B define en la parte inferior.
- La propiedad fundamental de esta estructura de datos es que, si B es una subclase de A, la
vtable de B contiene una copia de la vtable de A como parte inicial; los métodos redefinidos se
modifican adecuadamente. De esta manera, la invocación del método cuesta solo dos accesos
indirectos, siendo el desplazamiento de cada método en la vtable conocido estáticamente. Se
puede observar que esta implementación obviamente tiene en cuenta el hecho de que es
posible acceder a un objeto con una referencia que pertenece estáticamente a una de sus
superclases.
- En el ejemplo de la figura anterior, cuando se invoca el método f, el compilador calcula un
desplazamiento que permanece igual si f se invoca en un objeto de la clase A o en un objeto de
la clase B. Diferentes implementaciones de f definidas en las dos clases están ubicados en el
mismo desplazamiento en las vtables de A y B.
- En general, si tenemos una referencia estática pa de la clase A a algún objeto de una de sus
subclases, podemos compilar una llamada al método f (que, asumimos que es el n-ésimo en la
vtable de A) de la siguiente manera (hemos asumido que la dirección de un método ocupa w
bytes):
R1 := pa // acceder al objeto
R2 := *(R1) // vtable
R3 := *(R2 + (n − 1) × w) // indirecto a f
call *(R3) // llamada a f
- Ignorando el soporte para herencia múltiple, este esquema de implementación es casi el mismo
que se usa en C++.
Downcasting. Si la vtable de una clase también contiene el nombre de la clase en sí, la
implementación que hemos discutido permite conversiones descendentes en la jerarquía de
clases. Esto se llama downcasting y es un mecanismo que se utiliza con bastante frecuencia.
Permite la especialización del tipo de objeto corriendo en dirección opuesta a la jerarquía de
subtipos. Un ejemplo canónico del uso de este mecanismo se encuentra en algunos métodos
de librerías que se definen como lo más generales posible. La clase Object podría usar la
siguiente firma para definir un método para copiar objetos: Object clone(){...}
- La semántica es que clone devuelve una copia exacta del objeto que lo invocó (misma clase,
mismos valores en sus campos, etc.). Si tenemos un objeto o de la clase A, una invocación de
o.clone() devolverá una instancia de A, pero el tipo estático determinado por el compilador para
esta expresión es Object. Para poder usar esta nueva copia de manera significativa, tenemos
que "forzarla" al tipo A, lo que podemos indicar con una conversión: A a = (A) o.clone();
- Con esto, queremos decir que la máquina abstracta verifica que el tipo dinámico del objeto sea
realmente A (caso contrario, tendríamos un error de tipo en tiempo de ejecución).
10.3.2 EL PROBLEMA DE LA CLASE BASE FRÁGIL
- En el caso de herencia simple, la implementación del mecanismo descrito anteriormente es
razonablemente eficiente, dado que toda la información importante, excepto el puntero this,
está determinada estáticamente. Sin embargo, se puede demostrar que esta estática es la
fuente de problemas en un contexto conocido como el problema de la clase base frágil.
- Un sistema orientado a objetos se organiza en términos de un gran número de clases utilizando
una elaborada jerarquía de herencia. A menudo, algunas de estas clases generales las
proporcionan las librerías. Las modificaciones a una clase ubicada muy alto en la jerarquía, se
pueden sentir en sus subclases. Algunas superclases pueden, por lo tanto, comportarse de una
manera "frágil" porque una modificación aparentemente inocua de la superclase puede causar
el mal funcionamiento de las subclases.
- No es posible identificar la fragilidad analizando solo la superclase. Es necesario considerar
toda la jerarquía de herencia, algo que a menudo es imposible porque quien escribió la
superclase generalmente no tiene acceso a todas las subclases. El problema puede surgir por
varias razones. Aquí, sólo distinguiremos dos casos principales:
1. El problema es arquitectónico. Algunas subclases explotan aspectos de la
implementación de la superclase que se han modificado en la nueva versión. Este es un
problema extremadamente importante para la ingeniería de software. Puede limitarse
reduciendo la relación de herencia a favor de la relación de subtipo.
2. El problema es de implementación. El mal funcionamiento de la subclase depende solo
de cómo la máquina abstracta (y el compilador) ha representado la herencia. Este caso
a veces se conoce como el problema de la interfaz binaria frágil.
- En este texto, el primer caso no es nuestro principal interés; el segundo lo es. Un caso típico
aparece en el contexto de una compilación separada donde se agrega un método a una
superclase, incluso si el nuevo método no interactúa con nada más. Partamos de lo siguiente:
class Upper {
int x;
int f() {..}
}
class Lower extending Upper {
int y;
int g() {return y + f();}
}
la superclase se modifica así:
class Upper {
int x;
int h() {return x;}
int f() {..}
}
- Si la herencia se implementa como se describe en la Sect. 10.3.2, la subclase Lower (que ya
está compilada y vinculada a Super) deja de funcionar correctamente porque se ha cambiado el
desplazamiento utilizado para acceder al método f, ya que se determina estáticamente. Para
solucionar este problema es necesario recompilar todas las subclases de las clases
modificadas, solución que no siempre es fácil de realizar.
- Para obviar esta pregunta, es necesario calcular dinámicamente el desplazamiento de métodos
en la vtable (y también el desplazamiento de variables de instancia en la representación de
objetos), de alguna manera razonablemente eficiente. La siguiente sección describe una
posible solución.
10.3.3 DESPACHO DE MÉTODO DINÁMICO EN LA JVM
- En esta sección presentamos una descripción simple de la técnica utilizada por la máquina
virtual Java (JVM), la máquina abstracta que interpreta el lenguaje intermedio (bytecode)
generado por el compilador estándar de Java. Por razones de espacio, no podemos entrar en
los detalles de la arquitectura de la JVM; observamos que es una máquina basada en pilas (no
tiene registros accesibles para el usuario y todos los operandos de operación se pasan a una
pila contenida en el registro de activación de la función que está siendo ejecutado
actualmente). La JVM tiene módulos para verificar la seguridad de la operación. Nos limitamos
a discutir las líneas generales de la implementación de la herencia y el envío de métodos.
- En Java, la compilación de cada clase produce un archivo que la máquina abstracta carga
dinámicamente cuando el programa en ejecución se refiere a la clase. Este archivo contiene
una tabla (constant pool-el grupo de constantes) para los símbolos utilizados en la propia
clase. El grupo de constantes contiene entradas, por ejemplo, variables, métodos públicos y
privados, métodos y campos de otras clases utilizadas en el cuerpo de métodos, nombres de
otras clases mencionadas en el cuerpo de la clase, etc. Con cada variable de instancia y
nombre de método se registra información como la clase donde se definen los nombres y su
tipo. Cada vez que el código fuente usa el nombre, el código intermedio de la JVM busca el
índice de ese nombre en el grupo constante (para ahorrar espacio, no busca el nombre en sí).
Cuando durante la ejecución se hace referencia a un nombre por primera vez (utilizando su
índice), se resuelve utilizando la información del pool de constantes, se cargan las clases
necesarias (por ejemplo aquella en la que se introducen los nombres), se realizan
comprobaciones de visibilidad (por ejemplo, el método invocado debe existir realmente en la
clase que se refiere a él, no es privado, etc.); También se realizan comprobaciones de tipo. En
este punto, las máquinas abstractas guardan un puntero a esta información para que la
próxima vez que se use el mismo nombre, no sea necesario realizar la resolución por segunda
vez.
- La representación de métodos en un descriptor de clase puede considerarse análoga a la
vtable. La tabla de una subclase comienza con una copia de su superclase, donde los métodos
redefinidos tienen sus nuevas definiciones en lugar de las antiguas. Sin embargo, las
compensaciones no se calculan estáticamente. Cuando se va a ejecutar un método, se pueden
distinguir cuatro casos principales (que corresponden a distintas instrucciones de código de
bytes):
1. El método es estático. Esto es para un método asociado a una clase y no a una
instancia. No se puede hacer ninguna referencia (explícita o implícita) a this.
2. El método debe distribuirse dinámicamente (un método "virtual").
3. El método debe distribuirse dinámicamente y se invoca mediante this (un método
"especial").
4. El método proviene de una interfaz (es decir, de una clase completamente abstracta que
no proporciona implementación, un método de "interfaz").
- Ignorando el último caso por el momento, los otros 3 casos se pueden distinguir principalmente
por los parámetros pasados al método. En el primer caso, solo se pasan los parámetros
nombrados en la llamada. En el segundo caso, se pasa una referencia al objeto en el que se
llama al método. En el caso 3, se pasa una referencia a this. Por lo tanto, supongamos que
podemos invocar el método m en el objeto o como: o.m(parámetro)
- Usando la referencia a o, la máquina abstracta accede al grupo constante de su clase y extrae
el nombre de m. En este punto, busca este nombre en la tabla vtable de la clase y determina su
desplazamiento. El desplazamiento se guarda en caso de un uso futuro del mismo método en
el mismo objeto.
- Sin embargo, el mismo método también podría invocarse en otros objetos, posiblemente
pertenecientes a subclases de la que pertenece o (por ejemplo, un bucle for en cuyo cuerpo se
repite una llamada a m en todos los objetos de una matriz). Para evitar calcular el
desplazamiento cada vez (que sería el mismo independientemente de la clase efectiva de la
que se llama al método m es una instancia), el intérprete de JVM utiliza una técnica de
"reescritura de código". Esto sustituye a la instrucción de búsqueda estándar generada por el
compilador por una forma optimizada que toma como argumento el desplazamiento del método
en la vtable. Para arreglar ideas (y simplificar mucho), al traducir la invocación de un método
virtual m, el compilador podría generar la instrucción de código de bytes: invokevirtual index
donde index es el índice del nombre m en el grupo constante. Durante la ejecución de esta
instrucción, el intérprete de JVM calcula el desplazamiento d de m en su vtable y reemplaza la
instrucción anterior con lo siguiente: invokevirtual_quick d np
- Aquí, np es el número de parámetros que espera m (y que se encontrarán en la pila que forma
parte de su registro de activación). Cada vez que el flujo de control vuelve a esta instrucción, se
invoca m sin sobrecarga.
- Queda el caso de la invocación de un método de interfaz (es decir, el caso 4 anterior). En este
caso, el desplazamiento podría no ser el mismo en 2 invocaciones del mismo método en
objetos de diferentes clases. Considere la siguiente situación (en la que usamos sintaxis Java
en lugar de pseudocódigo):
interface Interface {
void foo();
}
public class A implements Interface {
int x;
void foo(){...}
}
public class B implements Interface {
int y;
int fie(){...}
int foo(){...}
}
Tanto A como B implementan Interface y, por lo tanto, son sus subtipos. Sin embargo, el
desplazamiento de foo es diferente en las dos clases. Considere nuestro ciclo habitual:
Interface V[10];
…
for (int i = 0; i<10; i=i+1)
V[i].foo();
En tiempo de ejecución, no sabemos si los objetos contenidos en V serán instancias de A, B o
alguna otra clase que implemente Interface. El compilador podría haber generado la instrucción
JVM para el cuerpo del bucle: invokeinterface index, 0
(el 0 sirve para llenar un byte que se utilizará en la versión “rápida”). No sería correcto
reemplazar directamente esta instrucción por una versión “rápida” que devuelve solo el
desplazamiento porque cambiar el objeto también podría cambiar la clase de la que es una
instancia. Lo que podemos hacer es guardar el desplazamiento pero no podemos destruir el
nombre original del método; por lo tanto, reescribimos la instrucción como:
invokeinterface_quick nome_di_foo, d
Aquí name_of_foo es información adecuada con la que reconstruir el nombre y la firma de foo;
d es el desplazamiento determinado de antemano. Cuando se vuelve a ejecutar esta
instrucción, el intérprete accede a la vtable utilizando el desplazamiento d y comprueba que
existe un método con el nombre y la firma solicitados. En el caso positivo, se invoca; en el caso
negativo, busca el método por nombre en la vtable, como si fuera la primera vez que fue vista, y
determina un nuevo desplazamiento d’, luego escribe este valor en el código en lugar de d.
10.3.4 HERENCIA MÚLTIPLE
- La implementación de la herencia múltiple plantea problemas interesantes e impone una
sobrecarga no despreciable. Los problemas son de 2 órdenes: por un lado, está el problema de
identificar cómo es posible adaptar la técnica vtable para manejar las llamadas a métodos; por
otro (y el problema es más interesante y también tiene un impacto sobre el lenguaje) está la
necesidad de determinar qué hacer con los datos presentes en las superclases. Esto dará lugar
a 2 interpretaciones diferentes de la herencia múltiple, a las que podemos referirnos como
herencia replicada y compartida, abordaremos estos problemas sucesivamente.
Estructura Vtable. Consideraremos el ejemplo de la figura siguiente. Está claro que no es
posible organizar la representación de una instancia de C, ni una vtable para C de tal forma que
la parte inicial coincida con la estructura correspondiente tanto en A como en B. (Un ejemplo de
herencia múltiple)
class A {
int x;
int f() {
return x;
}
}
class B {
int y;
int g() {
return y;
}
int h() {
return 2 * y;
}
}
class C extending A,B {
int z;
int g() {
return x + y + z;
}
int k() {
return z;
}
}
- Para representar una instancia de C, podemos comenzar con los campos de A y luego agregar
los campos de B. Finalmente, necesitamos enumerar los campos específicos de
C.(Representación de objetos y vtables para herencia múltiple)
- Sabemos que, usando la relación de subtipo, podemos acceder a una instancia de C usando
una referencia estática a cualquiera de los tres tipos A, B y C. Hay 2 casos distintos
correspondientes a las 2 vistas diferentes de una instancia de C. Si se trata de un acceso con
referencia estática de tipo C o de tipo A, la técnica descrita para herencia simple funciona
perfectamente (excepto que las compensaciones estáticas de las variables de instancia reales
en C deben tener en cuenta que las variables estáticas pertenecientes a B están en el medio).
- Cuando, por el contrario, una instancia de C se ve como un objeto de B, es necesario tener en
cuenta el hecho de que las variables de B no están al inicio del registro sino a una distancia, d,
de su inicio que está determinado estáticamente. Cuando, por tanto, accedemos a una
instancia de C a través de una referencia con tipo estático B, es necesario sumar la constante,
d, a la referencia.
- Surgen problemas similares para la estructura de la vtable. Una tabla para C se divide en dos
partes distintas: la primera contiene los métodos de A (posiblemente como redefiniciones) y los
métodos que realmente pertenecen a C. Una segunda parte comprende los métodos de B,
posiblemente con sus redefiniciones. En la representación de un objeto de instancia de C, hay
dos punteros a vtable. Correspondiente a la "vista como A y C", tendrá punteros a la vtable con
métodos de A y C. Correspondiente a la "vista como B", tendrá punteros a la vtable con los
métodos de B (tenga en cuenta que esto es una vtable para la clase C; la clase B tiene otra
vtable, que se usa para sus propias instancias.
- En general, cada superclase de una clase bajo herencia múltiple tiene su propia parte especial
en las vtables de sus subclases). La invocación de un método que pertenece a C, o que fue
redefinido o heredado de A, sigue las mismas reglas que la herencia simple. Para invocar un
método heredado (o redefinido) de B, el compilador debe tener en cuenta que el puntero a la
vtable para este método no reside al principio del objeto sino que está desplazado por d
posiciones. En el ejemplo de la figura, para llamar al método h de una instancia de C visto
como un objeto de tipo B (del cual, por lo tanto, tenemos un nombre estático, pb): se agrega d a
la referencia pb; utilizando un acceso indirecto, se obtiene la dirección de inicio de la segunda
vtable (la de B), luego se usa el método de invocación de desplazamiento estático apropiado h.
Esta llamada necesita una operación adicional (la primera adición) además de la requerida para
la herencia simple.
- Sin embargo, hemos descuidado la vinculación para this. ¿Qué referencia de objeto actual
deberíamos pasar a los métodos de C? Si se trata de métodos en la primera vtable (al que se
accede mediante un this que apunta al inicio del objeto, es decir con la “vista A y B”), es
necesario pasar el valor actual de this. Sin embargo, esto sería incorrecto para los métodos en
la segunda vtable (al que se accede usando el puntero this más el desplazamiento d).
Debemos distinguir dos casos:
● El método se hereda de B (el caso de h en la figura). En tal caso, es necesario pasar la
vista del objeto a través del cual hemos encontrado la vtable.
● El método se redefine en C (el caso de g). En este caso, el método puede referirse a
variables de instancia de A, por lo que es necesario pasarle una vista de la superclase.
- La situación es incómoda porque la búsqueda de métodos dinámicos requiere que esta
corrección del valor de this se realice en tiempo de ejecución. La solución más sencilla es
almacenar esta corrección en la vtable, junto con el nombre del método. Cuando se invoca el
método, esta corrección se sumará al valor actual de this. En nuestro ejemplo, las correcciones
se muestran en la figura (Representación de objetos y vtables para herencia múltiple) junto al
nombre del método asociado. La corrección se agrega a la vista del objeto a través del cual se
encontró la vtable.
- En general, si tenemos una referencia, pa, a una vista de la clase C de algún objeto, podemos
compilar una llamada al método h (que podemos asumir que es el n-ésimo en la vtable de B) de
la siguiente manera (la dirección de un método y la corrección cada uno ocupa w bytes):
R1 := pa // vista A
R1 := R1 + d // vista B
R2 := *(R1) // vtable de B
R3 := *(R2 + (n − 1) × 2 × w) // dirección de h
R2 := *(R2 + (n − 1) × 2 × w + w) // corrección
this := R1 + R2
call *(R3) // llamada a h
- Tenemos tres instrucciones y un acceso indirecto además de la secuencia para llamar a un
método usando herencia única.
Herencia múltiple replicada. La subsección anterior se ha ocupado del caso en el que una
clase hereda de 2 superclases. Estas superclases, sin embargo, pueden heredar por sí mismas
de una superclase común, produciendo un diamante, como discutimos en la Sección. 10.2.5:
class Top {
int w;
int f() {
return w;
}
}
class A extending Top {
int x;
int g() {
return w+x;
}
}
class B extending Top {
int y;
int f() {
return w+y;
}
int k() {
return y;
}
}
class Bottom extending A,B {
int z;
int h() {
return z;
}
}
- Las instancias y la vtable para A tienen una parte inicial que es una copia de la estructura
correspondiente a Top. Lo mismo ocurre con las instancias y la vtable replicada para B. Bajo la
herencia múltiple replicada, Bottom se construye utilizando el enfoque que ya hemos discutido
y, por lo tanto, consta de dos copias de las variables de instancia y los métodos de Top, como
se muestra en la figura siguiente (Implementación de herencia múltiple replicada).
- La implementación no plantea otros problemas además de los ya discutidos. Los conflictos de
nombres deben resolverse explícitamente (en particular, no será posible invocar el método f de
Top en un objeto de clase Bottom, ni asignar una instancia de Bottom a una referencia estática
de tipo Top, porque no sabríamos cuál de los dos copias de Top elegir).
Herencia múltiple compartida. La herencia múltiple replicada no siempre es la solución
conceptual que un ingeniero de software tiene en mente al diseñar una situación de diamante.
Cuando la clase en la parte inferior del diagrama contiene una única copia de la clase en la
parte superior, hablamos de herencia múltiple replicada. En tal caso, tanto A como B poseen su
propia copia de Top, pero Bottom solo tiene una copia.
- C++ permite la herencia compartida usando clases de base virtual. Cuando una clase se
define como virtual, todas sus subclases siempre contienen una sola copia de la misma, incluso
si hay más de una ruta de herencia, como ocurre en el caso del diamante. Con este
mecanismo, una clase se declara virtual o no virtual de una vez por todas. Si tenemos una
clase no virtual y, más tarde, descubrimos que necesitamos herencia con el intercambio, no hay
nada más que hacer que reescribir la clase y recompilar todo el sistema. Peor aún, una clase
es virtual para todas sus subclases, incluso si en algunos casos deseamos que sea virtual para
algunos y no virtual (que se replica) para otros. En tales casos, es necesario definir dos copias
de la clase, una virtual y otra no virtual.
- Con la herencia con el uso compartido suele haber un problema de conflicto de nombres. El
problema se resuelve de forma arbitraria mediante diferentes lenguajes. En C++, por ejemplo,
en el caso de un método de clase base virtual que se redefine en una subclase, se requiere
que siempre haya una redefinición que domine a todas las demás en el sentido de que aparece
en una clase que es una subclase de todas las demás clases donde se define este método. En
nuestro ejemplo, para el método f, la redefinición dominante es la de la clase B. Es, por tanto, la
heredada por Bottom. Si ambas clases A y B redefinen f, las definiciones de estas clases serían
ilegales en C++ porque no habría una redefinición dominante. Otros lenguajes permiten que las
clases heredadas elijan por qué camino se heredará el método; alternativamente, permiten una
calificación completa de los nombres para elegir explícitamente el método deseado. Como es
habitual, cuando se trata de herencia múltiple, no existe una solución elegante que sea
claramente mejor que cualquier otra.
- Ahora llegamos a la implementación de vtable para herencia múltiple compartida.
- En la figura anterior, la implementación utilizada en C++ se muestra esquemáticamente. C++ es
un lenguaje en el que todas sus subclases comparten una clase virtual. Dado que Bottom
contiene una única copia de Top, ya no es posible representar subclase y superclase de forma
contigua. A cada clase del diamante le corresponde una vista específica del objeto. En
correspondencia con cada vista, hay un puntero a la vtable correspondiente y un puntero a la
parte compartida del objeto. El acceso a las variables de instancia y métodos de Bottom, A y B
es el mismo que en el caso de herencia múltiple sin compartir. Para acceder a las variables de
instancia, o para invocar un método de Top, se requiere un acceso indirecto preliminar
(supongamos que f es el método n de la vtable para Top y cada dirección ocupa w bytes):
R1 := pa // vista Bottom
R1 := *(R1 + w) // vista Top
R2 := *(R1) // vtable de Top
R3 := *(R2 + (n − 1) × 2 × w) // dirección de f
R2 := *(R2 + (n − 1) × 2 × w + w) // corrección
this := R1 + R2
call *(R3) // llamada a h
- El valor necesario para corregir el valor de this es la diferencia entre la vista de la clase en la
que se declara el método y la vista de la clase en la que se redefine.
- Los lenguajes que permiten situaciones más elaboradas (por ejemplo, una sola superclase se
replica en algunas clases y se comparte en otras) requieren técnicas más sofisticadas que no
se pueden explicar en este libro.

Más contenido relacionado

La actualidad más candente

Tarea 5
Tarea 5Tarea 5
Tarea 5ar qb
 
programacion orientada a objetos
programacion orientada a objetosprogramacion orientada a objetos
programacion orientada a objetosjent46
 
CUESTIONARIO SOBRE PROGRAMACIÓN RELACIONADA A OBJETOS
CUESTIONARIO SOBRE PROGRAMACIÓN RELACIONADA A OBJETOSCUESTIONARIO SOBRE PROGRAMACIÓN RELACIONADA A OBJETOS
CUESTIONARIO SOBRE PROGRAMACIÓN RELACIONADA A OBJETOSLuis Miguel Gutierrez
 
Unidad 2 poo_clases_y_objetos
Unidad 2 poo_clases_y_objetosUnidad 2 poo_clases_y_objetos
Unidad 2 poo_clases_y_objetosRulox Quiñones
 
Trabajo de diceño y realizacion
Trabajo de diceño y realizacionTrabajo de diceño y realizacion
Trabajo de diceño y realizacionLolyPila
 
Lenguaje de Programación Orientada a Objetos
Lenguaje  de  Programación  Orientada  a Objetos Lenguaje  de  Programación  Orientada  a Objetos
Lenguaje de Programación Orientada a Objetos Marielena Lujano
 
Poo Java
Poo JavaPoo Java
Poo Javaeccutpl
 
Conceptos de POO (Programacion Orientada a Objetos)
Conceptos de POO (Programacion Orientada a Objetos)Conceptos de POO (Programacion Orientada a Objetos)
Conceptos de POO (Programacion Orientada a Objetos)Josue Lara Reyes
 
Elementos de una clase
Elementos de una claseElementos de una clase
Elementos de una claseIsaias Toledo
 
Base de datos orientada a objetos
Base de datos orientada a objetosBase de datos orientada a objetos
Base de datos orientada a objetosXavis Riofrio
 
Elementos básicos de la programación orientada a objetos.
Elementos básicos de la programación orientada a objetos.Elementos básicos de la programación orientada a objetos.
Elementos básicos de la programación orientada a objetos.Whaleejaa Wha
 

La actualidad más candente (20)

Tarea 5
Tarea 5Tarea 5
Tarea 5
 
programacion orientada a objetos
programacion orientada a objetosprogramacion orientada a objetos
programacion orientada a objetos
 
CUESTIONARIO SOBRE PROGRAMACIÓN RELACIONADA A OBJETOS
CUESTIONARIO SOBRE PROGRAMACIÓN RELACIONADA A OBJETOSCUESTIONARIO SOBRE PROGRAMACIÓN RELACIONADA A OBJETOS
CUESTIONARIO SOBRE PROGRAMACIÓN RELACIONADA A OBJETOS
 
Unidad 2 poo_clases_y_objetos
Unidad 2 poo_clases_y_objetosUnidad 2 poo_clases_y_objetos
Unidad 2 poo_clases_y_objetos
 
Programación Orientada a Objetos
Programación Orientada a ObjetosProgramación Orientada a Objetos
Programación Orientada a Objetos
 
Trabajo de diceño y realizacion
Trabajo de diceño y realizacionTrabajo de diceño y realizacion
Trabajo de diceño y realizacion
 
Conceptos poo (presentación1)
Conceptos poo (presentación1)Conceptos poo (presentación1)
Conceptos poo (presentación1)
 
Poo presentacion
Poo presentacionPoo presentacion
Poo presentacion
 
Clase
ClaseClase
Clase
 
Lenguaje de Programación Orientada a Objetos
Lenguaje  de  Programación  Orientada  a Objetos Lenguaje  de  Programación  Orientada  a Objetos
Lenguaje de Programación Orientada a Objetos
 
Benita ppp unidad 1
Benita ppp unidad 1Benita ppp unidad 1
Benita ppp unidad 1
 
Poo Java
Poo JavaPoo Java
Poo Java
 
Act10byme
Act10bymeAct10byme
Act10byme
 
Conceptos de POO (Programacion Orientada a Objetos)
Conceptos de POO (Programacion Orientada a Objetos)Conceptos de POO (Programacion Orientada a Objetos)
Conceptos de POO (Programacion Orientada a Objetos)
 
Elementos de una clase
Elementos de una claseElementos de una clase
Elementos de una clase
 
Programacion Orientada a Objetos
Programacion Orientada a ObjetosProgramacion Orientada a Objetos
Programacion Orientada a Objetos
 
Base de datos orientada a objetos
Base de datos orientada a objetosBase de datos orientada a objetos
Base de datos orientada a objetos
 
Elementos De Una Clase
Elementos De Una ClaseElementos De Una Clase
Elementos De Una Clase
 
Conceptos basicos POO
Conceptos basicos POOConceptos basicos POO
Conceptos basicos POO
 
Elementos básicos de la programación orientada a objetos.
Elementos básicos de la programación orientada a objetos.Elementos básicos de la programación orientada a objetos.
Elementos básicos de la programación orientada a objetos.
 

Similar a Objetos, clases y paradigma orientado a objetos

Programacion orientada a objetos
Programacion orientada a objetosProgramacion orientada a objetos
Programacion orientada a objetosAngel Laverde ID
 
Daniel espinosa garzon
Daniel espinosa garzonDaniel espinosa garzon
Daniel espinosa garzonorus004
 
Conceptualizacion lenguajes de programacion
Conceptualizacion lenguajes de programacionConceptualizacion lenguajes de programacion
Conceptualizacion lenguajes de programacionorus004
 
Introduccion al paradigma de la programacion orientado a objetos original
Introduccion al paradigma de la programacion orientado a objetos originalIntroduccion al paradigma de la programacion orientado a objetos original
Introduccion al paradigma de la programacion orientado a objetos originalJose Angel Rodriguez
 
Trabajo investigativo sobre la programación orientada a objetos y java
Trabajo investigativo sobre la programación orientada a objetos y javaTrabajo investigativo sobre la programación orientada a objetos y java
Trabajo investigativo sobre la programación orientada a objetos y javaJulio César Rojas Maza
 
Metodología orientada a objetos
Metodología orientada a objetosMetodología orientada a objetos
Metodología orientada a objetosalcrrsc
 
Programación orientado a objetos miranda burgos, armas martinez
Programación orientado a objetos miranda burgos, armas martinezProgramación orientado a objetos miranda burgos, armas martinez
Programación orientado a objetos miranda burgos, armas martinezErnesto Miranda
 
Semanas01y02
Semanas01y02Semanas01y02
Semanas01y02luisortiz
 
Termino de programacion
Termino de programacionTermino de programacion
Termino de programacionJENNY GUAYLLA
 
[ES] Programación orientada a objeto con java
[ES] Programación orientada a objeto con java[ES] Programación orientada a objeto con java
[ES] Programación orientada a objeto con javaEudris Cabrera
 
Diseño de Sistemas
Diseño de SistemasDiseño de Sistemas
Diseño de Sistemasjorgecaruci
 
Base de datos orientada a objetos vs base obje to relacion
Base de datos orientada a objetos vs base obje to relacionBase de datos orientada a objetos vs base obje to relacion
Base de datos orientada a objetos vs base obje to relacionAlfonso Triana
 
modularidad de programación 2da parte (3) (1).pptx
modularidad de programación 2da parte (3) (1).pptxmodularidad de programación 2da parte (3) (1).pptx
modularidad de programación 2da parte (3) (1).pptxjavierccallo
 

Similar a Objetos, clases y paradigma orientado a objetos (20)

Java
JavaJava
Java
 
Prog.orientada a objeto
Prog.orientada a objetoProg.orientada a objeto
Prog.orientada a objeto
 
metodos de clases
metodos de clasesmetodos de clases
metodos de clases
 
Programacion orientada a objetos
Programacion orientada a objetosProgramacion orientada a objetos
Programacion orientada a objetos
 
Daniel espinosa garzon
Daniel espinosa garzonDaniel espinosa garzon
Daniel espinosa garzon
 
Conceptualizacion lenguajes de programacion
Conceptualizacion lenguajes de programacionConceptualizacion lenguajes de programacion
Conceptualizacion lenguajes de programacion
 
Introduccion al paradigma de la programacion orientado a objetos original
Introduccion al paradigma de la programacion orientado a objetos originalIntroduccion al paradigma de la programacion orientado a objetos original
Introduccion al paradigma de la programacion orientado a objetos original
 
deber 4
deber 4deber 4
deber 4
 
Trabajo investigativo sobre la programación orientada a objetos y java
Trabajo investigativo sobre la programación orientada a objetos y javaTrabajo investigativo sobre la programación orientada a objetos y java
Trabajo investigativo sobre la programación orientada a objetos y java
 
Metodología orientada a objetos
Metodología orientada a objetosMetodología orientada a objetos
Metodología orientada a objetos
 
Programación orientado a objetos miranda burgos, armas martinez
Programación orientado a objetos miranda burgos, armas martinezProgramación orientado a objetos miranda burgos, armas martinez
Programación orientado a objetos miranda burgos, armas martinez
 
Semanas01y02
Semanas01y02Semanas01y02
Semanas01y02
 
Semanas01y02
Semanas01y02Semanas01y02
Semanas01y02
 
Termino de programacion
Termino de programacionTermino de programacion
Termino de programacion
 
[ES] Programación orientada a objeto con java
[ES] Programación orientada a objeto con java[ES] Programación orientada a objeto con java
[ES] Programación orientada a objeto con java
 
Programacion orientada a objetos
Programacion orientada a objetosProgramacion orientada a objetos
Programacion orientada a objetos
 
Base de Datos Orientada a Objetos
Base de Datos Orientada a ObjetosBase de Datos Orientada a Objetos
Base de Datos Orientada a Objetos
 
Diseño de Sistemas
Diseño de SistemasDiseño de Sistemas
Diseño de Sistemas
 
Base de datos orientada a objetos vs base obje to relacion
Base de datos orientada a objetos vs base obje to relacionBase de datos orientada a objetos vs base obje to relacion
Base de datos orientada a objetos vs base obje to relacion
 
modularidad de programación 2da parte (3) (1).pptx
modularidad de programación 2da parte (3) (1).pptxmodularidad de programación 2da parte (3) (1).pptx
modularidad de programación 2da parte (3) (1).pptx
 

Último

Una estrategia de seguridad en la nube alineada al NIST
Una estrategia de seguridad en la nube alineada al NISTUna estrategia de seguridad en la nube alineada al NIST
Una estrategia de seguridad en la nube alineada al NISTFundación YOD YOD
 
Elaboración de la estructura del ADN y ARN en papel.pdf
Elaboración de la estructura del ADN y ARN en papel.pdfElaboración de la estructura del ADN y ARN en papel.pdf
Elaboración de la estructura del ADN y ARN en papel.pdfKEVINYOICIAQUINOSORI
 
Tinciones simples en el laboratorio de microbiología
Tinciones simples en el laboratorio de microbiologíaTinciones simples en el laboratorio de microbiología
Tinciones simples en el laboratorio de microbiologíaAlexanderimanolLencr
 
Seleccion de Fusibles en media tension fusibles
Seleccion de Fusibles en media tension fusiblesSeleccion de Fusibles en media tension fusibles
Seleccion de Fusibles en media tension fusiblesSaulSantiago25
 
Manual_Identificación_Geoformas_140627.pdf
Manual_Identificación_Geoformas_140627.pdfManual_Identificación_Geoformas_140627.pdf
Manual_Identificación_Geoformas_140627.pdfedsonzav8
 
TAREA 8 CORREDOR INTEROCEÁNICO DEL PAÍS.pdf
TAREA 8 CORREDOR INTEROCEÁNICO DEL PAÍS.pdfTAREA 8 CORREDOR INTEROCEÁNICO DEL PAÍS.pdf
TAREA 8 CORREDOR INTEROCEÁNICO DEL PAÍS.pdfAntonioGonzalezIzqui
 
Comite Operativo Ciberseguridad 012020.pptx
Comite Operativo Ciberseguridad 012020.pptxComite Operativo Ciberseguridad 012020.pptx
Comite Operativo Ciberseguridad 012020.pptxClaudiaPerez86192
 
CHARLA DE INDUCCIÓN SEGURIDAD Y SALUD OCUPACIONAL
CHARLA DE INDUCCIÓN SEGURIDAD Y SALUD OCUPACIONALCHARLA DE INDUCCIÓN SEGURIDAD Y SALUD OCUPACIONAL
CHARLA DE INDUCCIÓN SEGURIDAD Y SALUD OCUPACIONALKATHIAMILAGRITOSSANC
 
Presentación electricidad y magnetismo.pptx
Presentación electricidad y magnetismo.pptxPresentación electricidad y magnetismo.pptx
Presentación electricidad y magnetismo.pptxYajairaMartinez30
 
Procesos-de-la-Industria-Alimentaria-Envasado-en-la-Produccion-de-Alimentos.pptx
Procesos-de-la-Industria-Alimentaria-Envasado-en-la-Produccion-de-Alimentos.pptxProcesos-de-la-Industria-Alimentaria-Envasado-en-la-Produccion-de-Alimentos.pptx
Procesos-de-la-Industria-Alimentaria-Envasado-en-la-Produccion-de-Alimentos.pptxJuanPablo452634
 
Principales aportes de la carrera de William Edwards Deming
Principales aportes de la carrera de William Edwards DemingPrincipales aportes de la carrera de William Edwards Deming
Principales aportes de la carrera de William Edwards DemingKevinCabrera96
 
Condensadores de la rama de electricidad y magnetismo
Condensadores de la rama de electricidad y magnetismoCondensadores de la rama de electricidad y magnetismo
Condensadores de la rama de electricidad y magnetismosaultorressep
 
Sesión 02 TIPOS DE VALORIZACIONES CURSO Cersa
Sesión 02 TIPOS DE VALORIZACIONES CURSO CersaSesión 02 TIPOS DE VALORIZACIONES CURSO Cersa
Sesión 02 TIPOS DE VALORIZACIONES CURSO CersaXimenaFallaLecca1
 
ARBOL DE CAUSAS ANA INVESTIGACION DE ACC.ppt
ARBOL DE CAUSAS ANA INVESTIGACION DE ACC.pptARBOL DE CAUSAS ANA INVESTIGACION DE ACC.ppt
ARBOL DE CAUSAS ANA INVESTIGACION DE ACC.pptMarianoSanchez70
 
Curso intensivo de soldadura electrónica en pdf
Curso intensivo de soldadura electrónica  en pdfCurso intensivo de soldadura electrónica  en pdf
Curso intensivo de soldadura electrónica en pdfFernandaGarca788912
 
presentacion medidas de seguridad riesgo eléctrico
presentacion medidas de seguridad riesgo eléctricopresentacion medidas de seguridad riesgo eléctrico
presentacion medidas de seguridad riesgo eléctricoalexcala5
 
INTEGRALES TRIPLES CLASE TEORICA Y PRÁCTICA
INTEGRALES TRIPLES CLASE TEORICA Y PRÁCTICAINTEGRALES TRIPLES CLASE TEORICA Y PRÁCTICA
INTEGRALES TRIPLES CLASE TEORICA Y PRÁCTICAJOSLUISCALLATAENRIQU
 
07 MECANIZADO DE CONTORNOS para torno cnc universidad catolica
07 MECANIZADO DE CONTORNOS para torno cnc universidad catolica07 MECANIZADO DE CONTORNOS para torno cnc universidad catolica
07 MECANIZADO DE CONTORNOS para torno cnc universidad catolicalf1231
 
Magnetismo y electromagnetismo principios
Magnetismo y electromagnetismo principiosMagnetismo y electromagnetismo principios
Magnetismo y electromagnetismo principiosMarceloQuisbert6
 

Último (20)

Una estrategia de seguridad en la nube alineada al NIST
Una estrategia de seguridad en la nube alineada al NISTUna estrategia de seguridad en la nube alineada al NIST
Una estrategia de seguridad en la nube alineada al NIST
 
Elaboración de la estructura del ADN y ARN en papel.pdf
Elaboración de la estructura del ADN y ARN en papel.pdfElaboración de la estructura del ADN y ARN en papel.pdf
Elaboración de la estructura del ADN y ARN en papel.pdf
 
Tinciones simples en el laboratorio de microbiología
Tinciones simples en el laboratorio de microbiologíaTinciones simples en el laboratorio de microbiología
Tinciones simples en el laboratorio de microbiología
 
Seleccion de Fusibles en media tension fusibles
Seleccion de Fusibles en media tension fusiblesSeleccion de Fusibles en media tension fusibles
Seleccion de Fusibles en media tension fusibles
 
Manual_Identificación_Geoformas_140627.pdf
Manual_Identificación_Geoformas_140627.pdfManual_Identificación_Geoformas_140627.pdf
Manual_Identificación_Geoformas_140627.pdf
 
TAREA 8 CORREDOR INTEROCEÁNICO DEL PAÍS.pdf
TAREA 8 CORREDOR INTEROCEÁNICO DEL PAÍS.pdfTAREA 8 CORREDOR INTEROCEÁNICO DEL PAÍS.pdf
TAREA 8 CORREDOR INTEROCEÁNICO DEL PAÍS.pdf
 
Comite Operativo Ciberseguridad 012020.pptx
Comite Operativo Ciberseguridad 012020.pptxComite Operativo Ciberseguridad 012020.pptx
Comite Operativo Ciberseguridad 012020.pptx
 
CHARLA DE INDUCCIÓN SEGURIDAD Y SALUD OCUPACIONAL
CHARLA DE INDUCCIÓN SEGURIDAD Y SALUD OCUPACIONALCHARLA DE INDUCCIÓN SEGURIDAD Y SALUD OCUPACIONAL
CHARLA DE INDUCCIÓN SEGURIDAD Y SALUD OCUPACIONAL
 
Presentación electricidad y magnetismo.pptx
Presentación electricidad y magnetismo.pptxPresentación electricidad y magnetismo.pptx
Presentación electricidad y magnetismo.pptx
 
Procesos-de-la-Industria-Alimentaria-Envasado-en-la-Produccion-de-Alimentos.pptx
Procesos-de-la-Industria-Alimentaria-Envasado-en-la-Produccion-de-Alimentos.pptxProcesos-de-la-Industria-Alimentaria-Envasado-en-la-Produccion-de-Alimentos.pptx
Procesos-de-la-Industria-Alimentaria-Envasado-en-la-Produccion-de-Alimentos.pptx
 
Principales aportes de la carrera de William Edwards Deming
Principales aportes de la carrera de William Edwards DemingPrincipales aportes de la carrera de William Edwards Deming
Principales aportes de la carrera de William Edwards Deming
 
Condensadores de la rama de electricidad y magnetismo
Condensadores de la rama de electricidad y magnetismoCondensadores de la rama de electricidad y magnetismo
Condensadores de la rama de electricidad y magnetismo
 
Sesión 02 TIPOS DE VALORIZACIONES CURSO Cersa
Sesión 02 TIPOS DE VALORIZACIONES CURSO CersaSesión 02 TIPOS DE VALORIZACIONES CURSO Cersa
Sesión 02 TIPOS DE VALORIZACIONES CURSO Cersa
 
ARBOL DE CAUSAS ANA INVESTIGACION DE ACC.ppt
ARBOL DE CAUSAS ANA INVESTIGACION DE ACC.pptARBOL DE CAUSAS ANA INVESTIGACION DE ACC.ppt
ARBOL DE CAUSAS ANA INVESTIGACION DE ACC.ppt
 
Curso intensivo de soldadura electrónica en pdf
Curso intensivo de soldadura electrónica  en pdfCurso intensivo de soldadura electrónica  en pdf
Curso intensivo de soldadura electrónica en pdf
 
presentacion medidas de seguridad riesgo eléctrico
presentacion medidas de seguridad riesgo eléctricopresentacion medidas de seguridad riesgo eléctrico
presentacion medidas de seguridad riesgo eléctrico
 
INTEGRALES TRIPLES CLASE TEORICA Y PRÁCTICA
INTEGRALES TRIPLES CLASE TEORICA Y PRÁCTICAINTEGRALES TRIPLES CLASE TEORICA Y PRÁCTICA
INTEGRALES TRIPLES CLASE TEORICA Y PRÁCTICA
 
07 MECANIZADO DE CONTORNOS para torno cnc universidad catolica
07 MECANIZADO DE CONTORNOS para torno cnc universidad catolica07 MECANIZADO DE CONTORNOS para torno cnc universidad catolica
07 MECANIZADO DE CONTORNOS para torno cnc universidad catolica
 
Magnetismo y electromagnetismo principios
Magnetismo y electromagnetismo principiosMagnetismo y electromagnetismo principios
Magnetismo y electromagnetismo principios
 
VALORIZACION Y LIQUIDACION MIGUEL SALINAS.pdf
VALORIZACION Y LIQUIDACION MIGUEL SALINAS.pdfVALORIZACION Y LIQUIDACION MIGUEL SALINAS.pdf
VALORIZACION Y LIQUIDACION MIGUEL SALINAS.pdf
 

Objetos, clases y paradigma orientado a objetos

  • 1. Resumen en Español: CAPÍTULO 10 “El paradigma Orientado a Objetos” LIBRO: Programming Languajes: Principles and Paradigms by Maurizio Gabrielli and Simone Martini AUTOR: Cristian Arciniegas
  • 2. Chapter 10.- The Object-Oriented Paradigm (10.2, 10.3) 10.2 CONCEPTOS FUNDAMENTALES - Los siguientes cuatro requisitos se satisfacen con el paradigma orientado a objetos y podemos tomarlos como características esenciales de este paradigma que separa un lenguaje orientado a objetos de otro que no lo es. a. Permiten la encapsulación y ocultar información. b. Equipados con un mecanismo que, bajo ciertas condiciones, soporta la herencia de la implementación de ciertas operaciones de otras, construcciones análogas. c. Se enmarcan en una noción de compatibilidad definida en términos de las operaciones admisibles para una determinada construcción. d. Permiten la selección dinámica de operaciones en función del “tipo efectivo” de los argumentos a los que se aplican. - Por tanto, los tomaremos en el siguiente orden: 1. Encapsulación y Abstracción. 2. Subtipos, que es una relación de compatibilidad basada en la funcionalidad de un objeto. 3. Herencia, que es la posibilidad de reutilizar la implementación de un método previamente definido en otro objeto o para otra clase. 4. Selección de método dinámico. - Estos son mecanismos distintos, cada uno exhibido por sí mismo por otros paradigmas. Como se verá, es su interacción lo que hace que el paradigma orientado a objetos sea tan atractivo para el diseño de software, incluso de grandes sistemas. 10.2.1 OBJETOS - La construcción principal de los lenguajes orientados a objetos es (claramente) la de objeto. Una cápsula que contiene tanto los datos como las operaciones que los manipulan y que proporciona una interfaz para el mundo exterior a través de la cual se puede acceder al objeto. - La idea metodológica que comparten los objetos con los ADT(Tipo de Dato Abstracto) es que los datos deben “permanecer juntos” con las operaciones que los manipulan. - Existe, entonces, una gran diferencia conceptual entre los objetos y los ADT. Aunque, en la definición de un ADT, los datos y las operaciones están juntos, cuando declaramos una variable de tipo abstracto, esa variable representa solo los datos que se pueden manipular mediante las operaciones. Cada objeto, por otro lado, es un contenedor que (al menos conceptualmente) encapsula tanto los datos como las operaciones. Un ejemplo es el objeto Counter c. - La figura sugiere que podemos imaginar un objeto como un registro. Algunos campos corresponden a datos (modificables), por ejemplo, el campo c; otros campos corresponden a las operaciones que están permitidas para manipular los datos (es decir, reset, get e inc). - Las operaciones se denominan métodos (o campos funcionales o funciones miembro) y pueden acceder a los elementos de datos contenidos en el objeto, que se denominan variables de instancia (o miembros o campos de datos). La ejecución de una operación se realiza enviando al objeto un mensaje que consta del nombre del método a ejecutar, posiblemente junto con los parámetros.
  • 3. - Para representar tales invocaciones, la notación Java y C++: object.method(parameters) - Un aspecto que no siempre está claro es que el objeto que recibe el mensaje es, al mismo tiempo, también un parámetro (n implícito) del método invocado. Además, los miembros de datos son accesibles utilizando el mismo mecanismo (si no están ocultos por cápsula, obviamente). Si un objeto, o, tiene una variable de instancia, v, con o.v, solicitamos o por el valor de v. - Si bien la notación sigue siendo uniforme, observamos que acceder a los datos es un mecanismo distinto al de invocar un método. La invocación de un método implica (o puede implicar) la selección dinámica del método que se va a ejecutar, mientras que el acceso a un elemento de datos es estático, aunque existen excepciones. - El nivel de opacidad de la cápsula se define cuando se crea el objeto. Los datos pueden ser más o menos directamente accesibles desde el exterior (por ejemplo, los datos podrían ser directamente inaccesibles, con el objeto, en cambio, proporcionando "observadores"), algunas operaciones pueden ser visibles en todas partes, otras visibles para algunos objetos, mientras que, finalmente, otros son completamente privados, que solo están disponibles dentro del propio objeto. - Junto con los objetos, los lenguajes de este paradigma también ponen a disposición mecanismos organizativos para los objetos. Estos mecanismos permiten agrupar objetos con la misma estructura (o con estructura similar). Sin un principio organizativo, que haga explícita la similitud de todos estos objetos, el programa perdería claridad expositiva y sería más difícil de documentar. Además, desde el punto de vista de la implementación, está claro que sería apropiado que el código de cada método se almacenara una sola vez, en lugar de ser copiado dentro de cada objeto con estructura similar. - Para resolver estos problemas, todo lenguaje orientado a objetos se basa en algunos principios organizativos. Entre estos principios, el que es de lejos el más conocido es el de las clases, aunque existe toda una familia de lenguajes orientados a objetos que carecen de clases. 10.2.2 CLASES - Una clase es un modelo para un conjunto de objetos. Establece cuáles serán sus datos (tipo junto con su visibilidad) y fija nombre, firma, visibilidad e implementación para cada uno de sus métodos. En un lenguaje con clases, cada objeto pertenece (al menos) a una clase. Por ejemplo, un objeto como el objeto Counter podría ser una instancia de la siguiente clase: class Counter{ private int x; public void reset() { x = 0; } public void get() { return x; } public void inc() { x = x+1; } } - Esta clase contiene la implementación de tres métodos los cuales son declarados public (visibles para todos), mientras que la variable de instancia x es private(inaccesible desde fuera del propio objeto pero accesible en la implementación de los métodos de esta clase). - Los objetos se crean dinámicamente mediante una instancia de su clase. Se asigna un objeto específico cuya estructura está determinada por su clase. Esta operación difiere de manera fundamental de un lenguaje a otro y depende del estatus lingüístico de las clases. En C ++ y Java las clases corresponden a un tipo y todos los objetos que instancian una clase A son
  • 4. valores de tipo A. Tomando este punto de vista en Java, con su modelo basado en referencias para variables, podemos crear un objeto Counter: Counter c = new Counter(); - El nombre c, de tipo Counter, está vinculado a un nuevo objeto que es una instancia de Counter. Podemos crear un nuevo objeto, distinto al anterior pero con la misma estructura, y vincularlo a otro nombre: Counter d = new Counter(); A la derecha del símbolo de asignación, podemos ver dos operaciones distintas: la creación de los objetos (asignación del almacenamiento necesario, new constructor) e inicialización (invocación del constructor de la clase, representado por el nombre de la clase y paréntesis. - Podemos suponer que el código de los métodos se almacena una sola vez en su clase y que cuando un objeto necesita ejecutar un método específico, este código se buscará en la clase de la que es una instancia. Para que esto suceda, el código del método debe acceder correctamente a las variables de instancia que son diferentes para cada objeto y, por lo tanto, no todas están almacenadas juntas en la clase sino dentro de la instancia. - Los métodos de la clase Counter se refieren a sus variables de instancia con el nombre this. Hemos visto que cuando un objeto recibe un mensaje solicitando la ejecución de un método, el objeto en sí es un parámetro de método implícito. Cuando se hace una referencia a una variable de instancia en el cuerpo de un método, hay una referencia implícita al objeto que actualmente ejecuta el método. - Desde el punto de vista lingüístico, el objeto actual generalmente se denota con un nombre único, generalmente self o this. Por ejemplo, la definición del método inc podría escribirse de la siguiente manera. Aquí, la referencia implícita al objeto actual se hace explícita: public void inc() { this.x = this.x+1; } - En el caso de una llamada a un método a través de this, el vínculo entre el método y el código al que se refiere se determina dinámicamente. Este es un aspecto importante de este paradigma. - Algunos lenguajes permiten que algunas variables y métodos se asocien con clases (y no con sus instancias). Se denominan variables y métodos de clase o estáticos. Las variables estáticas se almacenan juntas en la clase y los métodos estáticos no pueden, obviamente, referirse por this en su cuerpo porque no tienen un objeto actual. ---------------------------------------------------------------------------------------------------------------------------- Lenguajes basados en delegación - También existen lenguajes orientados a objetos que carecen de clases. En estos lenguajes, que se basan en la delegación (o en prototipos), el principio organizador no es la clase, sino la delegación; es decir, el principio de que un objeto puede pedir a otro objeto (su padre) que ejecute un método para él. Entre estos lenguajes, el progenitor es Self, que se desarrolló en Xerox PARC y Stanford a finales de la década de 1980. Otros lenguajes basados en la delegación son Dylan, que fue diseñado para distribuirse en una de las primeras versiones de Netscape. - Un objeto se crea a partir de la nada, o más a menudo, copiando (o clonando) otro objeto (que se llama su prototipo). Los prototipos juegan el papel metodológico de clases. Proporcionan el modelo para la estructuración y el funcionamiento de otros objetos, pero no son objetos especiales. Lingüísticamente, son objetos ordinarios que, sin embargo, no se utilizan para calcular, sino como modelo. Cuando se clona un prototipo, la copia mantiene una referencia al prototipo como padre. - Cuando un objeto recibe un mensaje pero no tiene un campo con ese nombre, pasa el mensaje a su padre y el proceso se repite. Cuando el mensaje llega a un objeto que lo entiende, se ejecuta el código asociado al método. El lenguaje asegura que la referencia a self se resuelva
  • 5. correctamente como una referencia al objeto que originalmente recibió el mensaje. En general, los datos del objeto se consideran iguales a los métodos. - El mecanismo de delegación (y la unificación de código y datos) hace que la herencia sea más poderosa. Es posible y natural crear objetos que comparten porciones de datos (en un lenguaje basado en clases, esto solo es posible usando datos estáticos asociados con clases; sin embargo, tal plantilla es demasiado “estática” para ser usada de manera rentable). Además, un objeto puede cambiar la referencia a su padre de forma dinámica y, por lo tanto, cambiar su propio comportamiento. ---------------------------------------------------------------------------------------------------------------------------- Objetos en el montón y en la pila. Todo lenguaje orientado a objetos permite la creación dinámica de objetos. El lugar donde se crean los objetos depende del lenguaje. La solución más común es asignar objetos en el montón y acceder a ellos usando referencias (que serán punteros reales en los lenguajes que lo permitan, pero serán variables si el lenguaje admite un modelo de referencia). Algunos lenguajes permiten la asignación y desasignación explícita de objetos en el montón (C++ es uno); otros, probablemente la mayoría, optan por un recolector de basura. - La opción de crear objetos en la pila como variables ordinarias no es muy común. C ++ es uno de esos lenguajes. Cuando se elabora una declaración de una variable de tipo clase, se realiza la creación e inicialización de un objeto de ese tipo. El objeto se asigna como valor a la variable. Las dos operaciones que contribuyen a la creación de un objeto (asignación e inicialización) ocurren implícitamente en los casos en que no se llama explícitamente al constructor. Algunos lenguajes, finalmente, permiten crear objetos en la pila y dejarlos sin inicializar. - En nuestro pseudolenguaje, asumimos que la creación de objetos ocurre explícitamente en el montón y tiene un modelo de variable de referencia. 10.2.3 ENCAPSULACIÓN - La encapsulación y la ocultación de información representan dos de los puntos cardinales de la abstracción de datos. Todos los lenguajes permiten la definición de objetos ocultando alguna parte de ellos (ya sean datos o métodos). En cada clase hay, por lo tanto, al menos dos vistas: partes privadas y públicas. En la vista privada, todo es visible: es el nivel de acceso posible dentro de la propia clase (por sus métodos). Sin embargo, a la vista del público, solo la información exportada explícitamente es visible. Decimos que la información pública es la interfaz de una clase, por analogía con los ADTs(Tipo de Dato Abstracto). 10.2.4 SUBTIPOS - Se puede hacer que una clase se corresponda, de forma natural, con el conjunto de objetos que son instancias de esa clase. Este conjunto de objetos es el tipo asociado con esa clase. En los lenguajes tipados, esta relación es explícita. Una definición de clase también introduce la definición de un tipo cuyos valores son las instancias de esa clase. En los lenguajes sin tipo (como Smalltalk), la correspondencia es sólo convencional e implícita. - Entre los tipos así obtenidos, se define una relación de compatibilidad en términos de las operaciones posibles sobre valores del tipo. El tipo asociado a la clase T es un subtipo de S cuando todo mensaje entendido (es decir, el que se puede recibir sin generar un error) de los objetos de S también es entendido por los objetos de T. Si un objeto se representa como un registro que contiene datos y funciones, la relación de subtipo corresponde al hecho de que T es un tipo de registro que contiene todos los campos de S, así como posiblemente otros campos. Más precisamente, teniendo en cuenta el hecho de que algunos campos de los dos tipos podrían ser privados, podríamos decir que T es un subtipo de S cuando la interfaz de S es
  • 6. un subconjunto de la interfaz de T (nótese la inversión: un subtipo es obtenido cuando tenemos una interfaz más grande). - Algunos lenguajes (como C++ y Java) utilizan una equivalencia de tipos basada en el nombre que no se extiende adecuadamente a una relación de compatibilidad completamente estructural. En tales lenguajes, no es, por tanto, la única propiedad estructural entre interfaces que define la relación de subtipo, pero debe ser introducida explícitamente por el programador. Este es el papel de la definición de subclases, o clases derivadas, que en nuestro pseudolenguaje se denotarán usando la construcción neutral extending: class NamedCounter extending Counter { private String name; public void set_name(String n) { name = n; } public String get_name() { return name; } } - La clase NamedCounter es una subclase de Counter (que, a su vez, es una superclase de NamedCounter), es decir, el tipo NamedCounter es un subtipo de Counter. Las instancias de NamedCounter contienen todos los campos de Counter (incluso sus campos privados, pero son inaccesibles en la subclase), además de tener nuevos campos introducidos por la definición. De esta manera, se puede garantizar la compatibilidad estructural (una subclase se deriva explícitamente de su superclase) pero esto se indica explícitamente en el programa. Redefinición de un método - En el ejemplo simple de NamedCounter, las subclases se limitan a extender la interfaz de la superclase. Una característica fundamental del mecanismo de subclase es la capacidad de una subclase para modificar la definición (la implementación) de un método presente en su superclase. Este mecanismo se denomina anulación de método. Así, se puede definir una subclase de Counter como: class NewCounter extending Counter { private int num_reset = 0; public void reset() { x = 0; num_reset = num_reset + 1; } public int howmany_resets() { return num_reset; } } - La clase NewCounter amplía simultáneamente la interfaz de Counter con nuevos campos y redefine el método de reinicio. Un método de reinicio enviado a una instancia de NewCounter provocará la invocación de la nueva implementación. Shadowing(Ocultamiento) Además de modificar la implementación de un método, una subclase también puede redefinir una variable de instancia (o campo) definida en una superclase. Este mecanismo se llama shadowing. Por razones de implementación, el shadowing es significativamente diferente del overriding. Por el momento, simplemente notamos que, en una subclase, una variable de instancia se puede redefinir con el mismo nombre y el mismo tipo que en la superclase. Podríamos, por ejemplo, modificar los contadores
  • 7. extendidos usando la siguiente subclase de NewCounter donde, por alguna (extraña) razón, el valor inicial de num_reset se inicializa a 2 y se incrementa en 2 cada vez: class EvenNewCounter extending NewCounter { private int num_reset = 2; public void reset() { x = 0; num_reset = num_reset + 2; } public int howmany_resets() { return num_reset; } } - Usando la noción habitual de visibilidad en lenguajes estructurados en bloques, cada referencia a num_reset dentro de EvenNewCounter se refiere a la variable local (inicializada en 2) y no a la declarada en NewCounter. Un mensaje reset enviado a una instancia de EvenNewCounter provocará la invocación de la nueva implementación para reset que utilizará el nuevo campo num_reset. Sin embargo, existe una gran diferencia, tanto a nivel semántico como a nivel de implementación, entre overriding y shadowing. Clases abstractas Para simplificar, hemos introducido el tipo asociado con una clase como el conjunto de sus instancias. Sin embargo, muchos lenguajes permiten la definición de clases que no pueden tener instancias porque la clase carece de la implementación de algún método. En tales clases, solo existe el nombre y el tipo (es decir, la firma) de uno o más métodos; se omite su implementación. Las clases de este tipo se denominan clases abstractas. Las clases abstractas sirven para proporcionar interfaces y pueden recibir implementaciones en subclases que redefinen (o, define por primera vez) el método que carece de implementación. Las clases abstractas también corresponden a tipos y el mecanismo que proporciona implementaciones para sus métodos también genera subtipos. La relación de subtipo En general, los lenguajes prohíben los ciclos en la relación de subtipo entre clases, es decir, no podemos tener tanto A subtipo B como B subtipo A, a menos que A y B coincidan. La relación de subtipo es, por tanto, un orden parcial. En muchos lenguajes, esta relación tiene un elemento máximo: el tipo del cual todos los demás tipos definidos por clases son subtipos. Denotaremos tal tipo con el nombre Object. Los mensajes que acepta una instancia de Object son bastante limitados (es un objeto de máxima generalidad). En general, encontramos un método de clonación que devuelve una copia del objeto, un método de igualdad y algunos otros. Además, algunos de estos métodos pueden ser abstractos en Object y, por lo tanto, deben ser redefinidos antes de poder usarse. Se puede ver que no se garantiza que, dado un tipo A, exista un tipo B único que es el supertipo inmediato de A. En el siguiente ejemplo: abstract class A { public int f(); } abstract class B { public int g(); } class C extending A, B { private x = 0;
  • 8. public int f() { return x; } public int g() { return x+1; }} - El tipo C es un subtipo tanto de A como de B y no hay otro tipo incluido entre C y los otros dos. La jerarquía de subtipos no es un árbol en general, sino sólo un gráfico orientado acíclico. Constructores. Ya hemos visto que un objeto es una estructura compleja que incluye datos y código, todos juntos. La creación de un objeto es, por tanto, también una operación de cierta complejidad que consta de dos acciones distintas: primero, la asignación de la memoria necesaria (en el montón o en la pila) y la inicialización adecuada de los datos del objeto. Esta última acción la realiza el constructor de la clase. Es decir, por código asociado a la clase y que el lenguaje garantiza que se ejecutará exactamente al mismo tiempo que se crea la instancia. El mecanismo del constructor es de cierta complejidad porque los datos de un objeto consisten no sólo en los declarados explícitamente en la clase cuya instancia se está creando, sino también en los datos declarados en sus superclases. Además, a menudo se permite más de un constructor para una clase. Tenemos, por tanto, una serie de preguntas sobre constructores que se pueden resumir en las siguientes: - Selección de constructor. Cuando el lenguaje lo permite, hay más de un constructor para una clase, entonces, ¿cómo se elige qué clase usar al crear un objeto específico? En algunos lenguajes (por ejemplo, C++ y Java), el nombre del constructor es el mismo que el de la clase. Múltiples constructores tienen todos el mismo nombre y deben distinguirse por su tipo o el número de argumentos o ambos (por lo tanto, están sobrecargados, con resolución estática). Dado que C++ permite la creación implícita de objetos en la pila, existen mecanismos específicos para seleccionar el constructor apropiado a utilizar en cada caso. Otros lenguajes permiten al programador elegir libremente los nombres de los constructores (pero los constructores permanecen sintácticamente distintos de los métodos ordinarios) y requieren que cada operación de creación esté siempre asociada con un constructor. - Encadenamiento de constructores. ¿Cómo y cuándo se inicializa la parte del objeto que pertenece a la superclase? Algunos lenguajes se limitan a ejecutar el constructor de la clase cuya instancia están construyendo. Si el programador tiene la intención de llamar a constructores de superclase, debe hacerlo explícitamente. Otros lenguajes (entre ellos C++ y Java) garantizan, por otro lado, que cuando se inicializa un objeto, el constructor de su superclase será invocado (encadenamiento de constructor) antes de que el constructor específico de la subclase realice cualquier otra operación. Una vez más, hay muchas cuestiones que cada lenguaje tiene que resolver. Entre estos, los dos más importantes son determinar qué constructor de superclase invocar y cómo determinar sus argumentos.
  • 9. 10.2.5 HERENCIA - Hemos visto que una subclase puede redefinir los métodos de su superclase. Pero, ¿qué pasa cuando la subclase no los redefine? En tales casos, la subclase hereda métodos de la superclase, en el sentido de que la implementación del método en la superclase está disponible para la subclase. Por ejemplo, NewCounter hereda de Counter el elemento de datos x y los métodos inc y get (pero no reset, que es redefinido). - De manera más general, podemos caracterizar la herencia como un mecanismo que permite la definición de nuevos objetos basados en la reutilización de los preexistentes. La herencia permite la reutilización del código en un contexto ampliable. Al modificar la implementación de un método, una clase automáticamente pone la modificación a disposición de todas sus subclases, sin que se requiera intervención por parte del programador. - Es importante comprender la diferencia entre la relación de herencia y la de subtipo. El concepto de subtipo tiene que ver con la posibilidad de utilizar un objeto en otro contexto. Es una relación entre las interfaces de dos clases. El concepto de herencia tiene que ver con la posibilidad de reutilizar el código que manipula un objeto. Es una relación entre las implementaciones de dos clases. - Estamos tratando con dos mecanismos que son completamente independientes. Tanto C++ como Java tienen construcciones que pueden introducir simultáneamente ambas relaciones entre las dos clases, pero esto no significa que los conceptos sean los mismos. - En la literatura, a veces vemos la distinción entre herencia de implementación (herencia) y herencia de interfaz (nuestra relación de subtipo). Herencia y visibilidad Hemos visto que hay dos visiones de cada clase: la privada y la pública, esta última compartida entre todos los clientes de la clase. Una subclase es un cliente particular de la superclase. Utiliza los métodos de la superclase para ampliar la funcionalidad de la superclase, pero a veces tiene que acceder a algunos datos no públicos. Por tanto, muchos lenguajes introducen una tercera vista de una clase: una para las subclases. Tomando el término de C++, podemos referirnos a él como la vista protegida de una clase. - Si la subclase tiene acceso a algunos detalles de la implementación de la superclase, la subclase depende mucho más de la superclase. Cada modificación de la superclase requerirá una modificación de la subclase. Desde el punto de vista pragmático, esto es razonable sólo si las dos clases están "próximas" entre sí, por ejemplo, si pertenecen al mismo paquete. Cuanto más fuerte sea el emparejamiento de las dos clases, más difícil será modificar y mantener el sistema resultante. Sin embargo, hacer coincidir las interfaces públicas y protegidas puede resultar demasiado restrictivo. Al poder acceder a la representación de las estructuras de datos, la subclase podrá implementar sus propias operaciones de una manera más eficiente. Herencia única y múltiple En algunos lenguajes, una clase puede heredar de una única superclase inmediata. La jerarquía de herencia es, por tanto, un árbol y decimos que el lenguaje tiene herencia única (o simple). Otros lenguajes, por otro lado, permiten que una clase herede métodos de más de una superclase inmediata; la jerarquía de herencia en tal caso es un grafo orientado acíclico y el lenguaje tiene herencia múltiple. - Hay solo unos pocos lenguajes que soportan herencia múltiple (entre los que se encuentran C++ y Eiffel), porque esto presenta problemas que no tienen una solución elegante ni a nivel conceptual ni de implementación. Los problemas fundamentales se relacionan con los conflictos de nombres. Tenemos un conflicto de nombres cuando una clase C hereda simultáneamente de A y B, que proporcionan implementación para métodos con la misma firma. El siguiente es un ejemplo simple: class A { int x;
  • 10. int f() { return x; } } class B { int y; int f() { return y; } } class C extending A, B { int h() { return f(); } } ---------------------------------------------------------------------------------------------------------------------------- Herencia y subtipos en Java - La relación de subtipo se introduce en Java utilizando la cláusula extends (para definir subclases) o la cláusula implements cuando una clase se declara subtipo de una o más interfaces (para Java, una interfaz es un tipo de clase abstracta incompleta en la que solo los nombres y firmas de métodos se incluyen - no incluyen implementaciones). La relación de herencia se introduce con la cláusula extends siempre que la subclase no redefina un método y, por lo tanto, usa una implementación de la superclase. Cabe señalar que nunca hay herencia de una interfaz porque la interfaz no tiene nada que heredar. El lenguaje restringe cada clase a tener una sola superclase inmediata (es decir, una sola superclase puede ser nombrada con una extends), pero permite que una sola clase (o interfaz) implemente más de una interfaz: interface A { int f(); } interface B { int g(); } class C { int x; int h() { return x+2; } } class D extends C implements A, B { int f() { return x; } int g() { return x+1; } }
  • 11. - Por tanto, Java tiene herencia única. La jerarquía de herencia es un árbol organizado por cláusulas extends. Además, la relación de herencia es, en Java, siempre una sub jerarquía de la jerarquía de subtipos. - En el ejemplo, cuando al mismo tiempo tenemos herencia de superclase e implementación de alguna interfaz abstracta, a menudo se denomina herencia mixta (porque los nombres de los métodos abstractos en la interfaz se mezclan con las implementaciones heredadas). En muchos manuales (e incluso en la definición oficial de Java...), se dice que Java tiene una herencia única para las clases, pero una herencia múltiple para las interfaces. Según nuestra terminología, no existe una verdadera herencia en lo que respecta a las interfaces. ---------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------- Herencia y subtipos en C++ - Los mecanismos de C++ que permiten la definición de la relación de herencia, y los responsables de la subtipificación, no son independientes. La definición de clases derivadas (el término de C++ para subclase) introduce la relación de herencia. También introduce una relación de subtipo cuando la clase derivada declara su clase base (que es la superclase) como public; cuando la clase base no es pública, la clase derivada hereda de la clase base pero no existe una relación de subtipo. class A { public: void f() {...} … } class B : public A { public: void g() {...} … } class C : A { public: void h() {...} … } - Ambas clases, B y C, heredan de A, pero solo B es un subtipo de A. - Dado que la relación de subtipo sigue a la de subclase, con las herramientas vistas hasta ahora, no podríamos introducir un subtipo de interfaz, es decir, una clase derivada de una clase base que no proporciona implementaciones sino que solo corrige una interfaz. Es con este fin que C++ introduce el concepto de clase base abstracta, en la que algunos métodos no necesitan tener una implementación. En tales casos, como ocurre con las interfaces Java, podemos tener subtipos sin herencia. ---------------------------------------------------------------------------------------------------------------------------- - ¿Cuál de los dos métodos llamados f se hereda en C? Podemos solucionar este problema de tres formas diferentes, ninguna de las cuales es totalmente satisfactoria: 1. Prohibir los choques sintácticos de nombres. 2. Exija que cualquier conflicto sea resuelto por el programador calificando apropiadamente cada referencia a un nombre que esté en conflicto. Por ejemplo, el cuerpo de h en la clase C, debe escribirse como B :: f () o como A :: f (), que es la solución adoptada por C++. 3. Decidir sobre una convención para resolver el conflicto, por ejemplo, favoreciendo la primera clase nombrada en la cláusula extending
  • 12. - En lo que respecta a la solución explícita, se puede ver que el conflicto podría observarse no en C sino en una de sus subclases (si no se llama a f dentro de C). Por tanto, el diseñador debe conocer la jerarquía de clases con cierta precisión. Sin embargo, en casos como este, es metodológicamente mejor redefinir f en C. Por ejemplo: class C extending A, B { int f() { return A :: f(); } int h() { return this.f(); } } de modo que en las subclases de C, no hay conflictos de nombres. - Los problemas más interesantes de la herencia múltiple están presentes en el llamado problema del diamante. Este caso surge cuando una clase hereda de dos superclases, cada una de las cuales hereda de la misma superclase única. Una situación simple de este tipo es la siguiente (la jerarquía de herencia en forma de diamante): class Top { int w; int f() { return w; } } class A extending Top { int x; int g() { return w+x; } } class B extending Top { int y; int f() { return w+y; } int k() { return y; } } class Bottom extending A,B { int z; int h() { return z; } }
  • 13. - En este caso, también, tenemos el problema habitual del conflicto de nombres, pero es fundamentalmente la cuestión de implementación la que es más relevante. Es decir, idear una técnica que permita la selección correcta y eficiente del código para f cuando se invoca este método en una instancia de la clase Bottom. - En conclusión, la herencia múltiple es una herramienta muy flexible para la combinación de abstracciones correspondientes a distintas funcionalidades. Algunas de las situaciones en las que trabaja se expresan mejor en realidad utilizando relaciones de subtipo (“heredando” de clases abstractas). No existen soluciones simples, inequívocas y elegantes a los problemas que plantea la herencia múltiple. El balance de costo-beneficio entre la herencia única y múltiple es ambiguo. 10.2.5 BÚSQUEDA DE MÉTODO DINÁMICO - La búsqueda (o envío) de métodos dinámicos es el corazón del paradigma orientado a objetos. En esto, las características que ya se han discutido se combinan para formar una nueva síntesis. En particular, la búsqueda de métodos dinámicos permite que coexistan la compatibilidad de subtipos y la abstracción. Esto es, en particular, algo que se consideró problemático para los tipos de datos abstractos. - Conceptualmente, el mecanismo es muy simple, ya hemos visto que un método definido para un objeto se puede redefinir (anular-overridden) en objetos que pertenecen a subtipos del objeto original. Por lo tanto, cuando se invoca un método, m, sobre un objeto, o, puede haber muchas versiones de m posibles para o. La selección de qué implementación para m se utilizará en una invocación es una función del tipo de objeto que recibe el mensaje. o.m (parámetros); Tenga en cuenta que lo relevante es el tipo de objeto que recibe el mensaje, no el tipo de referencia (o nombre) a ese objeto (que en cambio es información estática). - Vamos a dar un ejemplo en nuestro pseudo-lenguaje con las clases y contadores vistos en secciones anteriores, y en ese contexto ejecutamos el siguiente fragmento: NewCounter n = new NewCounter(); Counter c; c = n; c.reset(); class Counter { private int x; public void reset() { x = 0; } public int get() { return x; } public void inc() { x = x+1; } } class NewCounter extending Counter { private int num_reset = 0; public void reset() { x = 0; num_reset = num_reset + 1; } public int howmany_resets() {
  • 14. return num_reset; } } - El tipo (estático) del nombre c es Counter pero se refiere (dinámicamente) a una instancia de NewCounter. Por lo tanto, será el método reset de NewCounter el que se invoque. Tanto Counter como NewCounter se almacenan en una estructura de datos cuyo tipo es su supertipo: Counter V[100]; Ahora aplicamos el método reset a cada uno: for (int i=1; i<100; i=i+1) V[i].reset(); - La búsqueda dinámica nos asegura que se invocará el método correcto en cualquier contador. En general, un compilador no podrá decidir cuál será el tipo de objeto cuyo método será invocado, de ahí la dinámica de este mecanismo. - El lector debería haber notado una cierta analogía entre la sobrecarga y la búsqueda de métodos dinámicos. En ambos casos el problema es el mismo: el de resolver una situación ambigua en la que un solo nombre puede tener más de un significado. - Sin embargo, bajo sobrecarga, la ambigüedad se resuelve estáticamente, según el tipo de nombres involucrados. En la búsqueda de métodos, por otro lado, la solución del problema está en tiempo de ejecución y hace uso del tipo dinámico del objeto y no de su nombre. Sin embargo, no es un error pensar en la búsqueda de métodos como una operación de sobrecarga en tiempo de ejecución en la que el objeto que recibe el mensaje se considera el primer argumento (implícito) del método cuyo nombre debe resolverse. - Es importante observar explícitamente que la búsqueda de métodos dinámicos funciona incluso cuando un método en un objeto invoca un método en el mismo objeto, como sucede en el siguiente fragmento: class A { int a = 0; void f() {g();} void g() {a=1;} } class B extending A { int b = 0; void g() {b=2;} } - Supongamos ahora que tenemos un objeto b que es una instancia de B y queremos invocar en b el método f heredado por b de A. Ahora, f llama a g: ¿cuál de las dos implementaciones de g se ejecutará? Recuerde que el objeto que recibe el mensaje también es un parámetro importante del método. La llamada de g en el cuerpo de f se puede escribir más explícitamente como this.g(), donde, recuerde, this es una referencia al objeto actual. El objeto actual es b y, por lo tanto, el método que se invocará es el de la clase B. Se puede ver que, de esta manera, una llamada a un método como this.g () en el cuerpo de f puede referirse a ( implementaciones de) métodos que aún no están escritos y que estarán disponibles sólo más adelante a través de la jerarquía de clases. Este mecanismo, a través del cual el nombre this se vincula dinámicamente al objeto actual, se denomina vinculación tardía de sí mismo (o de this). - Notemos explícitamente que, a diferencia del overriding, shadowing es un mecanismo completamente estático. Considere, por ejemplo, el código: class A { int a = 1; int f() {return -a;} }
  • 15. class B extending A { int a = 2; int f() {return a;} } B obj_b = new B(); A obj_a = obj_b; print(obj_b.f()); print(obj_a.f()); print(obj_b.a); print(obj_a.a); ---------------------------------------------------------------------------------------------------------------------------- Envío dinámico en C ++ - En lenguajes como Java o Smalltalk, la invocación de cada método se realiza mediante el envío dinámico. C++ tiene como objetivos de diseño, eficiencia de ejecución y compatibilidad con C, lo que significa que el uso de una construcción C en un programa C++ debe ser eficiente como en C. - En C++, tenemos, por lo tanto, un método de envío estático (análogo a la llamada de función), así como una forma de envío dinámico que se realiza mediante funciones virtuales. Cuando se define un método (que es una función miembro en terminología C++), es posible especificar si es una función virtual o no. Solo se permite overriding de funciones virtuales. El envío dinámico se realiza en funciones de miembros virtuales. - Observemos, dicho sea de paso, que no está prohibido definir, en alguna clase, una función del mismo nombre y firma que una función no virtual definida en la superclase. En tal caso, no tenemos redefinición, sino sobrecarga, y esto puede ser resuelto estáticamente por el compilador usando el tipo de nombre usado para referirse a ese objeto. En el ejemplo que sigue declaramos una clase A y una subclase B: class A { public: void f() {printf("A");} virtual void g() {printf("A");} } class B : public A { public: void f() {printf("B");} virtual void g() {printf("B");} } Si, ahora, tenemos un puntero a de tipo A*, que apunta a una instancia de B, la invocación de la función a-> f () imprime A, mientras que la invocación de la función virtual a-> g () imprime B. ---------------------------------------------------------------------------------------------------------------------------- - donde print denota un método que genera el valor entero pasado como argumento. Las dos primeras invocaciones de print producen el valor 2 ‘dos veces’, como debería ser obvio. La tercera llamada también imprimirá el valor 2, dado que, como ya se vio anteriormente, el método f se ha redefinido en la clase B. De hecho, el objeto creado (con new B()) es una instancia de B y, por lo tanto, cada acceso a él, incluso aquellos a través de una variable de tipo A (como en el caso de obj_a), utiliza el método redefinido (que obviamente es utilizando los campos redefinidos en la clase B). La última aparición de print, en cambio, produce el valor 1. En este caso, de hecho, dado que no estamos tratando con la invocación de un método sino accediendo a un campo, es el tipo de referencia actual el que determina qué campo es para ser considerado. Dado que obj_a es de tipo A, cuando escribimos obj_a.a, el campo en cuestión es el de la clase A (inicializado a uno).
  • 16. - Esta distinción entre overring y shadowing, se explica a nivel de implementación utilizando el hecho de que el objeto de instancia de la clase B también contiene todos los campos de las superclases de B, como aclararemos en la siguiente sección. ---------------------------------------------------------------------------------------------------------------------------- Lenguajes multimétodo - En los lenguajes que hemos considerado hasta ahora, la invocación de un método tiene la forma: o.m(parámetros); - La búsqueda de métodos dinámicos usa el tipo de tiempo de ejecución del objeto que recibe el método, mientras que usa los tipos estáticos de los parámetros. En algunos otros lenguajes (por ejemplo, CLOS), esta asimetría se elimina. Los métodos ya no están asociados con clases, sino que son nombres globales. Cada nombre de método está sobrecargado por una serie de implementaciones (que en este caso se denominan métodos múltiples) y el objeto en el que se invoca el método se pasa a cada método múltiple. Al invocar un método múltiple, el código que debe ejecutarse se elige dinámicamente en función del tipo (dinámico) tanto del receptor como de todos los argumentos. - En estos lenguajes, hablamos de envío múltiple en lugar de envío único en lenguajes donde existe un receptor privilegiado. - En lenguajes con despacho múltiple, la analogía entre despacho (múltiple) dinámico y “sobrecarga dinámica” es más evidente; El envío múltiple consiste en la resolución (en tiempo de ejecución) de una sobrecarga sobre la base de información de tipo dinámico. ---------------------------------------------------------------------------------------------------------------------------- 10.3 ASPECTOS DE IMPLEMENTACIÓN - Cada lenguaje tiene su propio modelo de implementación que está optimizado para las características específicas que proporciona el lenguaje. Sin embargo, podemos señalar algunas líneas comunes que indican los principales problemas y sus soluciones. Objetos. Un objeto de instancia de la clase A puede usarse como el valor de cualquier superclase de A. En particular, es posible acceder no solo a las variables de instancia (campos de datos) explícitamente definidas en A, sino también a las definidas en sus superclases. Un objeto se puede representar como si fuera un registro, con tantos campos como variables haya en la clase de la que es instancia, además de todas las que aparecen en sus superclases. - En un caso de shadowing, o más bien cuando se usa un nombre de una variable de instancia de la clase con el mismo tipo en una subclase, el objeto contiene campos adicionales, cada uno correspondiente a una declaración diferente (a menudo, el nombre usado en la superclase no es accesible en la subclase, si no se utilizan calificaciones particulares, por ejemplo super). La representación también contiene un puntero al descriptor de la clase de la que es una instancia. - En el caso de un lenguaje con un sistema de tipos estáticos, esta representación permite la implementación simple de compatibilidad de subtipos (en el caso de herencia única). Para cada variable de instancia, el desplazamiento desde el inicio del registro se registra estáticamente. Si el acceso a un objeto de instancia de una clase se realiza utilizando una referencia (estática) que tiene como tipo una de las superclases de ese objeto, la verificación de tipo estático asegura que el acceso solo se puede realizar a un campo definido en la superclase, que está asignado en la parte inicial del registro. Clases y herencia. La representación de clases es la clave de la máquina abstracta de orientación a objetos. La implementación más simple e intuitiva es la que representa la jerarquía de clases mediante una lista vinculada. Cada elemento representa una clase y contiene (apunta a) la implementación de los métodos que están definidos o redefinidos en esa clase. Los elementos se vinculan mediante un puntero que apunta desde la subclase a la
  • 17. superclase inmediata. Cuando se invoca un método m de un objeto o que es una instancia de una clase C, el puntero almacenado en o se usa para acceder al descriptor de C y determinar si C contiene una implementación de m. Si no es así, el puntero se establece en la superclase y se repite el procedimiento. Por ejemplo: (una implementación simple de herencia) Enlace tardío de sí mismo. Un método se ejecuta de forma similar a una función. Un registro de activación para las variables locales del método, los parámetros y toda la otra información se inserta en la pila. Sin embargo, a diferencia de una función, un método también debe acceder a las variables de instancia del objeto en el que se llama; la identidad del objeto no se conoce en el momento de la compilación. - Sin embargo, la estructura de dicho objeto es conocida (depende de la clase) y, por lo tanto, el desplazamiento de cada variable de instancia en la representación de un objeto es estáticamente conocido (está sujeto a condiciones que dependen del lenguaje). Un puntero al objeto que recibió el método también se pasa como parámetro cuando se invoca un método. Durante la ejecución del cuerpo del método, este puntero es el método de this. Cuando se invoca el método en el mismo objeto que lo invoca, this todavía se pasa como parámetro. Durante la ejecución del método, cada acceso a una variable de instancia utiliza el desplazamiento de este puntero (en lugar de tener un desplazamiento de un puntero en los registros de activación, como es el caso de las variables locales de funciones). - Desde un punto de vista lógico, podemos suponer que el puntero this se pasa a través del registro de activación normalmente con todos los demás parámetros, pero esto causaría un acceso doblemente indirecto para cada variable de instancia (uno para acceder al puntero usando el puntero del registro de activación y uno para acceder a la variable usando this). De manera más eficiente, la máquina abstracta mantendrá el valor actual de this en un registro. 10.3.1 HERENCIA ÚNICA - Bajo la hipótesis de que el lenguaje tiene un sistema de tipos estáticos, la implementación que utiliza listas enlazadas puede ser reemplazada por otra más eficiente, en la que la selección del método requiere un tiempo constante (en lugar de un tiempo lineal en la profundidad de la jerarquía de clases). - Si los tipos son estáticos, el conjunto de métodos que cualquier objeto puede invocar se conoce en tiempo de compilación. La lista de estos métodos se guarda en el descriptor de clase. La
  • 18. lista contiene no sólo los métodos que se definen o redefinen explícitamente en la clase, sino también todos los métodos heredados de sus superclases. - Siguiendo la terminología de C ++, usaremos el término vtable (tabla de funciones virtuales) para referirnos a esta estructura de datos. Cada definición de clase tiene su propia vtable y todas las instancias de la misma clase comparten la misma vtable. Cuando se define una subclase, B, de la clase A, la vtabla de B se genera copiando la de A, reemplazando todos los métodos redefinidos en B y agregando los nuevos métodos que B define en la parte inferior. - La propiedad fundamental de esta estructura de datos es que, si B es una subclase de A, la vtable de B contiene una copia de la vtable de A como parte inicial; los métodos redefinidos se modifican adecuadamente. De esta manera, la invocación del método cuesta solo dos accesos indirectos, siendo el desplazamiento de cada método en la vtable conocido estáticamente. Se puede observar que esta implementación obviamente tiene en cuenta el hecho de que es posible acceder a un objeto con una referencia que pertenece estáticamente a una de sus superclases. - En el ejemplo de la figura anterior, cuando se invoca el método f, el compilador calcula un desplazamiento que permanece igual si f se invoca en un objeto de la clase A o en un objeto de la clase B. Diferentes implementaciones de f definidas en las dos clases están ubicados en el mismo desplazamiento en las vtables de A y B. - En general, si tenemos una referencia estática pa de la clase A a algún objeto de una de sus subclases, podemos compilar una llamada al método f (que, asumimos que es el n-ésimo en la vtable de A) de la siguiente manera (hemos asumido que la dirección de un método ocupa w bytes): R1 := pa // acceder al objeto R2 := *(R1) // vtable R3 := *(R2 + (n − 1) × w) // indirecto a f call *(R3) // llamada a f - Ignorando el soporte para herencia múltiple, este esquema de implementación es casi el mismo que se usa en C++. Downcasting. Si la vtable de una clase también contiene el nombre de la clase en sí, la implementación que hemos discutido permite conversiones descendentes en la jerarquía de clases. Esto se llama downcasting y es un mecanismo que se utiliza con bastante frecuencia. Permite la especialización del tipo de objeto corriendo en dirección opuesta a la jerarquía de subtipos. Un ejemplo canónico del uso de este mecanismo se encuentra en algunos métodos de librerías que se definen como lo más generales posible. La clase Object podría usar la siguiente firma para definir un método para copiar objetos: Object clone(){...}
  • 19. - La semántica es que clone devuelve una copia exacta del objeto que lo invocó (misma clase, mismos valores en sus campos, etc.). Si tenemos un objeto o de la clase A, una invocación de o.clone() devolverá una instancia de A, pero el tipo estático determinado por el compilador para esta expresión es Object. Para poder usar esta nueva copia de manera significativa, tenemos que "forzarla" al tipo A, lo que podemos indicar con una conversión: A a = (A) o.clone(); - Con esto, queremos decir que la máquina abstracta verifica que el tipo dinámico del objeto sea realmente A (caso contrario, tendríamos un error de tipo en tiempo de ejecución). 10.3.2 EL PROBLEMA DE LA CLASE BASE FRÁGIL - En el caso de herencia simple, la implementación del mecanismo descrito anteriormente es razonablemente eficiente, dado que toda la información importante, excepto el puntero this, está determinada estáticamente. Sin embargo, se puede demostrar que esta estática es la fuente de problemas en un contexto conocido como el problema de la clase base frágil. - Un sistema orientado a objetos se organiza en términos de un gran número de clases utilizando una elaborada jerarquía de herencia. A menudo, algunas de estas clases generales las proporcionan las librerías. Las modificaciones a una clase ubicada muy alto en la jerarquía, se pueden sentir en sus subclases. Algunas superclases pueden, por lo tanto, comportarse de una manera "frágil" porque una modificación aparentemente inocua de la superclase puede causar el mal funcionamiento de las subclases. - No es posible identificar la fragilidad analizando solo la superclase. Es necesario considerar toda la jerarquía de herencia, algo que a menudo es imposible porque quien escribió la superclase generalmente no tiene acceso a todas las subclases. El problema puede surgir por varias razones. Aquí, sólo distinguiremos dos casos principales: 1. El problema es arquitectónico. Algunas subclases explotan aspectos de la implementación de la superclase que se han modificado en la nueva versión. Este es un problema extremadamente importante para la ingeniería de software. Puede limitarse reduciendo la relación de herencia a favor de la relación de subtipo. 2. El problema es de implementación. El mal funcionamiento de la subclase depende solo de cómo la máquina abstracta (y el compilador) ha representado la herencia. Este caso a veces se conoce como el problema de la interfaz binaria frágil. - En este texto, el primer caso no es nuestro principal interés; el segundo lo es. Un caso típico aparece en el contexto de una compilación separada donde se agrega un método a una superclase, incluso si el nuevo método no interactúa con nada más. Partamos de lo siguiente: class Upper { int x; int f() {..} } class Lower extending Upper { int y; int g() {return y + f();} } la superclase se modifica así: class Upper { int x; int h() {return x;} int f() {..} } - Si la herencia se implementa como se describe en la Sect. 10.3.2, la subclase Lower (que ya está compilada y vinculada a Super) deja de funcionar correctamente porque se ha cambiado el desplazamiento utilizado para acceder al método f, ya que se determina estáticamente. Para
  • 20. solucionar este problema es necesario recompilar todas las subclases de las clases modificadas, solución que no siempre es fácil de realizar. - Para obviar esta pregunta, es necesario calcular dinámicamente el desplazamiento de métodos en la vtable (y también el desplazamiento de variables de instancia en la representación de objetos), de alguna manera razonablemente eficiente. La siguiente sección describe una posible solución. 10.3.3 DESPACHO DE MÉTODO DINÁMICO EN LA JVM - En esta sección presentamos una descripción simple de la técnica utilizada por la máquina virtual Java (JVM), la máquina abstracta que interpreta el lenguaje intermedio (bytecode) generado por el compilador estándar de Java. Por razones de espacio, no podemos entrar en los detalles de la arquitectura de la JVM; observamos que es una máquina basada en pilas (no tiene registros accesibles para el usuario y todos los operandos de operación se pasan a una pila contenida en el registro de activación de la función que está siendo ejecutado actualmente). La JVM tiene módulos para verificar la seguridad de la operación. Nos limitamos a discutir las líneas generales de la implementación de la herencia y el envío de métodos. - En Java, la compilación de cada clase produce un archivo que la máquina abstracta carga dinámicamente cuando el programa en ejecución se refiere a la clase. Este archivo contiene una tabla (constant pool-el grupo de constantes) para los símbolos utilizados en la propia clase. El grupo de constantes contiene entradas, por ejemplo, variables, métodos públicos y privados, métodos y campos de otras clases utilizadas en el cuerpo de métodos, nombres de otras clases mencionadas en el cuerpo de la clase, etc. Con cada variable de instancia y nombre de método se registra información como la clase donde se definen los nombres y su tipo. Cada vez que el código fuente usa el nombre, el código intermedio de la JVM busca el índice de ese nombre en el grupo constante (para ahorrar espacio, no busca el nombre en sí). Cuando durante la ejecución se hace referencia a un nombre por primera vez (utilizando su índice), se resuelve utilizando la información del pool de constantes, se cargan las clases necesarias (por ejemplo aquella en la que se introducen los nombres), se realizan comprobaciones de visibilidad (por ejemplo, el método invocado debe existir realmente en la clase que se refiere a él, no es privado, etc.); También se realizan comprobaciones de tipo. En este punto, las máquinas abstractas guardan un puntero a esta información para que la próxima vez que se use el mismo nombre, no sea necesario realizar la resolución por segunda vez. - La representación de métodos en un descriptor de clase puede considerarse análoga a la vtable. La tabla de una subclase comienza con una copia de su superclase, donde los métodos redefinidos tienen sus nuevas definiciones en lugar de las antiguas. Sin embargo, las compensaciones no se calculan estáticamente. Cuando se va a ejecutar un método, se pueden distinguir cuatro casos principales (que corresponden a distintas instrucciones de código de bytes): 1. El método es estático. Esto es para un método asociado a una clase y no a una instancia. No se puede hacer ninguna referencia (explícita o implícita) a this. 2. El método debe distribuirse dinámicamente (un método "virtual"). 3. El método debe distribuirse dinámicamente y se invoca mediante this (un método "especial"). 4. El método proviene de una interfaz (es decir, de una clase completamente abstracta que no proporciona implementación, un método de "interfaz"). - Ignorando el último caso por el momento, los otros 3 casos se pueden distinguir principalmente por los parámetros pasados al método. En el primer caso, solo se pasan los parámetros nombrados en la llamada. En el segundo caso, se pasa una referencia al objeto en el que se llama al método. En el caso 3, se pasa una referencia a this. Por lo tanto, supongamos que podemos invocar el método m en el objeto o como: o.m(parámetro)
  • 21. - Usando la referencia a o, la máquina abstracta accede al grupo constante de su clase y extrae el nombre de m. En este punto, busca este nombre en la tabla vtable de la clase y determina su desplazamiento. El desplazamiento se guarda en caso de un uso futuro del mismo método en el mismo objeto. - Sin embargo, el mismo método también podría invocarse en otros objetos, posiblemente pertenecientes a subclases de la que pertenece o (por ejemplo, un bucle for en cuyo cuerpo se repite una llamada a m en todos los objetos de una matriz). Para evitar calcular el desplazamiento cada vez (que sería el mismo independientemente de la clase efectiva de la que se llama al método m es una instancia), el intérprete de JVM utiliza una técnica de "reescritura de código". Esto sustituye a la instrucción de búsqueda estándar generada por el compilador por una forma optimizada que toma como argumento el desplazamiento del método en la vtable. Para arreglar ideas (y simplificar mucho), al traducir la invocación de un método virtual m, el compilador podría generar la instrucción de código de bytes: invokevirtual index donde index es el índice del nombre m en el grupo constante. Durante la ejecución de esta instrucción, el intérprete de JVM calcula el desplazamiento d de m en su vtable y reemplaza la instrucción anterior con lo siguiente: invokevirtual_quick d np - Aquí, np es el número de parámetros que espera m (y que se encontrarán en la pila que forma parte de su registro de activación). Cada vez que el flujo de control vuelve a esta instrucción, se invoca m sin sobrecarga. - Queda el caso de la invocación de un método de interfaz (es decir, el caso 4 anterior). En este caso, el desplazamiento podría no ser el mismo en 2 invocaciones del mismo método en objetos de diferentes clases. Considere la siguiente situación (en la que usamos sintaxis Java en lugar de pseudocódigo): interface Interface { void foo(); } public class A implements Interface { int x; void foo(){...} } public class B implements Interface { int y; int fie(){...} int foo(){...} } Tanto A como B implementan Interface y, por lo tanto, son sus subtipos. Sin embargo, el desplazamiento de foo es diferente en las dos clases. Considere nuestro ciclo habitual: Interface V[10]; … for (int i = 0; i<10; i=i+1) V[i].foo(); En tiempo de ejecución, no sabemos si los objetos contenidos en V serán instancias de A, B o alguna otra clase que implemente Interface. El compilador podría haber generado la instrucción JVM para el cuerpo del bucle: invokeinterface index, 0 (el 0 sirve para llenar un byte que se utilizará en la versión “rápida”). No sería correcto reemplazar directamente esta instrucción por una versión “rápida” que devuelve solo el desplazamiento porque cambiar el objeto también podría cambiar la clase de la que es una instancia. Lo que podemos hacer es guardar el desplazamiento pero no podemos destruir el nombre original del método; por lo tanto, reescribimos la instrucción como: invokeinterface_quick nome_di_foo, d
  • 22. Aquí name_of_foo es información adecuada con la que reconstruir el nombre y la firma de foo; d es el desplazamiento determinado de antemano. Cuando se vuelve a ejecutar esta instrucción, el intérprete accede a la vtable utilizando el desplazamiento d y comprueba que existe un método con el nombre y la firma solicitados. En el caso positivo, se invoca; en el caso negativo, busca el método por nombre en la vtable, como si fuera la primera vez que fue vista, y determina un nuevo desplazamiento d’, luego escribe este valor en el código en lugar de d. 10.3.4 HERENCIA MÚLTIPLE - La implementación de la herencia múltiple plantea problemas interesantes e impone una sobrecarga no despreciable. Los problemas son de 2 órdenes: por un lado, está el problema de identificar cómo es posible adaptar la técnica vtable para manejar las llamadas a métodos; por otro (y el problema es más interesante y también tiene un impacto sobre el lenguaje) está la necesidad de determinar qué hacer con los datos presentes en las superclases. Esto dará lugar a 2 interpretaciones diferentes de la herencia múltiple, a las que podemos referirnos como herencia replicada y compartida, abordaremos estos problemas sucesivamente. Estructura Vtable. Consideraremos el ejemplo de la figura siguiente. Está claro que no es posible organizar la representación de una instancia de C, ni una vtable para C de tal forma que la parte inicial coincida con la estructura correspondiente tanto en A como en B. (Un ejemplo de herencia múltiple) class A { int x; int f() { return x; } } class B { int y; int g() { return y; } int h() { return 2 * y; } } class C extending A,B { int z; int g() { return x + y + z; } int k() { return z; } } - Para representar una instancia de C, podemos comenzar con los campos de A y luego agregar los campos de B. Finalmente, necesitamos enumerar los campos específicos de C.(Representación de objetos y vtables para herencia múltiple)
  • 23. - Sabemos que, usando la relación de subtipo, podemos acceder a una instancia de C usando una referencia estática a cualquiera de los tres tipos A, B y C. Hay 2 casos distintos correspondientes a las 2 vistas diferentes de una instancia de C. Si se trata de un acceso con referencia estática de tipo C o de tipo A, la técnica descrita para herencia simple funciona perfectamente (excepto que las compensaciones estáticas de las variables de instancia reales en C deben tener en cuenta que las variables estáticas pertenecientes a B están en el medio). - Cuando, por el contrario, una instancia de C se ve como un objeto de B, es necesario tener en cuenta el hecho de que las variables de B no están al inicio del registro sino a una distancia, d, de su inicio que está determinado estáticamente. Cuando, por tanto, accedemos a una instancia de C a través de una referencia con tipo estático B, es necesario sumar la constante, d, a la referencia. - Surgen problemas similares para la estructura de la vtable. Una tabla para C se divide en dos partes distintas: la primera contiene los métodos de A (posiblemente como redefiniciones) y los métodos que realmente pertenecen a C. Una segunda parte comprende los métodos de B, posiblemente con sus redefiniciones. En la representación de un objeto de instancia de C, hay dos punteros a vtable. Correspondiente a la "vista como A y C", tendrá punteros a la vtable con métodos de A y C. Correspondiente a la "vista como B", tendrá punteros a la vtable con los métodos de B (tenga en cuenta que esto es una vtable para la clase C; la clase B tiene otra vtable, que se usa para sus propias instancias. - En general, cada superclase de una clase bajo herencia múltiple tiene su propia parte especial en las vtables de sus subclases). La invocación de un método que pertenece a C, o que fue redefinido o heredado de A, sigue las mismas reglas que la herencia simple. Para invocar un método heredado (o redefinido) de B, el compilador debe tener en cuenta que el puntero a la vtable para este método no reside al principio del objeto sino que está desplazado por d posiciones. En el ejemplo de la figura, para llamar al método h de una instancia de C visto como un objeto de tipo B (del cual, por lo tanto, tenemos un nombre estático, pb): se agrega d a la referencia pb; utilizando un acceso indirecto, se obtiene la dirección de inicio de la segunda vtable (la de B), luego se usa el método de invocación de desplazamiento estático apropiado h. Esta llamada necesita una operación adicional (la primera adición) además de la requerida para la herencia simple. - Sin embargo, hemos descuidado la vinculación para this. ¿Qué referencia de objeto actual deberíamos pasar a los métodos de C? Si se trata de métodos en la primera vtable (al que se accede mediante un this que apunta al inicio del objeto, es decir con la “vista A y B”), es necesario pasar el valor actual de this. Sin embargo, esto sería incorrecto para los métodos en la segunda vtable (al que se accede usando el puntero this más el desplazamiento d). Debemos distinguir dos casos: ● El método se hereda de B (el caso de h en la figura). En tal caso, es necesario pasar la vista del objeto a través del cual hemos encontrado la vtable. ● El método se redefine en C (el caso de g). En este caso, el método puede referirse a variables de instancia de A, por lo que es necesario pasarle una vista de la superclase.
  • 24. - La situación es incómoda porque la búsqueda de métodos dinámicos requiere que esta corrección del valor de this se realice en tiempo de ejecución. La solución más sencilla es almacenar esta corrección en la vtable, junto con el nombre del método. Cuando se invoca el método, esta corrección se sumará al valor actual de this. En nuestro ejemplo, las correcciones se muestran en la figura (Representación de objetos y vtables para herencia múltiple) junto al nombre del método asociado. La corrección se agrega a la vista del objeto a través del cual se encontró la vtable. - En general, si tenemos una referencia, pa, a una vista de la clase C de algún objeto, podemos compilar una llamada al método h (que podemos asumir que es el n-ésimo en la vtable de B) de la siguiente manera (la dirección de un método y la corrección cada uno ocupa w bytes): R1 := pa // vista A R1 := R1 + d // vista B R2 := *(R1) // vtable de B R3 := *(R2 + (n − 1) × 2 × w) // dirección de h R2 := *(R2 + (n − 1) × 2 × w + w) // corrección this := R1 + R2 call *(R3) // llamada a h - Tenemos tres instrucciones y un acceso indirecto además de la secuencia para llamar a un método usando herencia única. Herencia múltiple replicada. La subsección anterior se ha ocupado del caso en el que una clase hereda de 2 superclases. Estas superclases, sin embargo, pueden heredar por sí mismas de una superclase común, produciendo un diamante, como discutimos en la Sección. 10.2.5: class Top { int w; int f() { return w; } } class A extending Top { int x; int g() { return w+x; } } class B extending Top { int y; int f() { return w+y; } int k() { return y; } } class Bottom extending A,B { int z; int h() { return z; } }
  • 25. - Las instancias y la vtable para A tienen una parte inicial que es una copia de la estructura correspondiente a Top. Lo mismo ocurre con las instancias y la vtable replicada para B. Bajo la herencia múltiple replicada, Bottom se construye utilizando el enfoque que ya hemos discutido y, por lo tanto, consta de dos copias de las variables de instancia y los métodos de Top, como se muestra en la figura siguiente (Implementación de herencia múltiple replicada). - La implementación no plantea otros problemas además de los ya discutidos. Los conflictos de nombres deben resolverse explícitamente (en particular, no será posible invocar el método f de Top en un objeto de clase Bottom, ni asignar una instancia de Bottom a una referencia estática de tipo Top, porque no sabríamos cuál de los dos copias de Top elegir). Herencia múltiple compartida. La herencia múltiple replicada no siempre es la solución conceptual que un ingeniero de software tiene en mente al diseñar una situación de diamante. Cuando la clase en la parte inferior del diagrama contiene una única copia de la clase en la parte superior, hablamos de herencia múltiple replicada. En tal caso, tanto A como B poseen su propia copia de Top, pero Bottom solo tiene una copia. - C++ permite la herencia compartida usando clases de base virtual. Cuando una clase se define como virtual, todas sus subclases siempre contienen una sola copia de la misma, incluso si hay más de una ruta de herencia, como ocurre en el caso del diamante. Con este mecanismo, una clase se declara virtual o no virtual de una vez por todas. Si tenemos una clase no virtual y, más tarde, descubrimos que necesitamos herencia con el intercambio, no hay nada más que hacer que reescribir la clase y recompilar todo el sistema. Peor aún, una clase es virtual para todas sus subclases, incluso si en algunos casos deseamos que sea virtual para algunos y no virtual (que se replica) para otros. En tales casos, es necesario definir dos copias de la clase, una virtual y otra no virtual. - Con la herencia con el uso compartido suele haber un problema de conflicto de nombres. El problema se resuelve de forma arbitraria mediante diferentes lenguajes. En C++, por ejemplo, en el caso de un método de clase base virtual que se redefine en una subclase, se requiere que siempre haya una redefinición que domine a todas las demás en el sentido de que aparece en una clase que es una subclase de todas las demás clases donde se define este método. En nuestro ejemplo, para el método f, la redefinición dominante es la de la clase B. Es, por tanto, la
  • 26. heredada por Bottom. Si ambas clases A y B redefinen f, las definiciones de estas clases serían ilegales en C++ porque no habría una redefinición dominante. Otros lenguajes permiten que las clases heredadas elijan por qué camino se heredará el método; alternativamente, permiten una calificación completa de los nombres para elegir explícitamente el método deseado. Como es habitual, cuando se trata de herencia múltiple, no existe una solución elegante que sea claramente mejor que cualquier otra. - Ahora llegamos a la implementación de vtable para herencia múltiple compartida. - En la figura anterior, la implementación utilizada en C++ se muestra esquemáticamente. C++ es un lenguaje en el que todas sus subclases comparten una clase virtual. Dado que Bottom contiene una única copia de Top, ya no es posible representar subclase y superclase de forma contigua. A cada clase del diamante le corresponde una vista específica del objeto. En correspondencia con cada vista, hay un puntero a la vtable correspondiente y un puntero a la parte compartida del objeto. El acceso a las variables de instancia y métodos de Bottom, A y B es el mismo que en el caso de herencia múltiple sin compartir. Para acceder a las variables de instancia, o para invocar un método de Top, se requiere un acceso indirecto preliminar (supongamos que f es el método n de la vtable para Top y cada dirección ocupa w bytes): R1 := pa // vista Bottom R1 := *(R1 + w) // vista Top R2 := *(R1) // vtable de Top R3 := *(R2 + (n − 1) × 2 × w) // dirección de f R2 := *(R2 + (n − 1) × 2 × w + w) // corrección this := R1 + R2 call *(R3) // llamada a h - El valor necesario para corregir el valor de this es la diferencia entre la vista de la clase en la que se declara el método y la vista de la clase en la que se redefine. - Los lenguajes que permiten situaciones más elaboradas (por ejemplo, una sola superclase se replica en algunas clases y se comparte en otras) requieren técnicas más sofisticadas que no se pueden explicar en este libro.