viernes, 17 de junio de 2016

PROGRAMACION-IV

UNIDAD 4
Herencia y Polimorfismo
     Tal y como se ha visto en capítulos anteriores, una clase representa un conjunto de objetos que comparten una misma estructura y comportamiento. La estructura se determina mediante un conjunto de variables denominadas atributos, mientras que el comportamiento viene determinado por los métodos. Ya se ha visto en capítulos anteriores que esta forma de estructurar los programas ofrece grandes ventajas, pero lo que realmente distingue la programación orientada a objetos (POO) de otros paradigmas de programación es la capacidad que tiene la primera de definir clases que representen objetos que comparten solo una parte de su estructura y comportamiento, esto es, que representen objetos con ciertas similitudes, pero no iguales. Estas similitudes pueden expresarse mediante el uso de la herencia y el polimorfismo. Estos dos mecanismos son, sin lugar a dudas, pilares básicos de la POO.

4.1. Herencia
   La herencia es el mecanismo que permite derivar una o varias clases, denominadas subclases, de otra más genérica denominada superclase. La superclase reúne aquellos atributos y métodos comunes a una serie de subclases, mientras que estas ´ultimas ´únicamente definen aquellos atributos y métodos propios que no hayan sido definidos en la superclase. Se dice que una subclase extiende el comportamiento de la superclase. En ocasiones se utiliza el término clase base para referirse a la superclase y clase derivada para referirse a la subclase.
En la figura 4.1 se representa gráficamente el concepto de herencia.


4.1.1.   Conceptos básicos
La herencia en Java se implementa simplemente especificando la palabra reservada extends en la definición de la subclase:

class nombreSubclase extends nombreSuperclase {
// Atributos y métodos específicos de la subclase
}
Por ejemplo:
class Esfera {
private double radio;
public double setRadio( double r ) { radio = r; }
}
public double superficie() {
return 4*Math.PI*radio*radio;
}
class Planeta extends Esfera {
private int numSatelites;
}
public int setNumSatelites( int ns ) { numSatelites = ns;
}

En este ejemplo la clase Planeta hereda todos los métodos y atributos definidos en Esfera que no sean privados. Al fin y al cabo, un planeta también es una esfera (despreciaremos las pequeñas deformaciones que pueda tener). En el siguiente fragmento de código vemos cómo es posible acceder a los atributos y métodos heredados.

Planeta tierra = new Planeta();
tierra.setRadio(6378); // (Km.)
tierra.setNumSatelites( 1 );
System.out.println("Superficie = " + tierra.superficie());

En el ejemplo anterior puede observarse como los objetos de la clase Planeta utilizan los métodos heredados de Esfera (setRadio y superficie) exactamente del mismo modo a como utiliza los definidos en su misma clase (setNumSatelites).
Cuando se define un esquema de herencia, la subclase hereda todos los miembros (métodos y atributos) de la superclase que no hayan sido definidos como privados. Debe observarse que en el ejemplo anterior la clase Planeta no hereda el atributo radio. Sin embargo, esto no quiere decir que los planetas no tengan un radio asociado, sino que no es posible acceder directamente a ese atributo. En realidad los objetos de tipo Planeta pueden establecer su radio a través del método setRadio. Debe remarcarse que es una práctica muy habitual definir como privados los atributos de una clase e incluir métodos públicos que den acceso a dichos atributos, tal y como se explicó en el apartado 3.4.
Veamos un nuevo ejemplo sobre el uso de la herencia. Supongamos que queremos imple-mentar un juego en el que una nave debe disparar a ciertas amenazas que se le aproximan.

De forma muy simplista, podríamos definir el comportamiento de nuestra nave mediante el Siguiente código:

class Nave {
private int posX, posY;      // Posición de la nave en la pantalla
private int municion;
Nave() {
posX = 400;
posY = 50;
municion = 100;
}
void moverDerecha( int dist ) {
posX += dist;
}
void moverIzquierda( int dist ) {
posX -= dist;
}
void disparar() {
if( municion > 0 ) {
municion--;
// Codigo necesario para realizar disparo
}
}
void dibujar() {
// Obviaremos la parte gr´afica y nos limitaremos a mostrar
// un texto por la consola
System.out.println("Nave en " + posX + "," + posY);
System.out.println("Munici´on restante: " + municion);
}
}

