Threads en Java (Parte II)

Threads y prioridades

Aunque un programa utilice varios threads y aparentemente estos se ejecuten simultaneamente, el sistema ejecuta una sóla instrucción cada vez (esto es particularmente cierto en sistemas con una sola CPU), aunque realizado a velocidad suficiente para proporcionar la ilusión de simultaneidad. El mecanismo por el cual un sistema controla la ejecución concurrente de procesos se llama planificación (scheduling). Java soporta un mecanismo simple denominado planificacion por prioridad fija (fixed priority scheduling). Esto significa que la planificación de los threads se realiza en base a la prioridad relativa de un thread frente a las prioridades de otros.

La prioridad de un thread es un valor entero (cuanto mayor es el número, mayor es la prioridad), que puede asignarse con el método setPriority. Por defecto, la prioridad de un thread es igual a la del thread que lo creó. Cuando hay varios threads en condiciones de ser ejecutados (estado runnable), la máquina virtual elige el thread que tiene una prioridad más alta, que se ejecutará hasta que:
  • Un thread con una prioridad más alta esté en condiciones de ser ejecutado (runnable), o
  • El thread termina (termina su metodo run), o
  • Se detiene voluntariamente o
  • Alguna condición hace que el thread no sea ejecutable (runnable), como una operación de entrada/salida o, si el sistema operativo tiene planificación por división de tiempos (time slicing), cuando expira el tiempo asignado.
Si dos o más threads están listos para ejecutarse y tienen la misma prioridad, la máquina virtual va cediendo control de forma cíclica (round-robin).

El hecho de que un thread con una prioridad más alta interrumpa a otro se denomina se denomina "planificación con derecho preferente" (preemptive scheduling).

Cuando un thread entra en ejecución y no cede voluntariamente el control para que puedan ejecutarse otros threads, se dice que es un thread egoista (selfish thread). Algunos Sistemas Operativos, como Windows, combaten estas actitudes con una estrategia de planificación por división de tiempos (time-slicing), que opera con threads de igual prioridad que compiten por la CPU. En estas condiciones el Sistema Operativo asigna tiempos a cada thread y va cediendo el control consecutivamente a todos los que compiten por el control de la CPU, impidiendo que uno de ellos se apropie del sistema durante un intervalo de tiempo prolongado.

Este mecanismo lo proporciona el sistema operativo, no Java.

Sincronización de Threads

El ejemplo del capítulo anterior muestra un programa que ejecuta varios threads de forma asíncrona. Es decir, una vez que es iniciado, cada thread vive de forma independiente de los otros, no existe ninguna relación entre ellos, ni tampoco ningún conflicto, dado que no comparten nada. Sin embargo, hay ocasiones que distintos threads en un programa si necesitan establecer alguna relación entre sí o compartir objetos. Se necesita entonces algún mecanismo que permita sincronizar threads así como establecer unas 'reglas del juego' para acceder a recursos (objetos) compartidos.

Un ejemplo típico en que dos procesos necesitan sincronizarse, es el caso en que un thread produzca algún tipo de información que es procesada por otro thread. Al primer thread le denominaremos Productor y al segundo Consumidor. El Productor podría tener el siguiente aspecto:
public class Productor extends Thread {
    private Contenedor contenedor;

    public Productor (Contenedor c) {
        contenedor = c;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            contenedor.put(i);
            System.out.println("Productor. put: " + i);
            try {
                sleep((int)(Math.random() * 100));
            } catch (InterruptedException e) { }
        }
    }
}
Productor tiene una variables miembro: contenedor es una referencia a un objeto Contenedor, que sirve para almacenar los datos que va produciendo. El método run genera aleatoriamente el dato y lo coloca en el contenedor con el método put. Después espera una cantidad de tiempo aleatoria (hasta 100 milisegundos) con el método sleep. Productor no se preocupa de si el dato ya ha sido consumido o no. Simplemente lo coloca en el contenedor.

El Consumidor, por su parte podría tener el siguiente aspecto:
public class Consumidor extends Thread {
    private Contenedor contenedor;

    public Consumidor (Contenedor c) {
        contenedor = c;
    }

    public void run() {
        int value = 0;
        for (int i = 0; i < 10; i++) {
            value = contenedor.get();
            System.out.println("Consumidor. get: " + value);
        }
    }
}
El constructor es equivalente al del Productor. El método run, simplemente recupera el dato del contenedor con el método get y lo muestra en la consola. Tampoco el consumidor se preocupa de si el dato está ya disponible en el contenedor o no.

Productor y Consumidor se usarían desde un método main de la siguiente forma:
public class ProducTorConsumidorTest {
    public static void main(String[] args) {
        Contenedor c = new Contenedor ();
        Productor produce = new Productor (c);
        Consumidor consume = new Consumidor (c);

        produce.start();
        consume.start();
    }
}
Simplemente se crean los objetos, contendor, productor y consumidor y se inician los threads de estos dos últimos.

La sincronización que permite a productor y consumidor operar correctamente; es decir, que hace que consumidor espere hasta que haya un dato disponible, y que productor no genere uno nuevo hasta que haya sido consumido esta en la clase Contenedor, que tiene el siguiente aspecto:
public class CubbyHole {
    private int dato;
    private boolean hayDato = false;

    public synchronized int get() {
        while (hayDato == false) {
            try {
                // espera a que el productor coloque un valor.
                wait(); }
            catch (InterruptedException e) { }
        }
        hayDato = false;
        // notificar que el valor ha sido consumido.
        notifyAll();
        return dato;
    }

    public synchronized void put(int valor) {
        while (hayDato == true) {
            try {
                // espera a que se consuma el dato.
                wait();
            } catch (InterruptedException e) { }
        }
        dato = valor;
        hayDato = true;
        // notificar que ya hay dato.
        notifyAll();
    }
}
La variable miembro dato es la que contiene el valor que se almacena con put y se devuelve con get. La variable miembro hayDato es un flag interno que indica si el objeto contiene dato o no.

