Está en la página 1de 10

Herencia

La herencia es el mecanismo que permite derivar una o varias clases, denominadas subclases, de otra
más genérica llamada superclase. La superclase reune aquellos atributos y métodos comunes a una
serie de subclases, mientras que estas últimas ú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.

La herencia en Java se implementa simplemente especificando la palabra reservada extends en la


definición de la subclase:

1 class nombreSubclase extends nombreSuperclase {


2 // Atributos y metodos especificos de la subclase
3 }

Por ejemplo:

class Esfera {
1
2 private double radio;
3
4 public double setRadio( double r ) {
5 radio = r;
6 }
7
8 public double superficie() {
9 return 4*Math.PI*radio*radio;
10 }
11 }
12
13 class Planeta extends Esfera {
14
15 private int numSatelites;
16
17 public int setNumSatelites( int ns ) {
18 numSatelites = ns;
19 }
}

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:

1 Planeta tierra = new Planeta();


2 tierra.setRadio( 6378 ); // (Km.)
3 tierra.setNumSatelites( 1 );
4 System.out.println("Superficie = " + tierra.superficie());

En el ejemplo anterior puede observarse cómo 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. Ese es uno de los
motivos por el cual 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.

Veamos un nuevo ejemplo sobre el uso de la herencia. Supongamos que queremos implementar 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:

1 class Nave {
2
3 private int posX, posY; // Posicion de la nave en la pantalla ´
4 private int municion;
5
6 Nave() {
7 posX = 400;
8 posY = 50;
9 municion = 100;
10 }
11
12 void moverDerecha(int dist) {
13 posX += dist;
14 }
15
16 void moverIzquierda(int dist) {
17 posX -= dist;
18 }
19
20 void disparar() {
21 if (municion > 0) {
22 municion--;
23 // Codigo necesario para realizar disparo ´
24 }
25 }
26
27 void dibujar() {
28 // Obviaremos la parte grafica y nos limitaremos a mostrar ´
29 // un texto por la consola
30 System.out.println("Nave en " + posX + "," + posY);
31 System.out.println("Municion restante: " + municion);
32 }
33 }

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.

1 class NaveConEscudo extends Nave {


2
3 private boolean escudo;
4
5 NaveConEscudo() {
6 escudo = false;
7 }
8
9 void activarEscudo() {
10 escudo = true;
11 }
12
13 void desactivarEscudo() {
14 escudo = false;
15 }
16 }

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

1 class Juego {
2
3 public static void main(String[] args) {
4 NaveConEscudo miNave = new NaveConEscudo();
5 miNave.moverDerecha(20);
6 miNave.disparar();
7 miNave.activarEscudo();
8 miNave.dibujar();
9 }
10 }

La salida del programa anterior sería:

Nave en posicion 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 sólo introduce nuevas prestaciones respecto de la clase Nave, sino que además
debe modificar parte del comportamiento ya establecido.

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 múltiple en el que la clase C hereda de la clase A y de la clase
B, implicaríaa que la clase C heredaríaa 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.
Super
Una subclase puede referirse a la superclase inmediata mediante el uso de la palabra reservada super.
Esta palabra reservada puede utilizarse de dos formas:

• Para llamar al constructor de la superclase:

super( lista_de_argumentos_del_constructor );

• 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á ocultado. En este caso sería posible invocarlo mediante el uso de super:

super.atributo = . . . ;
super.metodo( lista_de_argumentos );

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.
Cuando existe una relación de herencia entre diversas clases y se crea un objeto de una subclase S, se
ejecuta no sólo el constructor de S sino también el de todas las superclases de S. Para ello se ejecuta en
primer lugar el constructor de la clase que ocupa el nivel más alto en la jerarquía de herencia y se
continúa de forma ordenada con el resto de las subclases

El siguiente ejemplo ilustra este comportamiento:


1 class A {
2
3 A() {
4 System.out.println("En A");
5 }
6 }
7
8 class B extends A {
9
10 B() {
11 System.out.println("En B");
12 }
13 }
14
15 class C extends B {
16
17 C() {
18 System.out.println("En C");
19 }
20 }
21
22 class Constructores_y_Herencia {
23
24 public static void main(String[] args) {
25 C obj = new C();
26 }
27 }

La salida de este programa sería:

En A
En B
En C

Es posible que una misma clase tenga más de un constructor (sobrecarga del constructor), tal y como se
muestra en el siguiente ejemplo:

1 class A {
2
3 A() {
4 System.out.println("En A");
5 }
6
7 A(int i) {
8 System.out.println("En A(i)");
9 }
10 }
11
12 class B extends A {
13
14 B() {
15 System.out.println("En B");
16 }
17
18 B(int j) {
19 System.out.println("En B(j)");
20 }
21 }

La cuestión que se plantea es: ¿qué constructores se invocarán 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ámetro,
parece claro que en la clase B se ejecutará el constructor B(int j). Sin embargo, puede haber confusión
acerca de qué constructor se ejecutará en A. La regla en este sentido es clara: mientras no se diga
explícitamente lo contrario, en la superclase se ejecutará siempre el constructor sin parámetros. 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 sentencia super(). Esta
sentencia invoca uno de los constructores de la superclase, el cual se escogerá en función de los
parámetros que contenga super().

En el siguiente ejemplo se especifica de forma explícita el constructor que deseamos ejecutar en A:

1 class A {
2
3 A() {
4 System.out.println("En A");
5 }
6
7 A(int i) {
8 System.out.println("En A(i)");
9 }
10 }
11
12 class B extends A {
13
14 B() {
15 System.out.println("En B");
16 }
17
18 B(int j) {
19 super(j); // Ejecutar en la superclase un constructor que acepte un entero
20 System.out.println("En B(j)");
21 }
22
}
23

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(), ésta deberá ser obligatoriamente la
primera sentencia del constructor. Esto es así porque se debe respetar el orden de ejecución de los
constructores comentado anteriormente.

En resumen, cuando se crea una instancia de una clase, para determinar el constructor que debe
ejecutarse en cada una de las superclases, en primer lugar se exploran los constructores en orden
jerárquico 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ún
constructor no aparece explícitamente una llamada a super(lista_de_argumentos) se entiende que de
forma implícita se está invocando a super() (constructor sin parámetros de la superclase). Finalmente,
una vez se conoce el constructor que debe ejecutarse en cada una de las clases que componen la
jerarquía, éstos se ejecutan en orden jerárquico descendente (desde la superclase hacia las subclases).

Sobreescritura de métodos
La herencia permite crear subclases que contienen todos los atributos y métodos privados de la clase
superior. Habitualmente en la subclase se definirán, además, atributos y métodos propios que no
existían en la superclase. En ocasiones, además interesa que la subclase modifique alguno de los
métodos heredados para que tengan un comportamiento distinto. Esto se realiza mediante la
sobreescritura de metodos. Simplemente consiste en implementar en la subclase un método que ya
existía en la superclase.

1 class Esfera {
2
3 double radio;
4
5 void mostrarValores() {
6 System.out.println("Radio = " + radio);
7 }
8 }
9
10 class Planeta extends Esfera {
11
12 int numSatelites;
13
14 void mostrarValores() {
15 System.out.println("Radio = " + radio + " - Num. Sat.= " + numSatelites);
16 }
17 }

En este ejemplo la clase Planeta hereda el método mostrarValores() de Esfera, pero luego lo
sobreescribe para poder mostrar tanto el radio como el número de satélites. En esta situación el método
mostrarValores ejecutará un código distinto dependiendo del objeto sobre el que se invoque.

public class EjemploSobreescritura {


1
2 public static void main(String[] args) {
3 Esfera e = new Esfera();
4 e.radio = 10;
5
6 Planeta p = new Planeta();
7 p.radio = 6378;
8 p.numSatelites = 1;
9
10 e.mostrarValores(); // Muestra los valores de la esfera
11 p.mostrarValores(); // Muestra los valores del planeta
12 }
}

El resultado de este programa sería:

Radio = 10
Radio = 6378 - Num. Sat.= 1

Supongamos que tenemos las siguientes clases:

1 class Nave {
2
3 int posX, posY;
4 int municion;
5
6 Nave() {
7 }
8
9 void moverDerecha(int dist) {
10 }
11
12 void moverIzquierda(int dist) {
13 }
14
15 void disparar() {
16 if (municion > 0) {
17 municion--;
18 }
19 }
20
21 void dibujar() {
22 }
23 }
24
25 class NaveConEscudo extends Nave {
26
27 boolean escudo;
28
29 NaveConEscudo() {
30 escudo = false;
31 }
32
33 void activarEscudo() {
34 escudo = true;
35 }
36
37 void desactivarEscudo() {
38 escudo = false;
39 }
40 }

En este ejemplo podría ser interesante que NaveConEscudo modificase el comportamiento de alguno de
los métodos heredados de Nave. Por ejemplo, que no pueda disparar si tiene el escudo activado. Para
ello, tal y como se ha visto, bastaría con sobreescribir el método correspondiente en la subclase:

1 class NaveConEscudo extends Nave {


2
3 boolean escudo;
4
5 NaveConEscudo() {
6 escudo = false;
7 }
8
9 void activarEscudo() {
10 escudo = true;
11 }
12
13 void desactivarEscudo() {
14 escudo = false;
15 }
16
17 // Sobreescribimos el metodo disparar ´
18 void disparar() {
19 if (escudo == false && municion > 0) {
20 municion--;
}
21
}
22
}

De este modo cada nave tiene su propia versión del método disparar. De forma similar podría
sobreescribirse el método dibujar para que cada tipo de nave tuviese una representación gráfica distinta.

La sobreescritura de un método podemos enfocarla desde dos puntos de vista: reescribir el método
completamente ignorando el código que hubiese en la superclase, o ampliar el código existente en la
superclase con nuevas instrucciones. En el primer caso hablaremos de reemplazo y en el segundo de
refinamiento.
Los ejemplos vistos anteriormente corresponden a sobreescritura mediante reemplazo. Para
implementar la sobreescritura mediante refinamiento se debe invocar desde el método sobreescrito de la
subclase el método correspondiente de la superclase mediante el uso de super. Por ejemplo podríamos
pensar que una NaveConEscudo se dibuja como una Nave con algunos elementos extras. En este
caso sería más interesante el refinamiento que el reemplazo, ya que aprovecharíaamos el código del
método dibuja() escrito para la clase Nave.

1
class Nave {
2
3
void dibujar() {
4
// Codigo para dibujar una nave b ´ asica ´
5
}
6
}
7
8
class NaveConEscudo extends Nave {
9
10
void dibujar() {
11
super.dibujar(); // Primero dibujamos una nave basica. ´
12
// Codigo para dibujar los elementos extra ´
13
}
14
}
15

También podría gustarte