Apuntes Java: Herencia – II

El modificador de acceso protected

El modificador de acceso protected proporciona una combinación de los accesos dados por los modificadores public y private. protected  proporciona acceso público para las clases derivadas y acceso privado (prohibido) para el resto de clases.

Por ejemplo, si en la clase Empleado definimos:

class Empleado {
    protected int sueldo;
    . . .
}

entonces desde la clase Ejecutivo se puede acceder al dato miembro sueldo, mientras que si se declara private no.

Up-casting y Down-casting

Siguiendo con el ejemplo de los apartados anteriores, dado que un Ejecutivo ES un Empleado se puede escribir la sentencia:

Empleado emp = new Ejecutivo("Máximo Dueño" , 2000);

Aquí se crea un objeto de la clase Ejecutivo que se asigna a una referencia de tipo Empleado. Esto es posible y no da error ni al compilar ni al ejecutar porque Ejecutivo es una clase derivada de Empleado. A esta operación en que un objeto de una clase derivada se asigna a una referencia cuyo tipo es alguna de las superclases se denomina 'upcasting'.

Cuando se realiza este tipo de operaciones, hay que tener cuidado porque para la referencia emp no existen los miembros de la clase Ejecutivo, aunque la referencia apunte a un objeto de este tipo. Así, las expresiones:

emp.aumentarSueldo(3);  // 1. ok. aumentarSueldo es de Empleado
emp.asignarPresupuesto(1500);  // 2. error de compilación

En la primera expresión no hay error porque el método aumentarSueldo está definido en la clase Empleado. En la segunda expresión se produce un error de compilación porque el método asignarPresupuesto no existe para la clase Empleado.

Por último, la situación para el método toString es algo más compleja. Si se invoca el método:

emp.toString();  // se invoca el metodo toString de Ejecutivo

el método que resultará llamado es el de la clase Ejecutivo. toString existe tanto para Empleado como para Ejecutivo, por lo que el compilador Java no determina en el momento de la compilación que método va a usarse. Sintácticamente la expresión es correcta. El compilador retrasa la decisión de invocar a un método o a otro al momento de la ejecución. Esta técnica se conoce con el nombre de dinamic binding o late binding. En el momento de la ejecución la JVM comprueba el contenido de la referencia emp. Si apunta a un objeto de la clase Empleado invocará al método toString de esta clase. Si apunta a un objeto Ejecutivo invocará por el contrario al método toString de Ejecutivo.

En nuestro ejemplo un Ejecutivo es un Empleado, pero lo contrario no es cierto; es decir, un Empleado no es un Ejecutivo, o mejor dicho, no siempre es un Ejecutivo. Por tanto, la siguiente expresión es errónea:

Ejecutivo ejecutivo = new Empleado("Armando Guerra", 800); // Error de compilación.

El down Casting, operación contraria del up-casting se explica en el apartado siguiente.

El Operador cast

Si se desea acceder a los métodos de la clase derivada teniendo una referencia de una clase base, como en el ejemplo del apartado anterior hay que convertir explicitamente la referencia de un tipo a otro. Esto se hace con el operador de cast de la siguiente forma:

Empleado emp = new Ejecutivo("Máximo Dueño" , 2000);
Ejecutivo ej = (Ejecutivo)emp; // se convierte la referencia de tipo
ej.asignarPresupuesto(1500); 

La expresión de la segunda línea convierte la referencia de tipo Empleado asignándola a una referencia de tipo Ejecutivo. Para el compilador es correcto porque Ejecutivo es una clase derivada de Empleado. En tiempo de ejecución la JVM convertirá la referencia si efectivamente emp apunta a un objeto de la clase Ejecutivo.  Si se intenta:

Empleado emp = new Empleado("Javier Todudas" , 2000);
Ejecutivo ej = (Ejecutivo)emp; 

no dará problemas al compilar, pero al ejecutar se producirá un error (ClassCastException) porque la referencia emp apunta a un objeto de clase Empleado y no a uno de clase Ejecutivo. Por este motivo la operación de cast tiene un cierto riesgo.

El operador instanceof