En el método put, antes de almacenar el valor en dato hay que asegurarse de que el valor anterior ha sido consumido. Si todavía hay valor (hayDato es true) se suspende la ejecución del thread mediante el método wait. Invocando wait (que es un método de la clase Object) se suspende el thread indefinidamente hasta que alguien le envíe una "señal" con el método notify o notifyAll. Cuando esto se produce (veremos que el notify lo produce el método get) el método continua, asume que el dato ya se ha consumido, almacena el valor en dato y envia a su vez un notifyAll para notificar a su vez que hay un dato disponible.

Por su parte, el método get chequea si hay dato disponible (no lo hay si hayDato es false) y si no lo hay espera hasta que le avisen (método wait). Una vez ha sido notificado (desde el método put) cambia el flag y devuelve el dato, pero antes notifica a put de que el dato ya ha sido consumido, y por tanto se puede almacenar otro.

La sincronización se lleva a cabo usando los métodos wait y notifiAll.

Existe además otro componente básico en el ejemplo. Los objetos productor y consumidor utilizan un recurso compartido que es el objeto contenedor. Si mientras el productor llama al método put y este se encuentra cambiando las variables miembro dato y hayDato, el consumidor llamara al método get y este a su vez empezará a cambiar estos valores, que podrían producirse resultados inesperados (este ejemplo es sencillo pero fácilmente pueden imaginarse otras situaciones más complejas).

Interesa, por tanto que mientras se esté ejecutando el método put nadie más acceda a las variables miembro del objeto. Esto se consigue con la palabra synchronized en la declaración del método. Cuando la máquina virtual inicia la ejecución de un método con este modificador, adquiere un bloqueo en el objeto sobre el que se ejecuta el método que impide que nadie más inicie la ejecución en ese objeto de otro método, que también esté declarado como syncrhonized. En nuestro ejemplo, cuando comienza el método put se bloquea el objeto de tal forma que si alguien intenta invocar el método get o put (ambos son synchronized) quedará en espera hasta que el bloqueo se libere (cuando termine la ejecución del método). Este mecanismo garantiza que los objetos compartidos mantienen la consistencia.

Este método de gestionar los bloqueos implica que:
  • Es responsabilidad del programador pensar y gestionar los bloqueos (A veces es una pesada responsabilidad).
  • Los métodos synchronized son más costosos en el sentido de que adquirir y liberar los bloqueos consume tiempo (este es el motivo por el que no están sincronizados por defecto todos los métodos).
  • Conviene evitar en lo posible el uso de objetos compartidos. Resultan difíciles de manejar.

REFERENCIAS:       arrakis

Threads en Java (Parte I)

Qué es un Thread

La Máquina Virtual Java (JVM) es un sistema multi-thread. Es decir, es capaz de ejecutar varias secuencias de ejecución (programas) simultáneamente. La JVM gestiona todos los detalles, asignación de tiempos de ejecución, prioridades, etc., de forma similar a como gestiona un Sistema Operativo múltiples procesos. La diferencia básica entre un proceso de Sistema Operativo y un Thread en Java es que los Threads corren dentro de la JVM, que es un proceso del Sistema Operativo y por tanto comparten todos los recursos, incluida la memoria y las variables y objetos allí definidos. A este tipo de procesos donde se comparte los recursos se les llama a veces "procesos ligeros" (lightweight process).

Java da soporte al concepto de Thread desde el mismo lenguaje, con algunas clases e interfaces definidas en el package java.lang y con métodos específicos para la manipulación de Threads en la clase Object.

Desde el punto de vista de las aplicaciones los threads son útiles porque permiten que el flujo del programa sea divido en dos o más partes, cada una ocupándose de alguna tarea. Por ejemplo, un Thread puede encargarse de la comunicación con el usuario, mientras otros actuan en segundo plano, realizando la transmisión de un fichero, accediendo a recursos del sistema (cargar sonidos, leer ficheros ...), etc. De hecho, todos los programas con interface gráfico (AWT o Swing) son multithread porque los eventos y las rutinas de dibujado de las ventanas corren en un thread distinto al principal.

La Clase Thread

La forma más directa para hacer un programa multi-thread es extender la clase Thread, y redefinir el método run(). Este método es invocado cuando se inicia el thread (mediante una llamada al método start() de la clase thread). El thread se inicia con la llamada al método run y termina cuando éste termina. El siguiente ejemplo ilustra estas ideas:
public class ThreadEjemplo extends Thread {
    public ThreadEjemplo(String str) {
        super(str);
    }
    public void run() {
        for (int i = 0; i < 10 ; i++)
            System.out.println(i + " " + getName());
        System.out.println("Termina thread " + getName());
    }
    public static void main (String [] args) {
        new ThreadEjemplo("Pepe").start();
        new ThreadEjemplo("Juan").start();
        System.out.println("Termina thread main");
    }
}
Compile y ejecute el programa. La salida, será algo asi:
Termina thread main
0 Pepe
1 Pepe
2 Pepe
3 Pepe
0 Juan
4 Pepe
1 Juan
5 Pepe
2 Juan
6 Pepe
3 Juan
7 Pepe
4 Juan
8 Pepe
5 Juan
9 Pepe
6 Juan
Termina thread Pepe
7 Juan
8 Juan
9 Juan
Termina thread Juan
BUILD SUCCESSFUL (total time: 1 second)
Ejecute varias veces el programa. Verá que no siempre se ejecuta igual.

Notas sobre el programa:
  • La clase Thread está en el package java.lang. Por tanto, no es necesario el import.
  • El constructor public Thread(String str) recibie un parámetro que es la identificación del Thread.
  • El método run contiene el bloque de ejecución del Thread. Dentro de él, el método getName() devuelve el nombre del Thread (el que se ha pasado como argumento al constructor).
  • El método main crea dos objetos de clase ThreadEjemplo y los inicia con la llamada al método start(). (el cual inicia el nuevo thread y llama al método run()).
  • Observe en la salida el primer mensaje de finalización del thread main. La ejecución de los threads es asíncrona. Realiza la llamada al método start(), éste le devuelve control y continua su ejecución, independiente de los otros threads.
  • En la salida los mensajes de un thread y otro se van mezclando. La máquina virtual asigna tiempos a cada thread.