Supongamos ahora que tras superar cierto nivel en el juego, podemos disponer de otra nave con mayores prestaciones (por ejemplo, un escudo protector). En este caso podemos tomar como base la nave anterior e implementar una segunda clase que contenga únicamente la nueva funcionalidad.

Ahora sería posible hacer uso de esta nueva nave en nuestro juego.

class NaveConEscudo extends Nave {
private boolean escudo;
NaveConEscudo() {
escudo = false;
}
void activarEscudo() {
escudo = true;
}
void desactivarEscudo() {
escudo = false;
}
}

Ahora sería posible hacer uso de esta nueva nave en nuestro juego.

class Juego {
public static void main(String[] args) {
NaveConEscudo miNave = new NaveConEscudo();
miNave.moverDerecha(20);
miNave.disparar();
miNave.activarEscudo();
miNave.dibujar ();
}
}
La salida del programa anterior ser´ıa:
Nave en posici´n 420 50
Municion restante: 99

El aspecto interesante del ejemplo anterior es observar que la subclase NaveConEscudo incluye todos los elementos de la clase Nave tal y como si hubiesen sido definidos en la propia clase NaveConEscudo. Esta característica permite definir con un mínimo esfuerzo nuevas clases basadas en otras ya implementadas, lo que redunda en la reutilización de código. Además, si en el uso de la herencia nos basamos en clases cuyo código ya ha sido verificado y el cual se supone exento de errores, entonces será más fácil crear programas robustos, ya que únicamente deberemos preocuparnos por validar la nueva funcionalidad añadida en la subclase.
Por supuesto sería posible crear objetos de tipo Nave en lugar de NaveConEscudo, pero en este caso no se podría ejecutar el método nave.activarEscudo(). En el ejemplo mostrado, los objetos de la clase NaveConEscudo disparan y se dibujan exactamente igual a como lo hacen los objetos de la clase Nave, ya que estos métodos han sido heredados. Podríamos pensar que a nuestra nueva nave no se le permita disparar cuando tiene activado el escudo, o que se dibuje de forma distinta a como lo hac´ıa la nave básica. Ello implica que la clase NaveConEscudo no solo introduce nuevas prestaciones respecto de la clase Nave, sino que además debe modificar parte del comportamiento ya establecido. En el apartado 4.2 veremos qué mecanismos nos permiten abordar este problema.
Hay que mencionar que en Java no existe la herencia múltiple debido a los problemas que ello genera. Si se permitiera un esquema de herencia como el mostrado en la figura 4.2 implicaría que la clase C heredaría tanto los métodos definidos en A como los definidos en B.


Podría ocurrir que tanto A como B contuvieran un método con el mismo nombre y parámetros pero distinta implementación (distinto código), lo que provocaría un conflicto acerca de cuál de las dos implementaciones heredar.



Figura 4.2: La herencia m´ultiple NO est´a permitida en Java.

Para acceder a un miembro de la superclase que ha sido ocultado. Por ejemplo, si
declaramos en la subclase un miembro con el mismo nombre que tiene en la superclase,
el miembro de la superclase quedar´a ocultado. En este caso ser´ıa posible invocarlo
mediante el uso de super.

super.atributo = . . .    ;
super.metodo( lista_de_argumentos );
Estos dos modos de usar la palabra super se ver´an con m´as detalle en los apartados 4.1.3
y 4.2.1 respectivamente.

4.1.3.    Constructores y herencia
A diferencia de lo que ocurre con los métodos y atributos no privados, los constructores no se heredan. Además de esta característica, deben tenerse en cuenta algunos aspectos sobre el comportamiento de los constructores dentro del contexto de la herencia, ya que dicho comportamiento es sensiblemente distinto al del resto de métodos.
Orden de ejecución de los constructores
Cuando existe una relaci´on de herencia entre diversas clases y se crea un objeto de una
subclase S, se ejecuta no s´olo el constructor de S sino tambi´en el de todas las superclases de
S. Para ello se ejecuta en primer lugar el constructor de la clase que ocupa el nivel m´as alto
en la jerarqu´ıa de herencia y se contin´ua de forma ordenada con el resto de las subclases.
El siguiente ejemplo ilustra este comportamiento:

class A {
A() { System.out.println("En A"); }
}
class B extends A {
B() { System.out.println("En B"); }
}
class C extends B {
C() { System.out.println("En C"); }
}
class Constructores_y_Herencia {
public static void main(String[] args) {
C obj = new C();
}
}
La salida de este programa ser´ıa:
En A
En B
En C
Para ser m´as preciso, tal y como se ver´a m´as adelante, en primer lugar se examinan los constructores
comenzando por las subclases que ocupan un nivel jer´arquico m´as bajo. Con ello se determina el constructor
que debe ejecutarse en la superclase inmediata (en caso de que haya m´as de un constructor en dicha super-
clase). Finalmente, una vez decidido qu´e constructor debe utilizarse en cada clase, se ejecutan comenzando
por la clase de mayor nivel jer´arquico.

¿Qu´e constructor se ejecuta en la superclase? Uso de super()
Como ya se ha visto en cap´ıtulos anteriores, es posible que una misma clase tenga m´as de
un constructor (sobrecarga del constructor), tal y como se muestra en el siguiente ejemplo:

class A {
A() { System.out.println("En A"); }
A(int i) { System.out.println("En A(i)"); }
}
class B extends A {
B() { System.out.println("En B"); }
B(int j) { System.out.println("En B(j)"); }
}
La cuesti´on que se plantea es: ¿qu´e constructores se invocar´an cuando se ejecuta la
sentencia B obj = new B(5); ? Puesto que hemos creado un objeto de tipo B al que le
pasamos un entero como par´ametro, parece claro que en la clase B se ejecutar´a el constructor
B(int j). Sin embargo, puede haber confusi´on acerca de qu´e constructor se ejecutar´a en
A. La regla en este sentido es clara: mientras no se diga expl´ıcitamente lo contrario, en la
superclase se ejecutar´a siempre el constructor sin par´ametros. Por tanto, ante la sentencia
B obj = new B(5), se mostrar´ıa:
En A
En B(j)
Hay, sin embargo, un modo de cambiar este comportamiento por defecto para permitir
ejecutar en la superclase un constructor diferente. Para ello debemos hacer uso de la sen-
tencia super(). Esta sentencia invoca uno de los constructores de la superclase, el cual se
escoger´a en funci´on de los par´ametros que contenga super().
En el siguiente ejemplo se especifica de forma expl´ıcita el constructor que deseamos
ejecutar en A:
class A {
A() {
System.out.println("En A");
}
A(int i) {
System.out.println("En A(i)");
}
}
class B extends A {
B() {
System.out.println("En B");
}
B(int j) {
super(j); // Ejecutar en la superclase un constructor
// que acepte un entero
System.out.println("En B(j)");
}
}

 En este caso la sentencia B obj = new B(5) mostrar´ıa:
En A(i)
En B(j)
Mientras que la sentencia B obj = new B() mostrar´ıa:
En A()
En B()
Ha de tenerse en cuenta que en el caso de usar la sentencia super(), ´esta deber´a ser
obligatoriamente la primera sentencia del constructor. Esto es as´ı porque se debe respetar el
orden de ejecuci´on de los constructores comentado anteriormente.
En resumen, cuando se crea una instancia de una clase, para determinar el construc-
tor que debe ejecutarse en cada una de las superclases, en primer lugar se exploran los
constructores en orden jer´arquico ascendente (desde la subclase hacia las superclase). Con
este proceso se decide el constructor que debe ejecutarse en cada una de las clases que
componen la jerarqu´ıa. Si en alg´un constructor no aparece expl´ıcitamente una llamada a
super(lista_de_argumentos) se entiende que de forma impl´ıcita se est´a invocando a
super() (constructor sin par´ametros de la superclase). Finalmente, una vez se conoce el
constructor que debe ejecutarse en cada una de las clases que componen la jerarqu´ıa, ´estos
se ejecutan en orden jer´arquico descendente (desde la superclase hacia las subclases).

P´erdida del constructor por defecto
Podemos considerar que todas las clases en Java tienen de forma impl´ıcita un construc-
tor por defecto sin par´ametros y sin c´odigo. Ello permite crear objetos de dicha clase sin
necesidad de incluir expl´ıcitamente un constructor. Por ejemplo, dada la clase
class A {
int i;
}
no hay ning´un problema en crear un objeto
A obj = new A();
ya que, aunque no lo escribamos, la clase A lleva implıcito un constructor por defecto:
class A {
int i;
A(){} // Constructor por defecto
}
Sin embargo, es importante saber que dicho constructor por defecto se pierde si escribimos
cualquier otro constructor. Por ejemplo, dada la clase
class A {
int i;
A( int valor) {
i=valor;
}
}
La sentencia A obj = new A() generar´a un error de compilaci´on, ya que en este caso no
existe ning´un constructor en A sin par´ametros. Hemos perdido el constructor por defecto. Lo
correcto ser´ıa, por ejemplo, A obj = new A(5).
Esta situaci´on debe tenerse en cuenta igualmente cuando exista un esquema de herencia.
Imaginemos la siguiente jerarqu´ıa de clases:

class A {
int i;
A( int valor) {
i=valor;
}
}
class B extends A {
int j;
B( int valor) {
j=valor;
}
}



En este caso la sentencia B obj = new B(5) generar´a igualmente un error ya que, puesto
que no hemos especificado un comportamiento distinto mediante super(), en A deber´ıa
ejecutarse el constructor sin par´ametros, sin embargo, tal constructor no existe puesto que
se ha perdido el constructor por defecto. La soluci´on pasar´ıa por sobrecargar el constructor
de A anadiendo un constructor sin par´ametros,

class A {
int i;
A() {
i=0;
}
A( int valor) {
i=valor;
}
}
class B extends A {
int j;
B( int valor) {
j=valor;
}
}

o bien indicar expl´ıcitamente en el constructor de B que se desea ejecutar en A un constructor
que recibe un entero como par´ametro, tal y como se muestra a continuaci´on:

class A {
int i;
A( int valor) {
i=valor;
}
}
class B extends A {
int j;
B( int valor) {
super(0);
j=valor;
}
}

4.1.4.      Modificadores de acceso
Los modificadores de acceso public y private descritos en el apartado 3.4.1 del cap´ıtulo
anterior se utilizan para controlar el acceso a los miembros de una clase. Existe un tercer
modificador de acceso, protected que tiene sentido utilizar cuando entra en juego la heren-
cia. Cuando se declara un atributo o m´etodo protected, dicho elemento no ser´a visible desde
aquellas clases que no deriven de la clase donde fue definido y que, adem´as, se encuentren
en un paquete diferente.

En la tabla 4.1 se resume la visibilidad de los atributos y m´etodos en funci´on de sus
modificadores de acceso.



private
Sin modificador
(friendly)
protected
public
Misma clase
S´ı
S´ı
S´ı
S´ı
Subclase del mismo paquete
No
S´ı
S´ı
S´ı
No subclase del mismo paquete
No
S´ı
S´ı
S´ı
Subclase de diferente paquete
No
No
S´ı
S´ı
No subclase de diferente paquete
No
No
No
S´ı











 Tabla 4.1: Acceso a miembros de una clase.

4.1.5.         La clase Object
´
 
La clase Object es una clase especial definida por Java. Object es la clase base de todas
las dem´as clases, de modo que cualquier clase en Java deriva directa o indirectamente de
´esta, sin necesidad de indicarlo expl´ıcitamente mediante la palabra extends. Esto significa
que en Java existe un unico ´arbol jer´arquico de clases en el que est´an incluidas todas, donde
la clase Object figura en la ra´ız de dicho ´arbol.
A continuaci´on se muestra un ejemplo en el que se observa que las clases que no llevan la
cl´ausula extends heredan impl´ıcitamente de Object. La figura 4.3 muestra el esquema de
herencia de este ejemplo.
class A { // Al no poner extends, deriva impl´ıcitamente de Object
}
class B extends A { // Deriva de A y, por tanto, de Object
}
En la clase Object existen varios m´etodos definidos, entre los cuales figuran:
boolean equals(Object o): compara dos objetos para ver si son equivalentes y devuelve
un valor booleano.
String toString(): devuelve una cadena de tipo String que conteniene una descripci´on
del objeto. Este m´etodo se llama impl´ıcitamente cuando se trata de imprimir un objeto
con println() o cuando se opera con el objeto en una “suma” (concatenaci´on) de
cadenas.
void finalize(): este m´etodo es invocado autom´aticamente por el garbage collector cuan-
do se determina que ya no existen m´as referencias al objeto.
Para que estos m´etodos sean de utilidad, las subclases de Object deber´an sobreescribirlos,
tal y como se explica en el apartado 4.2.7.


    
Figura 4.3: En Java existe un unico ´arbol jerarquico, en el que la clase Object ocupa el nivel mas alto.

No hay comentarios:

Publicar un comentario