Hemos visto que una referencia puede apuntar a un objeto que no sea exactamente del mismo tipo que la propia referencia. En el apartado anterior veiamos que el objeto emp, de tipo Empleado, en realidad apunta a un objeto de tipo Ejecutivo.

Si en un momento dados necesitamos conocer que tipo de objeto contiene una referencia se puede usar el operador instanceof, de la siguiente forma:

Empleado emp = new Ejecutivo("Máximo Dueño" , 2000);
. . .
if (emp instanceof Empleado) 
   System.out.println("Es un empleado");
if (emp instanceof Ejecutivo)
   System.out.println("Es un ejecutivo");

La expresión emp instanceof Ejecutivo devuelve true si la instancia de emp es de tipo Ejecutivo, y false en caso contrario.

La clase Object

En Java existe una clase base que es la raíz de la jerarquía y de la cual heredan todas aunque no se diga explícitamente mediante la clausula extends. Esta clase base se llama Object y contiene algunos métodos básicos. La mayor parte de ellos no hacen nada pero pueden ser redefinidos por las clases derivadas para implementar comportamientos específicos. Los métodos declarados por la clase Object son los siguientes:

public class Object { 
    public final Class getClass() { . . . } 
    public String toString() { . . . } 
    public boolean equals(Object obj) { . . . } 
    public int hashCode() { . . . } 
    protected Object clone() throws CloneNotSupportedException { . . . } 
    public final void wait() throws IllegalMonitorStateException,
                                      InterruptedException { . . . } 
    public final void wait(long millis) throws IllegalMonitorStateException, 
                                                  InterruptedException {. . .}
    public final void wait(long millis, int nanos) throws
                                             IllegalMonitorStateException, 
                                             InterruptedException { . . . } 
    public final void notify() throws IllegalMonitorStateException { . . . }
    public final void notifyAll() throws    
                                    IllegalMonitorStateException { . . . } 
    protected void finalize() throws Throwable { . . . }     
}

Las cláusulas final y throws se verán más adelante. Como puede verse toString es un método de Object, que puede ser redefinido en las clases derivadas. Los métodos wait, notify y notifyAll tienen que ver con la gestión de threads de la JVM. El método finalize ya se ha comentado al hablar del recolector de basura.

Para una descripción exhaustiva de los métodos de Object se puede consultar la documentación de la API del JDK.

La cláusula final

En ocasiones es conveniente que un método no sea redefinido en una clase derivada o incluso que una clase completa no pueda ser extendida. Para esto está la cláusula final, que tiene significados levemente distintos según se aplique a un dato miembro, a un método o a una clase.

Para una clase, final significa que la clase no puede extenderse. Es, por tanto el punto final de la cadena de clases derivadas. Por ejemplo si se quisiera impedir la extensión de la clase Ejecutivo, se pondría:

final class Ejecutivo {
. . .
}

Para un método, final significa que no puede redefinirse en una clase derivada. Por ejemplo si declaramos:

class Empleado {
    . . . 
    public final void aumentarSueldo(int porcentaje) {
        . . .
    } 
    . . .
}

entonces la clase Ejecutivo, clase derivada de Empleado no podría reescribir el método aumentarSueldo, y por tanto cambiar su comportamiento.

Para un dato miembro, final significa también que no puede ser redefinido en una clase derivada, como para los métodos, pero además significa que su valor no puede ser cambiado en ningún sitio; es decir el modificador final sirve también para definir valores constantes. Por ejemplo:

class Circulo {
    . . .
    public final static float PI = 3.141592;
    . . .
}

En el ejemplo se define el valor de PI como de tipo float, estático (es igual para todas las instancias), constante (modificador final) y de acceso público.

Herencia simple

Java incorpora un mecanismo de herencia simple. Es decir, una clase sólo puede tener una superclase directa de la cual hereda todos los datos y métodos. Puede existir una cadena de clases derivadas en que la clase A herede de B y B herede de C, pero no es posible escribir algo como:

class A extends B , C ....  // error

Este mecanismo de herencia múltiple no existe en Java.

Java implanta otro mecanismo que resulta parecido al de herencia múltiple que es el de las interfaces que se verá más adelante.

Última actualización: 22/11/2016