La Interface Runnable

La interface Runnable proporciona un método alternativo a la utilización de la clase Thread, para los casos en los que no es posible hacer que nuestra clase extienda la clase Thread. Esto ocurre cuando nuestra clase, que deseamos correr en un thread independiente, deba extender alguna otra clase. Dado que no existe herencia múltiple, nuestra clase no puede extender a la vez la clase Thread y otra más. En este caso, nuestra clase debe implantar la interface Runnable, variando ligeramente la forma en que se crean e inician los nuevos threads.

El siguiente ejemplo es equivalente al del apartado anterior, pero utilizando la interface Runnable:
public class ThreadEjemplo implements Runnable {
    public void run() {
        for (int i = 0; i < 5 ; i++)
            System.out.println(i + " " + Thread.currentThread().getName());
        System.out.println("Termina thread " + Thread.currentThread().getName());
    }
    public static void main (String [] args) {
        new Thread (new ThreadEjemplo() , "Pepe").start();
        new Thread (new ThreadEjemplo() , "Juan").start();
        System.out.println("Termina thread main");
    }
}
Observese en este caso que:
  • Se implanta la interface Runnable en lugar de extender la clase Thread.
  • El constructor que había antes no es necesario.
  • En el main, observe la forma en que se crea el thread. Esa expresión es equivalente a:
ThreadEjemplo ejemplo = new ThreadEjemplo();
Thread thread = new Thread ( ejemplo , "Pepe") ;
thread.start();
  • Primero se crea la instancia de nuestra clase.
  • Después se crea una instancia de la clase Thread, pasando como parámetros la referencia de nuestro objeto y el nombre del nuevo thread.
  • Por último, se llama al método start de la clase thread. Este método iniciará el nuevo thread y llamará al método run() de nuestra clase.
  • Por útlimo, obsérvese la llamada al método getName() desde run(). getName es un método de la clase Thread, por lo que nuestra clase debe obtener una referencia al thread propio. Es lo que hace el método estático currentThread() de la clase Thread.

El ciclo de vida de un Thread

El gráfico de la figura 1 resume el ciclo de vida de un thread en Java:

Figura 1. Ciclo de vida de un Thread en Java.

Cuando se instancia la clase Thread (o una subclase) se crea un nuevo Thread que está en su estado inicial ("New Thread" en el gráfico). En este estado es simplemente un objeto más. No existe todavía el thread en ejecución. El único método que puede invocarse sobre él es el método start.

Cuando se invoca el método start sobre el thread el sistema crea los recursos necesarios, lo planifica (le asigna prioridad) y llama al método run. En este momento el thread está corriendo.

Si el método run invoca internamente el método sleep o wait o el thread tiene que esperar por una operación de entrada/salida, entonces el thread pasa al estado "no runnable" (no ejecutable) hasta que la condición de espera finalice. Durante este tiempo el sistema puede ceder control a otros threads activos.

Por último, cuando el método run finaliza, el thread termina y pasa a la situación "Dead" (Muerto).

REFERENCIAS:       arrakis

Comentarios, documentación y convenciones de nombres en Java

Comentarios

En Java existen comentarios de línea con // y bloques de comentario que comienzan con /* y terminan con */. Por ejemplo:
// Comentario de una linea
/* comienzo de comentario
   continua comentario
   fin de comentario */

Comentarios para documentación

El JDK proporciona una herramienta para generar páginas HTML de documentación a partir de los comentarios incluidos en el código fuente. El nombre de la herramienta es javadoc. Para que javadoc pueda generar los textos HTML es necesario que se sigan unas normas de documentación en la fuente, que son las siguientes:
  • Los comentarios de documentación deben empezar con /** y terminar con */.
  • Se pueden incorporar comentarios de documentación a nivel de clase, a nivel de variable (dato miembro) y a nivel de método.
  • Se genera la documentación para miembros public y protected.
  • Se pueden usar tags para documentar ciertos aspectos concretos como listas de parámetros o valores devueltos. Los tags se indican a continuación.

Tipo de tag Formato Descripción
Todos @see
Permite crear una referencia a la documentación de otra clase o método.
Clases @version
Comentario con datos indicativos del número de versión.
Clases @author
Nombre del autor.
Clases @since
Fecha desde la que está presente la clase.
Métodos @param
Parámetros que recibe el método.
Métodos @return
Significado del dato devuelto por el método.
Métodos @throws
Comentario sobre las excepciones que lanza.
Métodos @deprecated
Indicación de que el método es obsoleto.

Toda la documentación del API de Java está creada usando esta técnica y la herramienta javadoc.

Una clase comentada

import java.util.*;

/** Un programa simple en Java.
 * Envía un saludo e indica que día es hoy.
 * @author Mundo Telecomunicaciones
 * @version 1
 */
public class HolaATodos {

    /** Unico punto de entrada.
     * @param args Array de Strings.
     * @return No devuelve ningun valor.
     * @throws No dispara ninguna excepcion.
     */
    public static void main(String [] args) {
        System.out.println("Hola a todos");
        System.out.println(new Date());
    }

}

Convenciones de nombres

ORACLE recomienda un estilo de codificación que es seguido en el API de Java y en este post, y que consiste en:
  • Utilizar nombres descriptivos para las clases, evitando los nombres muy largos.
  • Para los nombres de clases poner la primera letra en mayúsculas y las demás en minúsculas. Por ejemplo: Empleado
  • Si el nombre tiene varias palabras ponerlas todas juntas (sin separar con - o _) y poner la primera letra de cada palabra en mayúsculas. Por ejemplo: InstrumentoMusical.
  • Para los nombres de miembros (datos y métodos) seguir la misma norma, pero con la primera letra de la primera palabra en minúsculas. Por ejemplo: registrarOyente.
  • Para las constantes (datos con el modificador final) usar nombres en mayúsculas, separando las palabras con _ solamente.

