Práctica 12 San Sebastián, mayo 1999 Programación Java Javier García de Jalón · José Ignacio Rodríguez Alfonso Brazález · Alberto Larzabal · Jesús Calleja · Jon García Informática 2: Práctica nº 12 página 1 INDICE Ejercicio 1: Ejercicio 2: Ejercicio 3: Ejercicio 4: Ejercicio 5: Creación de la clase Timer...................................................................................................... 1 Sincronizar el acceso a un objeto I .......................................................................................... 3 Sincronizar el acceso a un objeto II ......................................................................................... 3 Movimiento oscilatorio de un objeto en la pantalla.................................................................. 4 Introducción de la técnica del doble buffer en la clase OscilaCanvas ....................................... 5 Antes de comenzar la práctica abre el Windows Explorer y comprueba que se ha creado de modo automático en tu disco G:\ un directorio llamado Inf2prac12. No deberás moverlo a otro subdirectorio ni cambiarle de nombre. Por motivos de orden es importante que todos los ejercicios de esta práctica se creen dentro de este directorio, porque esta semana se recogerá la práctica. Como recomendación general, mantén abierto el Windows Explorer y comprueba de vez en cuando que los proyectos de los distintos ejercicios se están guardando correctamente. Hay que evitar copiar los ejercicios de otra persona, principalmente porque así no se aprende a programar: a programar sólo se aprende programando. Puedes utilizar también Windows Explorer para ayudar a Visual J++ 6.0 a crear un proyecto nuevo a partir de los ficheros del anterior. Ejercicio 1: Creación de la clase Timer En este ejercicio se trata de crear una clase denominada Timer que actúe de una forma similar al objeto Timer que aparece en el entorno de programación Visual Basic. Consistía en incluir un objeto Timer en algún módulo o formulario del proyecto y programar una función Timer_NombreTimer() que se ejecutaba cada cierto número de milisegundos (propiedad Interval). Existía además la posibilidad de pararlo o arrancarlo nuevamente con la propiedad Enabled (valores true o false). Crea un proyecto llamado Ejer1 dentro de G:\Inf2Prac12 y guarda en él los ficheros que vayas creando. La clase Timer (se guardará en un fichero llamado Timer.java) se deberá encargar de ejecutar cada cierto número de milisegundos el método timer() implementado por otras clases . Se creará una interface llamada ITimer con un único método void timer() el cual deberá ser implementado por aquellas clases que deseen tener una función que se ejecute repetidamente. A continuación se muestra la interface ITimer: // interface ITimer.java public interface ITimer{ public void timer(); } // función a implementar Las variables miembro de la clase Timer serán: private private private private private Thread String long boolean ITimer t; name; interval; enabled; func; // // // // // // thread utilizado para ejecutar el método run() nombre del Timer intervalo de tiempo entre ejecuciones propiedad para indicar si está activo referencia a la clase que contiene el método timer() a ejecutar Implementará la interface Runnable de forma que el thread t ejecutará el método run() de esta clase Timer. El constructor de Timer ( public Timer(String nombre, ITimer func) ) recibirá como argumento una referencia a un objeto que implemente la interface ITimer. Será el método run() de la clase Timer el encargado de llamar a la función timer() de esa referencia (func) Informática 2: Práctica nº 12 página 2 con el intervalo de tiempo indicado por la variable interval. Además deberá definir los siguientes métodos públicos: String getName() void setInterval(long i) long getInterval() void setEnabled(boolean val) void run() /** /** /** /** /** devuelve el nombre del Timer */ se asigna el intervalo en milisegundos*/ se lee el intervalo en milisegundos*/ se para o arranca el thread */ método principal de Timer */ Se creará un fichero PruebaTimer.java desde donde se probará la clase Timer junto con la interface ITimer. Esta clase contiene el programa principal main(). Como el entorno Visual J++ cierra la consola y la aplicación en cuanto termina la ejecución, sin permitir ver con calma los resultados, se ha incluido una “espera” por medio de un mensaje y la lectura de un carácter, que debe producirse antes de terminar la aplicación. El fichero es el siguiente: // fichero PruebaTimer.java // prueba de la clase Timer public class PruebaTimer implements ITimer{ private Timer miTimer; private int n=0; public PruebaTimer(){ miTimer = new Timer("MiTimer",this); } // método para arrancar el Timer public void arranca(){ miTimer.setEnabled(true); } // método que se ejecuta cada intervalo de tiempo public void timer(){ n++; System.out.println("Ejecutado "+n+" veces..."); if(n==5){ // cuando n llega a 5 duplicamos la velocidad miTimer.setInterval(miTimer.getInterval()/2); } if(n==20){ // cuando n llega a 20 paramos el Timer miTimer.setEnabled(false); System.out.println("paramos el timer..."); } } /** inicio del programa */ public static void main(String[] args) { PruebaTimer mT = new PruebaTimer(); mT.arranca(); // se espera a pulsar Intro para terminar try { while ((Thread.currentThread().activeCount() )> 1) { Thread.currentThread().sleep(200); } System.out.print("Pulse return para finalizar "); System.in.read(); } catch (java.io.IOException e) {} catch (InterruptedException ie) {} } // fin del método main } // fin de la clase PruebaTimer Informática 2: Práctica nº 12 Ejercicio 2: página 3 Sincronizar el acceso a un objeto I El uso de varios threads o hilos puede enriquecer mucho un programa permitiendo realizar varias tareas simultáneamente. Pero a su vez si en un programa coexisten más de un thread la programación se complica. Por un lado es necesario estudiar el impacto que puede tener el hecho de que dos o más threads puedan modificar o acceder simultáneamente a las variables de un mismo objeto o clase. Por otro, hay que tener en cuenta la sincronización de varios threas ya que se puede llegar un punto donde un thread tenga que esperar a que otros finalicen una tarea antes de que aquel pueda continuar (por ejemplo que un segundo thread termine de leer un fichero). En este ejercicio se va a presentar un ejemplo del primer caso, es decir de accesos y modificaciones simultáneas. Crea un proyecto llamado Ejer2 con Visual J++ en el directorio de la práctica Inf2prac12. Copia a este directorio los ficheros VectorTest.java y VectorSinc.java que se encuentran en Q:\Infor2\Prac12\Ejer2. El fichero VectorSinc.java incluye un vector cuyos métodos no están sincronizados. El constructor de la clase ( public VectorSinc(int n) ) crea un vector de tamaño n y le asigna valores de 1 a n. Tiene dos métodos: el método showVals() muestra por pantalla los valores del vector y el método reverseVals() invierte el orden de los números. El funcionamiento deseado del programa es que los valores del vector estén siempre ordenados de 1 a n o de n a 1. Si el programa tuviera un sólo thread no habría ningún problema ya que sólo se podría ejecutar un método simultáneamente. Pero, ¿qué pasa si hay dos o más threads actuando sobre un mismo objeto de la clase VectorSinc? El funcionamiento puede ser incorrecto ya un thread puede estar modificando los valores utilizando reverseVals() y otro puede estar leyendo los valores con showVals() sin terminar de modificar. Incluso se puede dar el caso de que dos threads intenten invertir el orden de los valores simultáneamente con lo que el resultado es impredecible. En la función reverseVals() se ha incluido un sleep() para poder observar el mal funcionamiento ya que debido al poco tiempo que tardaría en ejecutarse esta función habría pocas posibilidades (aunque no nulas) de que ocurriera. Ejecuta varias veces el programa VectorTest.java y observa su funcionamiento. Verás que muchas veces los valores no salen ordenados. Modifica el fichero VectorSinc.java incluyendo a los dos métodos el indicador synchronized. De esta forma se consigue que no puedan ejecutarse simultáneamente dos métodos sobre un mismo objeto de la clase VectorSinc. Si mientras un thread ejecuta el método reverseVals() o showVals() el otro intenta ejecutar uno de los dos métodos este último se quedará bloqueado hasta que el primero haya finalizado la ejecución del método. Vuelve a ejecutar varias veces el programa VectorTest. En este caso los valores deberían aparecer siempre correctamente. Es importante tener en cuenta que si se añade un método a la clase que no esté sincronizado éste siempre se puede ejecutar aunque en ese mismo momento se esté ejecutando un método sincronizado. Prueba a quitar la palabra synchronized del método showVals() y ver qué ocurre. Ejercicio 3: Sincronizar el acceso a un objeto II En este ejercicio se presenta el caso de un thread que llega a un punto donde los datos a leer no se encuentran preparados y tiene que esperar por ellos. Crea un proyecto llamado Ejer3 con Visual J++ en el directorio de la práctica Inf2prac12. Copia a este directorio los ficheros Bandeja.java, Cocinero.java , Comensal.java, y Restaurante.java que se encuentran en Q:\Infor2\Prac12\Ejer3. A partir del nombre de los ficheros es fácil adivinar sobre qué tema está basado el ejercicio. Se trata de un restaurante donde como es habitual el cocinero tarda un tiempo en preparar los Informática 2: Práctica nº 12 página 4 distintos platos. El comensal (es uno de los threads) pide (método get() de Bandeja) un plato a un objeto Bandeja y si no hay un plato preparado debe esperar (método wait()) a que esté preparado por el cocinero (otro thread) quién pondrá el plato en la bandeja utilizando el método put() de Bandeja. La condición que indica si hay un plato preparado es el estado de preparado, variable miembro de la clase Bandeja. Obsérvese que si preparado es false, el método get() detiene el thread (llamando a wait()) hasta que se llame al método notify() o notifyAll() de la clase Bandeja. Es el método put() quién llama a notifyAll() una vez preparado el plato y después de asignar preparado = true. En este ejercicio se pide leer detenidamente y comprender el código de las distintas clases. El método wait() espera indefinidamente a que sea llamado el método notify() o notifyAll(). Cambia el método wait() por wait(1000) y comprueba qué sucede. En este segundo caso este método esperará como máximo (si no se llama antes a notify() o notifyAll()) 1000 milisegundos volviéndose a comprobar nuevamente si preparado es true. Ejercicio 4: Movimiento oscilatorio de un objeto en la pantalla En este ejercicio se trata de crear un applet que represente un movimiento oscilatorio. Un applet es una aplicación que se ejecuta en un browser al cargar una página HTML que contiene las tags <APPLET…> … </APPLET>. Tal y como se indica en el manual los applets no tienen ventana propia sino que es el browser quién les asigna un panel donde dibujar. Otra notable diferencia es la forma de arrancar el programa: mientras que en las aplicaciones "independientes" comienzan por el método main(), las applets comienzan por el método init() de la clase Applet. El browser crea un objeto de la clase derivada de Applet, llama al método init() y posteriormente al método start(), ambos de la clase Applet. No se llama por lo tanto al constructor de la clase. La labor que normalmente realiza el constructor se debe pasar a la función init(). La clase Applet deriva de Panel y por lo tanto hereda todas los métodos gráficos. Crea un proyecto llamado Ejer4 con Visual J++ en el directorio de la práctica G:\Inf2prac12. Copia a este directorio los ficheros Timer.java e ITimer.java realizados en el primer ejercicio de esta práctica. Estos ficheros serán utilizados para realizar la animación. Crea una nueva clase llamada Oscila (en un fichero Oscila.java) que herede de la clase Applet. Crea también un fichero llamado AppletOscila.html e incluye el código necesario para ejecutar Oscila.class como applet. El objeto móvil se dibujará en una nueva clase llamada OscilaCanvas (derivada de Canvas) la cual será añadida al panel del applet. Esta clase OscilaCanvas tendrá el método public void dibujar(long t) que se encargará de llamar al método paint() una vez actualizada la variable time que será miembro de la clase OscilaCanvas. La posición del móvil se evaluará mediante el método getX() que deberá ser miembro de OscilaCanvas (la variable tiempo estará expresada en milisegundos): // método que define el movimiento horizontal public int getX(long t) { return (int)(100*Math.sin(1.5*t/1000)); } La clase Oscila tendrá una variable miembro que represente el tiempo en milisegundos de la animación. Además creará un objeto de tipo Timer para realizar la animación. Deberá implementar la interface ITimer y por lo tanto definir el método timer(). Este método aumentará el tiempo y llamará al método dibujar() de la clase OscilaCanvas pasándole dicho tiempo Se incluirá un botón de forma que sea posible detener y arrancar el móvil. Informática 2: Práctica nº 12 página 5 Figura 1. Visualización de movimiento oscilatorio utilizado como aplicación Además de poder funcionar como applet, es habitual en muchos casos incluir el método main() que posibilite ejecutar la clase derivada de Applet como una aplicación fuera de un navegador. En este caso el método main() debe realizar las labores que realiza el browser, esto es: crear una ventana donde incluir el applet, crear un objeto de ese tipo, llamar a su método init() y por último llamar al método start(). Copia la clase VentanaCerrable.java a la carpeta del proyecto, incluye en la clase Oscila el siguiente código y pruébalo como aplicación (ver Figura 1): // se define un programa principal para poder // ejecutarse como aplicación public static void main(String []args) { // se crea un frame (no se puede derivar de VentanaCerrable) VentanaCerrable vc = new VentanaCerrable("Applet como aplicación"); vc.setSize(300, 200); vc.setLocation(100, 100); // se crea un objeto de la clase que deriva de Applet // se recuerda que los applets descienden de Panel Oscila osc = new Oscila(); // se añade el applet a la zona centro del frame vc.add(osc, "Center"); // se llama a los métodos fundamentales del applet osc.init(); osc.start(); // se muestra la ventana vc.show(); } // fin método main() Ejercicio 5: Introducción de la técnica del doble buffer en la clase OscilaCanvas Este ejercicio es muy similar al anterior. Crea un proyecto llamado Ejer5 y copia en él todos los ficheros del ejercicio anterior. Se trata de modificar la clase OscilaCanvas de forma que la animación se realice utilizando la técnica del doble buffer. Esta técnica consiste en no pintar directamente sobre el componente que se encuentra en la pantalla sino que se pinta sobre una imagen sin mostrar. Una vez que se ha finalizado de "construir" la imagen, está se mostrará directamente sobre el componente. De esta forma se consigue una notable mejora en el parpadeo de las imágenes al redibujar. A su vez de debe redefinir el método update() para que no borre la anterior imagen.