REFERENCIAS:       arrakis

Clases embebidas (Inner classes) en Java

Concepto

Una clase embebida es una clase que se define dentro de otra. Es una característica de Java que permite agrupar clases lógicamente relacionadas y controlar la "visibilidad" de una clase. El uso de las clases embebidas no es obvio y contienen detalles algo más complejos que escapan del ámbito de esta introducción.

Se puede definir una clase embebida de la siguiente forma:
class Externa {
    . . .
    class Interna {
        . . .
    }
}
La clase Externa puede instanciar y usar la clase Interna como cualquier otra, sin limitación ni cambio en la sintaxis de acceso:
class Externa {
    . . .
    class Interna {
        . . .
    }
    void metodo() {
        Interna i = new Interna(. . .);
        . . .
    }
}
Una diferencia importante es que un objeto de la clase embebida está relacionado siempre con un objeto de la clase que la envuelve, de tal forma que las instancias de la clase embebida deben ser creadas por una instancia de la clase que la envuelve. Desde el exterior estas referencias pueden manejarse, pero calificandolas completamente; es decir, nombrando la clase externa y luego la interna. Además, una instancia de la clase embebida tiene acceso a todos los datos miembros de la clase que la envuelve sin usar ningún calificador de acceso especial (como si le pertenecieran). Todo esto se ve en el ejemplo siguiente.

Ejemplo

Un ejemplo donde puede apreciarse fácilmente el uso de clases embebidas es el concepto de iterador. Un iterador es una clase diseñada para recorrer y devolver ordenadamente los elementos de algún tipo de contenedor de objetos. En el ejemplo se hace una implementación muy elemental que trabaja sobre un array.
class Almacen {
    private Object [] listaObjetos;
    private int numElementos = 0;
    Almacen (int maxElementos) {
        listaObjetos = new Object[maxElementos];
    }
    public Object get(int i) {
        return listaObject[i];
    }
    public void add(Object obj) {
        listaObjetos[numElementos++] = obj;
    }
    public Iterador getIterador() {
        new Iterador();
    }

    class Iterador {
        int indice = 0;
        Object siguiente() {
           if(indice < numElementos) return listaObjetos[indice++];
           else return null;
        }
    }
}
Y la forma de usarse, sería:
Almacen alm = new Almacen(10);  // se crea un nuevo almacen
. . .
alm.add(...);  // se añaden objetos
. . .
// para recorrerlo
Almacen.Iterador i = alm.getIterador();  // obtengo un iterador para alm
Object o;
while ( (o = i.siguiente()) != null) {
    . . .
}

REFERENCIAS:       arrakis

Interfaces en Java

Concepto de Interface

El concepto de Interface lleva un paso más adelante la idea de las clases abstractas. En Java una interface es una clase abstracta pura; es decir, una clase donde todos los métodos son abstractos (no se implementa ninguno). Permite al diseñador de clases establecer la forma de una clase (nombres de métodos, listas de argumentos y tipos de retorno, pero no bloques de código). Una interface puede también contener datos miembro, pero estos son siempre static y final. Una interface sirve para establecer un "protocolo" entre clases.

Para crear una interface, se utiliza la palabra clave interface en lugar de class. La interface puede definirse public o sin modificador de acceso, y tiene el mismo significado que para las clases. Todos los métodos que declara una interface son siempre public.

Para indicar que una clase implementa los métodos de una interface se utiliza la palabra clave implements. El compilador se encargará de verificar que la clase efectivamente declare e implemente todos los métodos de la interface. Una clase puede implementar más de una interface.

Declaración y uso

Una interface se declara:
interface nombre_interface {
    tipo_retorno nombre_metodo ( lista_argumentos ) ;
    . . .
}
Por ejemplo:
interface InstrumentoMusical {
    void tocar();
    void afinar();
    String tipoInstrumento();
}
Y una clase que implementa la interface:
class InstrumentoViento extends Object implements InstrumentoMusical {
    void tocar() { . . . };
    void afinar() { . . .};
    String tipoInstrumento() {}
}

class Guitarra extends InstrumentoViento {
    String tipoInstrumento() {
        return "Guitarra";
    }
}
La clase InstrumentoViento implementa la interface, declarando los métodos y escribiendo el código correspondiente. Una clase derivada puede también redefinir si es necesario alguno de los métodos de la interface.

Referencias a Interfaces

Es posible crear referencias a interfaces, pero las interfaces no pueden ser instanciadas. Una referencia a una interface puede ser asignada a cualquier objeto que implemente la interface. Por ejemplo:
InstrumentoMusical instrumento = new Guitarra();
instrumento.play();
System.out.println(instrumento.tipoInstrumento());

InstrumentoMusical i2 = new InstrumentoMusical();   //error. No se puede instanciar.

Extensión de interfaces

Las interfaces pueden extender otras interfaces y, a diferencia de las clases, una interface puede extender más de una interface. La sintaxis es:
interface nombre_interface extends nombre_interface , . . . {
    tipo_retorno nombre_metodo ( lista_argumentos ) ;
    . . .
}

Agrupaciones de constantes

Dado que, por definición, todos los datos miembros que se definen en una interface son static y final, y dado que las interfaces no pueden instanciarse resultan una buena herramienta para implantar grupos de constantes. Por ejemplo:
public interface Meses {
    int ENERO = 1 , FEBRERO = 2 . . . ;
    String [] NOMBRES_MESES = { " " , "Enero" , "Febrero" , ... };
}
Esto puede usarse simplemente:
System.out.println(Meses.NOMBRES_MESES[ENERO]);

Ejemplo de aplicación casi real

El ejemplo mostrado a continuación es una simplificación de como funciona realmente la gestión de eventos en el sistema gráfico de usuario soportado por el API de Java (AWT o swing). Se han cambiado los nombres y se ha simplificado para mostrar un caso real en que el uso de interfaces resuelve un problema concreto.

Supongamos que tenemos una clase que representa un botón de acción en un entorno gráfico de usuario (el típico botón de confirmación de una acción o de cancelación). Esta clase pertenecerá a una amplia jerarquía de clases y tendrá mecanismos complejos de definición y uso que no son objeto del ejemplo. Sin embargo, podríamos pensar que la clase Boton tiene miembros como los siguientes.
class Boton extends . . . {
    protected int x , y, ancho, alto;  // posicion del boton
    protected String texto;  // texto del boton
    Boton(. . .) {
        . . .
    }
    void dibujar() { . . .}
    public void asignarTexto(String t) { . . .}
    public String obtenerTexto() { . . .)
    . . .
}
Lo que aquí nos interesa es ver lo que sucede cuando el usuario, utilizando el ratón pulsa sobre el botón. Supongamos que la clase Boton tiene un método, de nombre por ejemplo click(), que es invocado por el gestor de ventanas cuando ha detectado que el usuario ha pulsado el botón del ratón sobre él. El botón deberá realizar alguna acción como dibujarse en posición "pulsado" (si tiene efectos de tres dimensiones) y además, probablemente, querrá informar a alguien de que se ha producido la acción del usuario. Es en este mecanismo de "notificación" donde entra el concepto de interface. Para ello definimos una interface Oyente de la siguiente forma:
interface Oyente {
    void botonPulsado(Boton b);
}
La interface define un único método botonPulsado. La idea es que este método sea invocado por la clase Boton cuando el usuario pulse el botón. Para que esto sea posible en algún momento hay que notificar al Boton quien es el Oyente que debe ser notificado. La clase Boton quedaría:
class Boton extends . . . {
    . . .
    private Oyente oyente;
    void registrarOyente(Oyente o) {
        oyente = o;
    }
    void click() {
        . . .
        oyente.botonPulsado(this);
    }
}
El método registrarOyente sirve para que alguien pueda "apuntarse" como receptor de las acciones del usuario. Obsérvese que existe una referencia de tipo Oyente. A Boton no le importa que clase va a recibir su notificación. Simplemente le importa que implante la interface Oyente para poder invocar el método botonPulsado. En el método click se invoca este método. En el ejemplo se le pasa como parámetro una referencia al propio objeto Boton. En la realidad lo que se pasa es un objeto "Evento" con información detallada de lo que ha ocurrido.

Con todo esto la clase que utiliza este mecanismo podría tener el siguiente aspecto:
class miAplicacion extends . . . implements Oyente {
    public static void main(String [] args) {
        new miAplicacion(. . .);
        . . .
    }
    . . .
    miAplicacion(. . .) {
        . . .
        Boton b = new Boton(. . .);
        b.registrarOyente(this);
    }

    . . .
    void botonPulsado(Boton x) {
        // procesar click
        . . .
    }
}
Obsérvese en el método registrarOyente que se pasa la referencia this, que en el lado de la clase Boton es recogido como una referencia a la interface Oyente. Esto es posible porque la clase miAplicacion implementa la interface Oyente. En términos clásicos de herencia miAplicacion ES un Oyente.

REFERENCIAS:       arrakis

Clases abstractas en Java

Concepto

Hay ocasiones, cuando se desarrolla una jerarquía de clases en que algún comportamiento está presente en todas ellas pero se materializa de forma distinta para cada una. Por ejemplo, pensemos en una estructura de clases para manipular figuras geométricas. Podríamos pensar en tener una clase genérica, que podría llamarse FiguraGeometrica y una serie de clases que extienden a la anterior que podrían ser Circulo, Poligono, etc. Podría haber un método dibujar dado que sobre todas las figuras puede llevarse a cabo esta acción, pero las operaciones concretas para llevarla a cabo dependen del tipo de figura en concreto (de su clase). Por otra parte la acción dibujar no tiene sentido para la clase genérica FiguraGeometrica, porque esta clase representa una abstracción del conjunto de figuras posibles.

Para resolver esta problemática Java proporciona las clases y métodos abstractos. Un método abstracto es un método declarado en una clase para el cual esa clase no proporciona la implementación (el código). Una clase abstracta es una clase que tiene al menos un método abstracto. Una clase que extiende a una clase abstracta debe implementar los métodos abstractos (escribir el código) o bien volverlos a declarar como abstractos, con lo que ella misma se convierte también en clase abstracta.

Declaración e implementación de métodos abstractos

Siguiendo con el ejemplo del apartado anterior, se puede escribir:
abstract class FiguraGeometrica {
    . . .
    abstract void dibujar();
    . . .
}

class Circulo extends FiguraGeometrica {
    . . .
    void dibujar() {
        // codigo para dibujar Circulo
        . . .
    }
}
La clase abstracta se declara simplemente con el modificador abstract en su declaración. Los métodos abstractos se declaran también con el mismo modificador, declarando el método pero sin implementarlo (sin el bloque de código encerrado entre {}). La clase derivada se declara e implementa de forma normal, como cualquier otra. Sin embargo, si no declara e implementa los métodos abstractos de la clase base (en el ejemplo el método dibujar) el compilador genera un error indicando que no se han implementado todos los métodos abstractos y que, o bien, se implementan, o bien se declara la clase abstracta.

Referencias y objetos abstractos

Se pueden crear referencias a clases abstractas como cualquier otra. No hay ningún problema en poner:
FiguraGeometrica figura;
Sin embargo, una clase abstracta no se puede instanciar, es decir, no se pueden crear objetos de una clase abstracta. El compilador producirá un error si se intenta:
FiguraGeometrica figura = new FiguraGeometrica();
Esto es coherente dado que una clase abstracta no tiene completa su implementación y encaja bien con la idea de que algo abstracto no puede materializarse.

Sin embargo, utilizando el up-casting visto en el capítulo dedicado a la Herencia si se puede escribir:
FiguraGeometrica figura = new Circulo(. . .);
figura.dibujar();
La invocación al método dibujar se resolverá en tiempo de ejecución y la JVM llamará al método de la clase adecuada. En nuestro ejemplo se llamará al método dibujar de la clase Circulo.

REFERENCIAS:       arrakis

Clases Wrapper (elvoltorio) en Java

Definición y uso de clases envoltorio

En ocasiones es muy conveniente poder tratar los datos primitivos (int, boolean, etc.) como objetos. Por ejemplo, los contenedores definidos por el API en el package java.util (Arrays dinámicos, listas enlazadas, colecciones, conjuntos, etc.) utilizan como unidad de almacenamiento la clase Object. Dado que Object es la raíz de toda la jerarquía de objetos en Java, estos contenedores pueden almacenar cualquier tipo de objetos. Pero los datos primitivos no son objetos, con lo que quedan en principio excluidos de estas posibilidades.

Para resolver esta situación el API de Java incorpora las clases envoltorio (wrapper class), que no son más que dotar a los datos primitivos con un envoltorio que permita tratarlos como objetos. Por ejemplo podríamos definir una clase envoltorio para los enteros, de forma bastante sencilla, con:
public class Entero {
    private int valor;

    Entero(int valor) {
        this.valor = valor;
    }

    int intValue() {
        return valor;
    }
}
La API de Java hace innecesario esta tarea al proporcionar un conjunto completo de clases envoltorio para todos los tipos primitivos. Adicionalmente a la funcionalidad básica que se muestra en el ejemplo las clases envoltorio proporcionan métodos de utilidad para la manipulación de datos primitivos (conversiones de / hacia datos primitivos, conversiones a String, etc.)

Las clases envoltorio existentes son:
  • Byte para byte.
  • Short para short.
  • Integer para int.
  • Long para long.
  • Boolean para boolean.
  • Float para float.
  • Double para double.
  • Character para char.
Observese que las clases envoltorio tienen siempre la primera letra en mayúsculas.

Las clases envoltura se usan como cualquier otra:
Integer i = new Integer(5);
int x = i.intValue();
Hay que tener en cuenta que las operaciones aritméticas habituales (suma, resta, multiplicación, etc.) están definidas solo para los datos primitivos por lo que las clases envoltura no sirven para este fin.

Las variables primitivas tienen mecanismos de reserva y liberación de memoria más eficaces y rápidos que los objetos por lo que deben usarse datos primitivos en lugar de sus correspondientes envolturas siempre que se pueda.

Resumen de métodos de Integer

Las clases envoltorio proporcionan también métodos de utilidad para la manipulación de datos primitivos. La siguiente tabla muestra un resumen de los métodos disponibles para la clase Integer.

Método Descripción
Integer(int valor)
Integer(String valor)
Constructores a partir de int y String.
int intValue() /
byte byteValue() /
float floatValue() . . .
Devuelve el valor en distintos formatos, int, long, float, etc.
boolean equals(Object obj)
Devuelve true si el objeto con el que se compara es un Integer y su valor es el mismo.
static Integer getInteger(String s)
Devuelve un Integer a partir de una cadena de caracteres. Estático.
static int parseInt(String s)
Devuelve un int a partir de un String. Estático.
static String toBinaryString(int i)
static String toOctalString(int i)
static String toHexString(int i)
static String toString(int i)
Convierte un entero a su representación en String en binario, octal, hexadecimal, etc. Estáticos.
String toString()
Convierte a String (devuelve una cadena) cualquier objeto Java.
static Integer valueOf(String s)
Devuelve un Integer a partir de un String. Estático.

El API de Java contiene una descripción completa de todas las clases envoltorio en el package java.lang.


REFERENCIAS:       arrakis

Gestión de excepciones en Java

Excepciones y categorías

Las excepciones son el mecanismo por el cual pueden controlarse en un programa Java las condiciones de error que se producen. Estas condiciones de error pueden ser errores en la lógica del programa como un índice de un array fuera de su rango, una división por cero o errores disparados por los propios objetos que denuncian algún tipo de estado no previsto, o condición que no pueden manejar.

La idea general es que cuando un objeto encuentra una condición que no sabe manejar crea y dispara una excepción que deberá ser capturada por el que le llamó o por alguien más arriba en la pila de llamadas. Las excepciones son objetos que contienen información del error que se ha producido y que heredan de la clase Throwable o de la clase Exception. Si nadie captura la excepción interviene un manejador por defecto que normalmente imprime información que ayuda a encontrar quién produjo la excepción.

Existen dos categorías de excepciones:
  • Excepciones verificadas: El compilador obliga a verificarlas. Son todas las que son lanzadas explicitamente por objetos de usuario.
  • Excepciones no verificadas: El compilador no obliga a su verificación. Son excepciones como divisiones por cero, excepciones de puntero nulo, o índices fuera de rango.

Generación de excepciones

Supongamos que tenemos una clase Empresa que tiene un array de objetos Empleado (clase vista en capítulos anteriores). En esta clase podríamos tener métodos para contratar un Empleado (añadir un nuevo objeto al array), despedirlo (quilarlo del array) u obtener el nombre a partir del número de empleado. La clase podría ser algo así como lo siguiente:
public class Empresa {
    String nombre;
    Empleado [] listaEmpleados;
    int totalEmpleados = 0;
    . . .
    Empresa(String n, int maxEmp) {
        nombre = n;
        listaEmpleados = new Empleado [maxEmp];
    }
    . . .
    void nuevoEmpleado(String nombre, int sueldo) {
        if (totalEmpleados < listaEmpleados.length ) {
            listaEmpleados[totalEmpleados++] = new Empleado(nombre,sueldo);
        }
    }
}
Observese, que en el método nuevoEmpleado se comprueba que hay sitio en el array para almacenar la referencia al nuevo empleado. Si lo hay se crea el objeto. Pero si no lo hay, el método no hace nada más. No da ninguna indicación de si la operación ha tenido éxito o no. Se podría hacer una modificación para que, por ejemplo el método devolviera un valor booleano true si la operación se ha completado con éxito y false si ha habido algún problema.

Otra posibilidad es generar una excepción verificada (Una excepción no verificada se produciría si no se comprobara si el nuevo empleado va a caber o no en el array). Veamos como se haría esto.

Las excepciones son clases, que heredan de la clase genérica Exception. Es necesario por tanto, asignar un nombre a nuestra excepción. Se suelen asignar nombres que den alguna idea del tipo de error que controlan. En nuestro ejemplo le vamos a llamar CapacidadEmpresaExcedida.

Para que un método lance una excepción:
  • Debe declarar el tipo de excepción que lanza con la cláusula throws, en su declaración.
  • Debe lanzar la excepción, en el punto del código adecuado con la sentencia throw.
En nuestro ejemplo:
void nuevoEmpleado(String nombre, int sueldo) throws CapacidadEmpresaExcedida {
    if (totalEmpleados < listaEmpleados.length) {
        listaEmpleados[totalEmpleados++] = new Empleado(nombre,sueldo);
    }
    else throw new CapacidadEmpresaExcedida(nombre);
}
Además, necesitamos escribir la clase CapacidadEmpresaExcedida. Sería algo así:
public class CapacidadEmpresaExcedida extends Exception {

    CapacidadEmpresaExcedida(String nombre) {
        super("No es posible añadir el empleado " + nombre);
    }
    . . .
}
La sentencia throw crea un objeto de la clase CapacidadEmpresaExcedida. El constructor tiene un argumento (el nombre del empleado). El constructor simplemente llama al constructor de la superclase pasándole como argumento un texto explicativo del error ( y el nombre del empleado que no se ha podido añadir).

La clase de la excepción puede declarar otros métodos o guardar datos de depuración que se consideren oportunos. El único requisito es que extienda la clase Exception. Consultar la documentación del API para ver una descripción completa de la clase Exception.

De esta forma se pueden construir métodos que generen excepciones.

Captura de excepciones

Con la primera versión del método nuevoEmpleado (sin excepción) se invocaría este método de la siguiente forma:
Empresa em = new Empresa("La Primera");
em.nuevoEmpleado("Carlos Fernández",500);
Si se utilizara este formato en el segundo caso (con excepción) el compilador produciría un error indicando que no se ha capturado la excepción verificada lanzada por el método nuevoEmpleado. Para capturar la excepción es utiliza la construcción try/catch, de la siguiente forma:
Empresa em = new Empresa("La Primera");
try {
    em.nuevoEmpleado("Carlos Fernández",500);
}
catch (CapacidadEmpresaExcedida exc) {
    System.out.println(exc.toString());
    System.exit(1);
}
  • Se encierra el código que puede lanzar la excepción en un bloque try/catch.
  • A continuación del catch se indica que tipo de excepción se va a capturar.
  • Después del catch se escribe el código que se ejecutará si se lanza la excepción.
  • Si no se lanza la excepción el bloque catch no se ejecuta.
El formato general del bloque try/catch es:
try {
    . . .
}
catch (Clase_Excepcion nombre) { . . .}
catch (Clase_Excepcion nombre) { . . .}
. . .
Observese que se puede capturar más de un tipo de excepción declarando más de una sentencia catch. También se puede capturar una excepción genérica (clase Exception) que engloba a todas las demás.

En ocasiones el código que llama a un método que dispara una excepción tampoco puede (o sabe) manejar esa excepción. Si no sabe que hacer con ella puede de nuevo lanzarla hacia arriba en la pila de llamada para que la gestione quien le llamo (que a su vez puede capturarla o reenviarla). Cuando un método no tiene intención de capturar la excepción debe declararla metdiante la cláusula throws, tal como hemos visto en el método que genera la excepción.

Supongamos que, en nuestro ejemplo es el método main de una clase el que invoca el método nuevoEmpleado. Si no quiere capturar la excepción debe hacer lo siguiente:
public static void main(String [] args) throws CapacidadEmpresaExcedida {
    Empresa em = new Empresa("La Primera");
    em.nuevoEmpleado("Carlos Fernández",500);
}

Cláusula finally

La cláusula finally forma parte del bloque try/catch y sirve para especificar un bloque de código que se ejecutará tanto si se lanza la excepción como si no. Puede servir para limpieza del estado interno de los objetos afectados o para liberar recursos externos (descriptores de fichero, por ejemplo). La sintaxis global del bloque try/catch/finally es:
try {
    . . .
}
catch (Clase_Excepcion nombre) { . . .}
catch (Clase_Excepcion nombre) { . . .}
. . .
finally { . . .}

REFERENCIAS:       arrakis

Herencia en Java - Parte II

El modificador de acceso protected

El modificador de acceso protected es una combinación de los accesos que proporcionan 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.

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 porque la referencia emp apunta a un objeto de clase Empleado y no a uno de clas Ejecutivo.

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 explicitamente 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 exahustiva 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.

REFERENCIAS:       arrakis

Herencia en Java - Parte I

Introducción

La herencia es una de las características básicas de la Programación Orientada a Objetos. Uno de los calificadores que entra en la definición cuando se pretende resumir en pocas palabras lo que es o distingue un Lenguaje Orientado a Objetos (Otros calificadores que entran en estas definiciones suelen ser encapsulación y polimorfismo). Es una de las bases que permite cosas tales como la reutilización del código, especialización, o evolución. Sin embargo también conduce a sistemas que son más complicados de entender y mantener.

La Herencia es el mecanismo por el cual una clase extiende las propiedades y comportamientos (datos y métodos) de otra clase, que se denomina clase base. La clase 'heredera' se denomina clase extendida o derivada. Una clase derivada tiene sus propias propiedades y comportamientos (los que están especificados en su código), pero además tiene todas las propiedades y comportamientos de la clase base. Esto que quizá resulta un poco dificil de entender en abstracto es bastante más sencillo con los ejemplos de los siguientes apartados (eso espero).

La herencia provoca algunas situaciones de programación curiosas que es necesario entender bien porque son de uso muy habitual y su falta de compresión puede provocar dificultades graves. Te aconsejo que leas con cuidado este capítulo y el siguiente y en caso de dudas consultes textos más extensos.

A pesar de ser una característica básica, la herencia no siempre aparece en los programas que uno hace, sobre todo si son sencillos. Quizá la dificultad estriba en saber reconocer cuando es necesaria o conveniente o cuando es mejor evitar sus complicaciones. Esto es evidentemente un problema de diseño, que no siempre se aborda con suficiente cuidado y que da más de un quebradero de cabeza.

Composición

En anteriores ejemplos se ha visto que una clase tiene datos miembro que son instancias de otras clases. Por ejemplo:
class Circulo {
    Punto centro;
    int radio;
    float superficie() {
        return 3.14 * radio * radio;
    }
}
Esta técnica en la que una clase se compone o contiene instancias de otras clases se denomina composición. Es una técnica muy habitual cuando se diseñan clases. En el ejemplo diríamos que un Circulo tiene un Punto (centro) y un radio.

Herencia

Pero además de esta técnica de composición es posible pensar en casos en los que una clase es una extensión de otra. Es decir una clase es como otra y además tiene algún tipo de característica propia que la distingue. Por ejemplo podríamos pensar en la clase Empleado y definirla como:
class Empleado {
    String nombre;
    int numEmpleado , sueldo;

    static private int contador = 0;

    Empleado(String nombre, int sueldo) {
        this.nombre = nombre;
        this.sueldo = sueldo;
        numEmpleado = ++contador;
    }

    public void aumentarSueldo(int aumento) {
        sueldo += (int)(sueldo * aumento / 100);
    }

    public String toString() {
        return "Num. empleado " + numEmpleado + " Nombre: " +                nombre + " Sueldo: " + sueldo;
    }
}
En el ejemplo el Empleado se caracteriza por un nombre (String) y por un número de empleado y sueldo (enteros). La clase define un constructor que asigna los valores de nombre y sueldo y calcula el número de empleado a partir de un contador (variable estática que siempre irá aumentando), y dos métodos, uno para calcular el nuevo sueldo cuando se produce un aumento de sueldo (método aumentarSueldo) y un segundo que devuelve una representación de los datos del empleado en un String.(método toString).

Con esta representación podemos pensar en otra clase que reuna todas las características de Empleado y añada alguna propia. Por ejemplo, la clase Ejecutivo. A los objetos de esta clase se les podría aplicar todos los datos y métodos de la clase Empleado y añadir algunos, como por ejemplo el hecho de que un Ejecutivo tiene un presupuesto.

Así diriamos que la clase Ejecutivo extiende o hereda la clase Empleado. Esto en Java se hace con la clausula extends que se incorpora en la definición de la clase, de la siguiente forma:
class Ejecutivo extends Empleado {
    int presupuesto;

    void asignarPresupuesto(int p) {
        presupuesto = p;
    }
}
Con esta definición un Ejecutivo es un Empleado que además tiene algún rasgo distintivo propio. El cuerpo de la clase Ejecutivo incorpora sólo los miembros que son específicos de esta clase, pero implícitamente tiene todo lo que tiene la clase Empleado.

A Empleado se le llama clase base o superclase y a Ejecutivo clase derivada o subclase.

Los objetos de las clases derivadas se crean igual que los de la clase base y pueden acceder tanto sus datos y métodos como a los de la clase base. Por ejemplo:
Ejecutivo jefe = new Ejecutivo( "Armando Mucho", 1000);
jefe.asignarPresupuesto(1500);
jefe.aumentarSueldo(5);
Nota: La discusión acerca de los constructores se verá un poco más adelante.

¡Atención!: Un Ejecutivo ES un Empleado, pero lo contrario no es cierto. Si escribimos:
Empleado curri = new Empleado( "Esteban Comex Plota" , 100) ;
curri.asignarPresupuesto(5000);    // error
se producirá un error de compilación pues en la clase Empleado no existe ningún método llamado asignarPresupuesto.

Redefinición de métodos. El uso de super.

Además se podría pensar en redefinir algunos métodos de la clase base pero haciendo que métodos con el mismo nombre y características se comporten de forma distinta. Por ejemplo podríamos pensar en rediseñar el método toString de la clase Empleado añadiendo las características propias de la clase Ejecutivo. Así se podría poner:
class Ejecutivo extends Empleado {
    int presupuesto;

    void asignarPresupuesto(int p) {
        presupuesto = p;
    }

    public String toString() {
        String s = super.toString();
        s = s + " Presupuesto: " + presupuesto;
        return s;
    }
}
De esta forma cuando se invoque jefe.toString() se usará el método toString de la clase Ejecutivo en lugar del existente en la clase Empleado.

Observese en el ejemplo el uso de super, que representa referencia interna implícita a la clase base (superclase). Mediante super.toString() se invoca el método toString de la clase Empleado.

Inicialización de clases derivadas

Cuando se crea un objeto de una clase derivada se crea implicitamente un objeto de la clase base que se inicializa con su constructor correspondiente. Si en la creación del objeto se usa el constructor no-args, entonces se produce una llamada implicita al constructor no-args para la clase base. Pero si se usan otros constructores es necesario invocarlos explicitamente.

En nuestro ejemplo dado que la clase método define un constructor, necesitaremos también un constructor para la clase Ejecutivo, que podemos completar así:
class Ejecutivo extends Empleado {
    int presupuesto;

    Ejecutivo (String n, int s) {
        super(n,s);
    }

    void asignarPresupuesto(int p) {
        presupuesto = p;
    }

    public String toString() {
        String s = super.toString();
        s = s + " Presupuesto: " + presupuesto;
        return s;
    }
}
Observese que el constructor de Ejecutivo invoca directamente al constructor de Empleado mediante super(argumentos). En caso de resultar necesaria la invocación al constructor de la superclase debe ser la primera sentencia del constructor de la subclase.

REFERENCIAS:       arrakis