Material de apoyo Curso JAVA 2004 - proyecto FoMCalidad

October 9, 2017 | Autor: Eduardo Di Santi | Categoría: IT Education
Share Embed


Descripción

Tutorial básico de Java CARACTERISTICAS Las características principales que nos ofrece Java respecto a cualquier otro lenguaje de programación, son: Es SIMPLE: Java ofrece toda la funcionalidad de un lenguaje potente, pero sin las características menos usadas y más confusas de éstos. C++ es un lenguaje que adolece de falta de seguridad, pero C y C++ son lenguajes más difundidos, por ello Java se diseñó para ser parecido a C++ y así facilitar un rápido y fácil aprendizaje. Java elimina muchas de las características de otros lenguajes como C++, para mantener reducidas las especificaciones del lenguaje y añadir características muy útiles como el garbage collector (reciclador de memoria dinámica). No es necesario preocuparse de liberar memoria, el reciclador se encarga de ello y como es un thread de baja prioridad, cuando entra en acción, permite liberar bloques de memoria muy grandes, lo que reduce la fragmentación de la memoria. Java reduce en un 50% los errores más comunes de programación con lenguajes como C y C++ al eliminar muchas de las características de éstos, entre las que destacan: ● ● ● ● ● ●

aritmética de punteros no existen referencias registros (struct) definición de tipos (typedef) macros (#define) necesidad de liberar memoria (free)

Aunque, en realidad, lo que hace es eliminar las palabras reservadas (struct, typedef), ya que las clases son algo parecido. Además, el intérprete completo de Java que hay en este momento es muy pequeño, solamente ocupa 215 Kb de RAM. Es ORIENTADO A OBJETOS: Java implementa la tecnología básica de C++ con algunas mejoras y elimina algunas cosas para mantener el objetivo de la simplicidad del lenguaje. Java trabaja con sus datos como objetos y con interfaces a esos objetos. Soporta las tres características propias del paradigma de la orientación a objetos: encapsulación, herencia y polimorfismo. Las plantillas de objetos son llamadas, como en C++, clases y sus copias, instancias. Estas instancias, como en C++, necesitan ser construidas y destruidas en espacios de memoria. Java incorpora funcionalidades inexistentes en C++ como por ejemplo, la resolución dinámica de métodos. Esta característica deriva del lenguaje Objective C, propietario del sistema operativo Next. En C++ se suele trabajar con librerías dinámicas (DLLs) que obligan a recompilar la aplicación cuando se retocan las funciones que se encuentran en su interior. Este inconveniente es resuelto por Java mediante una interfaz específica llamada RTTI (RunTime Type Identification) que define la interacción entre objetos excluyendo variables de instancias o implementación de métodos. Las clases en Java tienen una representación en el runtime que permite a los programadores interrogar por el tipo de clase y enlazar dinámicamente la clase con el resultado de la búsqueda. Es DISTRIBUIDO: Java se ha construido con extensas capacidades de interconexión TCP/IP. Existen librerías de rutinas para acceder e interactuar con protocolos como http y ftp. Esto permite a los programadores acceder a la información a través de la red con tanta facilidad como a los ficheros locales.

La verdad es que Java en sí no es distribuido, sino que proporciona las librerías y herramientas para que los programas puedan ser distribuidos, es decir, que se corran en varias máquinas, interactuando. Es ROBUSTO: Java realiza verificaciones en busca de problemas tanto en tiempo de compilación como en tiempo de ejecución. La comprobación de tipos en Java ayuda a detectar errores, lo antes posible, en el ciclo de desarrollo. Java obliga a la declaración explícita de métodos, reduciendo así las posibilidades de error. Maneja la memoria para eliminar las preocupaciones por parte del programador de la liberación o corrupción de memoria. También implementa los arrays auténticos, en vez de listas enlazadas de punteros, con comprobación de límites, para evitar la posibilidad de sobreescribir o corromper memoria resultado de punteros que señalan a zonas equivocadas. Estas características reducen drásticamente el tiempo de desarrollo de aplicaciones en Java. Además, para asegurar el funcionamiento de la aplicación, realiza una verificación de los byte-codes, que son el resultado de la compilación de un programa Java. Es un código de máquina virtual que es interpretado por el intérprete Java. No es el código máquina directamente entendible por el hardware, pero ya ha pasado todas las fases del compilador: análisis de instrucciones, orden de operadores, etc., y ya tiene generada la pila de ejecución de órdenes. Java proporciona, pues: ● ● ● ●

Comprobación de punteros Comprobación de límites de arrays Excepciones Verificación de byte-codes

Es de ARQUITECTURA NEUTRAL: Para establecer Java como parte integral de la red, el compilador Java compila su código a un fichero objeto de formato independiente de la arquitectura de la máquina en que se ejecutará. Cualquier máquina que tenga el sistema de ejecución (run-time) puede ejecutar ese código objeto, sin importar en modo alguno la máquina en que ha sido generado. Actualmente existen sistemas run-time para Solaris 2.x, SunOs 4.1.x, Windows 95, Windows NT, Linux, Irix, Aix, Mac, Apple y probablemente haya grupos de desarrollo trabajando en el porting a otras plataformas.

El código fuente Java se "compila" a un código de bytes de alto nivel independiente de la máquina. Este código (byte-codes) está diseñado para ejecutarse en una máquina hipotética que es implementada por un sistema run-time, que sí es dependiente de la máquina. En una representación en que tuviésemos que indicar todos los elementos que forman parte de la

arquitectura de Java sobre una plataforma genérica, obtendríamos una figura como la siguiente:

En ella podemos ver que lo verdaderamente dependiente del sistema es la Máquina Virtual Java (JVM) y las librerías fundamentales, que también nos permitirían acceder directamente al hardware de la máquina. Además, habrá APIs de Java que también entren en contacto directo con el hardware y serán dependientes de la máquina, como ejemplo de este tipo de APIs podemos citar: ● ● ● ● ● ●

Java 2D: gráficos 2D y manipulación de imágenes Java Media Framework : Elementos críticos en el tiempo: audio, video... Java Animation: Animación de objetos en 2D Java Telephony: Integración con telefonía Java Share: Interacción entre aplicaciones multiusuario Java 3D: Gráficos 3D y su manipulación

Es SEGURO: La seguridad en Java tiene dos facetas. En el lenguaje, características como los punteros o el casting implícito que hacen los compiladores de C y C++ se eliminan para prevenir el acceso ilegal a la memoria. Cuando se usa Java para crear un navegador, se combinan las características del lenguaje con protecciones de sentido común aplicadas al propio navegador. El lenguaje C, por ejemplo, tiene lagunas de seguridad importantes, como son los errores de alineación. Los programadores de C utilizan punteros en conjunción con operaciones aritméticas. Esto le permite al programador que un puntero referencie a un lugar conocido de la memoria y pueda sumar (o restar) algún valor, para referirse a otro lugar de la memoria. Si otros programadores conocen nuestras estructuras de datos pueden extraer información confidencial de nuestro sistema. Con un lenguaje como C, se pueden tomar números enteros aleatorios y convertirlos en punteros para luego acceder a la memoria: printf( "Escribe un valor entero: " ); scanf( "%u",&puntero ); printf( "Cadena de memoria: %s\n",puntero );

Otra laguna de seguridad u otro tipo de ataque, es el Caballo de Troya. Se presenta un programa como una utilidad, resultando tener una funcionalidad destructiva. Por ejemplo, en UNIX se visualiza el contenido de un directorio con el comando ls. Si un programador deja un comando destructivo bajo esta referencia, se puede correr el riesgo de ejecutar código malicioso, aunque el comando siga haciendo la funcionalidad que se le supone, después de lanzar su carga destructiva. Por ejemplo, después de que el caballo de Troya haya enviado por correo el /etc/shadow a su creador, ejecuta la funcionalidad de ls persentando el contenido del directorio. Se notará un retardo, pero nada inusual.

El código Java pasa muchos tests antes de ejecutarse en una máquina. El código se pasa a través de un verificador de byte-codes que comprueba el formato de los fragmentos de código y aplica un probador de teoremas para detectar fragmentos de código ilegal -código que falsea punteros, viola derechos de acceso sobre objetos o intenta cambiar el tipo o clase de un objeto-. Si los byte-codes pasan la verificación sin generar ningún mensaje de error, entonces sabemos que: ● ● ● ● ●

El código no produce desbordamiento de operandos en la pila El tipo de los parámetros de todos los códigos de operación son conocidos y correctos No ha ocurrido ninguna conversión ilegal de datos, tal como convertir enteros en punteros El acceso a los campos de un objeto se sabe que es legal: public, private, protected No hay ningún intento de violar las reglas de acceso y seguridad establecidas

El Cargador de Clases también ayuda a Java a mantener su seguridad, separando el espacio de nombres del sistema de ficheros local, del de los recursos procedentes de la red. Esto limita cualquier aplicación del tipo Caballo de Troya, ya que las clases se buscan primero entre las locales y luego entre las procedentes del exterior. Las clases importadas de la red se almacenan en un espacio de nombres privado, asociado con el origen. Cuando una clase del espacio de nombres privado accede a otra clase, primero se busca en las clases predefinidas (del sistema local) y luego en el espacio de nombres de la clase que hace la referencia. Esto imposibilita que una clase suplante a una predefinida. En resumen, las aplicaciones de Java resultan extremadamente seguras, ya que no acceden a zonas delicadas de memoria o de sistema, con lo cual evitan la interacción de ciertos virus. Java no posee una semántica específica para modificar la pila de programa, la memoria libre o utilizar objetos y métodos de un programa sin los privilegios del kernel del sistema operativo. Además, para evitar modificaciones por parte de los crackers de la red, implementa un método ultraseguro de autentificación por clave pública. El Cargador de Clases puede verificar una firma digital antes de realizar una instancia de un objeto. Por tanto, ningún objeto se crea y almacena en memoria, sin que se validen los privilegios de acceso. Es decir, la seguridad se integra en el momento de compilación, con el nivel de detalle y de privilegio que sea necesario. Dada, pues la concepción del lenguaje y si todos los elementos se mantienen dentro del estándar marcado por Sun, no hay peligro. Java imposibilita, también, abrir ningún fichero de la máquina local (siempre que se realizan operaciones con archivos, éstas trabajan sobre el disco duro de la máquina de donde partió el applet), no permite ejecutar ninguna aplicación nativa de una plataforma e impide que se utilicen otros ordenadores como puente, es decir, nadie puede utilizar nuestra máquina para hacer peticiones o realizar operaciones con otra. Además, los intérpretes que incorporan los navegadores de la Web son aún más restrictivos. Bajo estas condiciones (y dentro de la filosofía de que el único ordenador seguro es el que está apagado, desenchufado, dentro de una cámara acorazada en un bunker y rodeado por mil soldados de los cuerpos especiales del ejército), se puede considerar que Java es un lenguaje seguro y que los applets están libres de virus. Respecto a la seguridad del código fuente, no ya del lenguaje, JDK proporciona un desemsamblador de byte-code, que permite que cualquier programa pueda ser convertido a código fuente, lo que para el programador significa una vulnerabilidad total a su código. Utilizando javap no se obtiene el código fuente original, pero sí desmonta el programa mostrando el algoritmo que se utiliza, que es lo realmente interesante. La protección de los programadores ante esto es utilizar llamadas a programas nativos, externos (incluso en C o C++) de forma que no sea descompilable todo el código; aunque así se pierda portabilidad. Esta es otra de las cuestiones que Java tiene pendientes. Es PORTABLE: Más allá de la portabilidad básica por ser de arquitectura independiente, Java implementa otros estándares de portabilidad para facilitar el desarrollo. Los enteros son siempre enteros y además, enteros de 32 bits en complemento a 2. Además, Java construye sus interfaces de usuario a través de un sistema abstracto de ventanas de forma que las ventanas puedan ser implantadas en entornos Unix, Pc o Mac.

Es INTERPRETADO: El intérprete Java (sistema run-time) puede ejecutar directamente el código objeto. Enlazar (linkar) un programa, normalmente, consume menos recursos que compilarlo, por lo que los desarrolladores con Java pasarán más tiempo desarrollando y menos esperando por el ordenador. No obstante, el compilador actual del JDK es bastante lento. Por ahora, que todavía no hay compiladores específicos de Java para las diversas plataformas, Java es más lento que otros lenguajes de programación, como C++, ya que debe ser interpretado y no ejecutado como sucede en cualquier programa tradicional. Se dice que Java es de 10 a 30 veces más lento que C, y que tampoco existen en Java proyectos de gran envergadura como en otros lenguajes. La verdad es que ya hay comparaciones ventajosas entre Java y el resto de los lenguajes de programación, y una ingente cantidad de folletos electrónicos que supuran fanatismo en favor y en contra de los distintos lenguajes contendientes con Java. Lo que se suele dejar de lado en todo esto, es que primero habría que decidir hasta que punto Java, un lenguaje en pleno desarrollo y todavía sin definición definitiva, está maduro como lenguaje de programación para ser comparado con otros; como por ejemplo con Smalltalk, que lleva más de 20 años en cancha. La verdad es que Java para conseguir ser un lenguaje independiente del sistema operativo y del procesador que incorpore la máquina utilizada, es tanto interpretado como compilado. Y esto no es ningún contrasentido, me explico, el código fuente escrito con cualquier editor se compila generando el byte-code. Este código intermedio es de muy bajo nivel, pero sin alcanzar las instrucciones máquina propias de cada plataforma y no tiene nada que ver con el p-code de Visual Basic. El byte-code corresponde al 80% de las instrucciones de la aplicación. Ese mismo código es el que se puede ejecutar sobre cualquier plataforma. Para ello hace falta el run-time, que sí es completamente dependiente de la máquina y del sistema operativo, que interpreta dinámicamente el byte-code y añade el 20% de instrucciones que faltaban para su ejecución. Con este sistema es fácil crear aplicaciones multiplataforma, pero para ejecutarlas es necesario que exista el run-time correspondiente al sistema operativo utilizado. Es MULTITHREADED: Al ser multithreaded (multihilvanado, en mala traducción), Java permite muchas actividades simultáneas en un programa. Los threads (a veces llamados, procesos ligeros), son básicamente pequeños procesos o piezas independientes de un gran proceso. Al estar los threads contruidos en el lenguaje, son más fáciles de usar y más robustos que sus homólogos en C o C++. El beneficio de ser miltithreaded consiste en un mejor rendimiento interactivo y mejor comportamiento en tiempo real. Aunque el comportamiento en tiempo real está limitado a las capacidades del sistema operativo subyacente (Unix, Windows, etc.), aún supera a los entornos de flujo único de programa (single-threaded) tanto en facilidad de desarrollo como en rendimiento. Cualquiera que haya utilizado la tecnología de navegación concurrente, sabe lo frustrante que puede ser esperar por una gran imagen que se está trayendo. En Java, las imágenes se pueden ir trayendo en un thread independiente, permitiendo que el usuario pueda acceder a la información en la página sin tener que esperar por el navegador. Es DINAMICO: Java se beneficia todo lo posible de la tecnología orientada a objetos. Java no intenta conectar todos los módulos que comprenden una aplicación hasta el tiempo de ejecución. Las librería nuevas o actualizadas no paralizarán las aplicaciones actuales (siempre que mantengan el API anterior).

Java también simplifica el uso de protocolos nuevos o actualizados. Si su sistema ejecuta una aplicación Java sobre la red y encuentra una pieza de la aplicación que no sabe manejar, tal como se ha explicado en párrafos anteriores, Java es capaz de traer automáticamente cualquiera de esas piezas que el sistema necesita para funcionar.

Java, para evitar que los módulos de byte-codes o los objetos o nuevas clases, haya que estar trayéndolos de la red cada vez que se necesiten, implementa las opciones de persistencia, para que no se eliminen cuando de limpie la caché de la máquina.

JAVA PARA APLICACIONES CORPORATIVAS Java actualmente está en boca de todos, Java e Intranet son las palabras de moda. Pero, surge la pregunta de si esta es una buena tecnología para desarrollar aplicaciones corporativas. Y la respuesta es afirmativa y voy a proponer argumentos para esa afirmación. En donde la red sea algo crítico, Java facilita tremendamente la vida de la programación corporativa. Durante años, las grandes empresas se han convencido de que la "red" corporativa es la arteria por donde fluye la sangre que mantiene vivo su negocio. Desde el gran servidor de sus oficinas centrales, hasta los servidores de las delegaciones, las estaciones de trabajo de los programadores y la marabunta de PCs, la información va fluyendo de unos a otros. Para muchas compañías, la Red es la Empresa. Si esta red no se mantiene sana, los pedidos no llegan, el inventario no se actualiza, el software no se desarrolla adecuadamente, los clientes no están satisfechos y, fundamentalmente, el dinero no entra. La necesidad de diagnosticar y reducir la arterioesclerosis de la red, hace que se estén inyectando continuamente nuevas metodologías que subsanen este grave problema. ¿Es Java la medicina? Está claro que cuando vemos un cepillo animado limpiando los dientes, cubos moviéndose en 3-D, o una banda de gatos locos en applets de Java, nos convencemos de que es el lenguaje idóneo para Internet. Pero, qué pasa con las aplicaciones corporativas, ¿sería una buena tecnología allí donde la red es el punto crítico? Vamos a intentar responder comparando las capacidades de Java contra la lista de necesidades de la red corporativa.

Desarrollo rápido de aplicaciones Hace años, se decía que los programadores pronto desaparecerían. Los generadores automáticos de programas, eliminarían a los generadores humanos y el mundo sería un lugar mejor para vivir. Desafortunadamente, quienes decían esto no tuvieron en cuenta una acelerada demanda de software de calidad para muy diferentes aplicaciones. Sin embargo, la tecnología de objetos pronto vino a intentar facilitar la tarea, adoptando el modelo de "generar parte de un programa", así, generando la parte básica de un programa (los objetos), se podría conectar con otras partes para proporcionar diferentes utilidades al usuario. El lenguaje C++ es una buena herramienta, pero no cumple totalmente la premisa. Visual Basic y NextStep, se acercan cada vez más al poder de los objetos. Java facilita la creación de entornos de desarrollo-aplicaciones de modo similar, pero además es flexible, poderoso y efectivo. Los programadores ahora disponen de herramientas de programación de calidad beta, que apuntan hacia esa meta, como son el Java WorkShop de SunSoft, el entorno Java de Borland, el Café de Symantec, y pronto, herramientas más sofisticadas como Netcode o FutureTense. Esto proporciona una gran progresión a los entornos de desarrollo Java.

Aplicaciones efectivas y eficientes Las aplicaciones que se crean en grandes empresas deben ser más efectivas que eficientes; es decir, conseguir que el programa funcione y el trabajo salga adelante es más importante que el que lo haga eficientemente. Esto no es una crítica, es una realidad de la programación corporativa. Al ser un lenguaje más simple que cualquiera de los que ahora están en el cajón de los programadores, Java permite a éstos concentrarse en la mecánica de la aplicación, en vez de pasarse horas y horas incorporando APIs para el control de las ventanas, controlando minuciosamente la memoria, sincronizando los ficheros de cabecera y corrigiendo los agónicos mensajes del linker. Java tiene su propio toolkit para interfaces, maneja por sí mismo la memoria que utilice la aplicación, no permite ficheros de cabecera separados (en aplicaciones puramente Java) y solamente usa enlace dinámico. Muchas de las implementaciones de Java actuales son puros intérpretes. Los byte-codes son interpretados por el sistema run-time de Java, la Máquina Virtual Java (JVM), sobre el ordenador del

usuario. Aunque ya hay ciertos proveedores que ofrecen compiladores nativos Just-In-Time (JIT). Si la Máquina Virtual Java dispone de un compilador instalado, las secciones (clases) del byte-code de la aplicación se compilarán hacia la arquitectura nativa del ordenador del usuario. Los programas Java en ese momento rivalizarán con el rendimiento de programas en C++. Los compiladores JIT no se utilizan en la forma tradicional de un compilador; los programadores no compilan y distribuyen binarios Java a los usuarios. La compilación JIT tiene lugar a partir del byte-code Java, en el sistema del usuario, como una parte (opcional) del entorno run-time local de Java. Muchas veces, los programadores corporativos, ansiosos por exprimir al máximo la eficiencia de su aplicación, empiezan a hacerlo demasiado pronto en el ciclo de vida de la aplicación. Java permite algunas técnicas innovadoras de optimización. Por ejemplo, Java es inherentemente multithreaded, a la vez que ofrece posibilidades de multithread como la clase Thread y mecanismos muy sencillos de usar de sincronización; Java en sí utiliza threads. Los desarrolladores de compiladores inteligentes pueden utilizar esta característica de Java para lanzar un thread que compruebe la forma en que se está utilizando la aplicación. Más específicamente, este thread podría detectar qué métodos de una clase se están usando con más frecuencia e invocar a sucesivos niveles de optimización en tiempo de ejecución de la aplicación. Cuanto más tiempo esté corriendo la aplicación o el applet, los métodos estarán cada vez más optimizados (Guava de Softway es de este tipo). Si un compilador JIT está embebido en el entorno run-time de Java, el programador no se preocupa de hacer que la aplicación se ejecute óptimamente. Siempre he pensado que en los Sistemas Operativos tendría que aplicarse esta filosofía; un optimizador progresivo es un paso más hacia esta idea.

Portabilidad para programador y programa En una empresa de relativo tamaño hay una pléyade diferente de ordenadores. Probablemente nos encontremos con estaciones de trabajo Sun para el desarrollo de software, hordas de PCs para cada empleado, algún Mac en el departamento de documentación, una estación de trabajo HP en administración y una estación SGI en la sala de demos. Desarrollar aplicaciones corporativas para un grupo tan diferente de plataformas en excesivamente complejo y caro. Hasta ahora era complicado convencer a los programadores de cada arquitectura que utilizasen un API común para reducir el coste de las aplicaciones. Con un entorno run-time de Java portado a cada una de las arquitecturas de las plataformas presentes en la empresa y una buena librería de clases ("packages" en Java), los programadores pueden entenderse y encontrar muy interesante trabajar con Java. Esta posibilidad hará tender a los programadores hacia Java, justo donde otros intentos anteriores con entornos universales (como Galaxy o XVT) han fracasado. Estos APIs eran simplemente inadecuados, no orientados a redes y, verdaderamente, pesados. Una vez que los programas estén escritos en Java, otro lado interesante del asunto es que los programadores también son portables. El grupo de programadores de la empresa puede ahora enfrentarse a un desarrollo para cualquiera de las plataformas. La parte del cliente y del servidor de una aplicación estarán ahora escritas en el mismo lenguaje. Ya no será necesario tener un grupo que desarrolle en Solaris en del departamento de I+D, programadores trabajando sobre Visual Basic en el departamento de documentación y programadores sobre GNU en proyectos especiales; ahora todos ellos podrán estar juntos y formar el grupo de software de la empresa.

Costes de desarrollo En contraste con el alto coste de los desarrollos realizados sobre estaciones de trabajo, el coste de creación de una aplicación Java es similar al de desarrollar sobre un PC. Desarrollar utilizando un software caro para una estación de trabajo (ahora barata) es un problema en muchas empresas. La eficiencia del hardware y el poco coste de mantenimiento de una estación de trabajo Sun, por ejemplo, resulta muy atractivo para las empresas; pero el coste adicional del entorno de desarrollo con C++ es prohibitivo para la gran mayoría de ellas. La llegada de Java e Intranet

reducen considerablemente estos costes. Las herramientas Java ya no están en el entorno de precios de millones de pesetas, sino a los niveles confortables de precio de las herramientas de PCs. Y con el crecimiento cada día mayor de la comunidad de desarrolladores de software freeware y shareware que incluso proporcionan el código fuente, los programadores corporativos tienen un amplio campo donde moverse y muchas oportunidades de aprender y muchos recursos a su disposición. El éxito que Internet ha proporcionado a los equipos de software corporativos es un regalo. El precio del software es ahora el mismo para un poderoso equipo corriendo Unix que para un PC. Incluso Netscape tiene al mismo precio la versión Unix de su servidor Web SuiteSpot que la versión PC/NT. Esta es la filosofía de precios que parece ser será la que se siga con las herramientas basadas en Java.

Mantenimiento y soporte Un problema bien conocido que ocurre con el software corporativo es la demanda de cuidados y realimentación. Java no es, ciertamente, la cura para la enfermedad del mantenimiento, pero tiene varias características que harán la vida del enfermero más fácil. Uno de los componentes del JDK es javadoc. Si se usan ciertas convenciones en el código fuente Java (como comenzar un comentario con /** y terminarlo con */), javadoc se puede fácilmente generar páginas HTML con el contenido de esos comentarios, que pueden visualizarse en cualquier navegador. La documentación del API de Java ha sido creada de este modo. Esto hace que el trabajo de documentar el código de nuevas clases Java sea trivial. Otro gran problema del desarrollador corporativo es la creación y control de makefiles. Leerse un makefile es como estar leyendo la historia de empresa. Normalmente se pasan de programador a programador, quitando la información que no es esencial, siempre que se puede. Esto hace que muchos de los makefiles de las aplicaciones contengan docenas de librerías, una miríada de ficheros de cabecera y ultra-confusos macros. Es como mirar en el estómago de la ballena de Jonás. Java reduce las dependencia de complejos makefiles drásticamente. Primero, no hay ficheros de cabecera. Java necesita que todo el código fuente de una clase se encuentre en un solo fichero. Java tiene la inteligencia de make en el propio lenguaje para simplificar la compilación de byte-codes. Por ejemplo: public class pepe { Guitarra flamenca; } public class guitarra { }

// Fichero: pepe.java // Fichero: guitarra.java

% javac -verbose pepe.java [parsed pepe.java in 720ms] [loaded C:\JAVA\BIN\..\classes\java\lang\Object.class in 220ms] [checking class pepe] [parsed .\\Guitarra.java in 50ms] [wrote pepe.class] [checking class Guitarra] [wrote .\\Guitarra.class] [done in 2300ms]

El compilador Java se da cuenta de que necesita compilar el fichero guitarra.java. Ahora vamos a forzarlo a que recompile pepe.java sin cambiar guitarra.java, podremos comprobar que el compilador de byte-code Java no recompila innecesariamente el fichero guitarra.java. % javac -verbose pepe.java [parsed pepe.java in 440ms] [loaded C:\JAVA\BIN\..\classes\java\lang\Object.class in 160ms] [checking class pepe] [loaded .\\Guitarra.java in 0ms] [wrote pepe.class] [done in 1860ms]

Ahora, si modificamos guitarra.java (añadiendo, por ejemplo, otro miembro a la clase) y compilamos pepe.java, el compilador Java se dará cuenta de que debe recompilar tanto pepe.java como guitarra.java % javac -verbose pepe.java [parsed pepe.java in 710ms] [loaded C:\JAVA\BIN\..\classes\java\lang\Object.class in 220ms] [checking class pepe] [parsed .\\Guitarra.java in 0ms] [wrote pepe.class] [checking class Guitarra] [wrote .\\Guitarra.class] [done in 2640ms]

En el libro Just Java de Peter van der Linden hay un capítulo excelente acerca del compilador de Java, si tienes oportunidad, no dejes de leerlo.

Aprendizaje Si la empresa está llena de programadores de C++ con alguna experiencia en el manejo de librería gráficas, aprenderán rápidamente lo esencial de Java. Si el equipo de ingenieros no conoce C++, pero maneja cualquier otro lenguaje de programación orientada a objetos, les llevará pocas semanas dominar la base de Java. Lo que sí que no es cierto es que haya que aprender C++ antes de aprender Java. Si los ingenieros de la empresa no conocen ningún lenguaje orientado a objetos, sí que tienen que aprender los fundamentos de esta tecnología antes de nada, y luego aplicarlos a la programación con Java. El análisis y diseño orientado a objetos debe ser comprendido antes de intentar nada con Java. Los programadores de Java sin un fondo de conocimientos de OOA/D producirán código pobre. Además, los libros sobre Java crecen como la espuma, ya hay más de 25 publicados, y si buscas "Progamming in Java" en la Red, encontrarás 312 Web sites, y 30 más dedicados a "Learning Java". Y si esto, evidentemente, no es el sustituto de un instructor humano, hay ya varias empresas que ofrecen enseñanza de Java, entre ellas, Sun.

Resumen En base a los argumentos que acabamos de exponer, ¿podría una empresa utilizar Java para sus aplicaciones críticas? En este instante, sería suficiente un acercamiento a Java. Porque más importante que la elección de Java o cualquier otro lenguaje de programación es un buen diseño de la arquitectura de la aplicación. Diseñar la aplicación para que esté distribuida entre servidores y clientes, y la línea de partida debe ser el diseño modular. Algunas sugerencias para adoptar Java como tecnología corporativa, serían: 1. Usar Java en el desarrollo de la interface del cliente; Java es suficientemente estable para desarrollar una interface portable. Utilizar herramientas de programación más estables en los servidores, porque son la parte crítica. 2. Portar o crear un servidor no-crítico en Java, de forma que tanto cliente como servidor estén escritos en Java. 3. Utilizar Java en proyectos de envergadura tanto en el cliente como en el servidor, para valorar la efectividad de Java. Intranet está creciendo actualmente más rápido que Internet. Las organizaciones corporativas están adoptando la metodología Internet para proporcionar soluciones a sus usuarios y clientes. Java tiene todas las cartas para ser una herramienta de inestimable valor en el desarrollo de aplicaciones corporativas.

Actualmente ya hay entornos de desarrollo integrados completos para Java, diferentes del JDK de Sun. Symantec dispone de un compilador de Java para Windows 95 y Windows NT, con las ventajas del aumento de velocidad de proceso y capacidades multimedia que esto proporciona, Symantec Café. Borland también está trabajando en ello y la nueva versión de su entorno de desarrollo soporta Java. Sun ha lanzado la versión comercial de su propio entorno de desarrollo para Java, el Java Workshop, enteramente escrito en Java. En este curso usaremos (sin tratarlas a profunidadad) dos de estas herramientas, Visual café for Java de Symantec y NetBeans de Sun Microsystemas cuya versión comunity es gratuita y posee un poder de desarrollo de tipo enterprise. También hablaremos de un nuevo concepto llamado Eclipse, que estimamos tendrá predominio en los proximos años. En profundidad veremos solamente el JDK. El entorno básico del JDK de Java que proporciona Sun está formado por herramientas en modo texto, que son: java, intérprete que ejecuta programas en byte-code. javac, compilador de Java que convierte el código fuente en byte-code. javah, crea ficheros de cabecera para implementar métodos para cualquier clase. javap, es un descompilador de byte-code a código fuente Java. javadoc, es un generador automático de documentos HTML a partir del código fuente Java. javaprof, es un profiler para aplicaciones de un solo thread. HotJava, es un navegador Web escrito completamente en Java. El entorno habitual pues, consiste en un navegador que pueda ejecutar applets, un compilador que convierta el código fuente Java a byte-code y el intérprete Java para ejecutar los programas. Estos son los componenetes básicos para desarrollar algo en Java. No obstante se necesita un editor para escribir el código fuente, y no son estrictamente necesarias otras herramientas como el debugger, un entorno visual, la documentación o un visualizador de jerarquía de clases. Tan es así, que disponiendo del navegador Netscape 2.0 no se necesita ni tan siquiera el JDK (a petición de varios amigos que disfrutan del uso de Linux pero no disponen de soporte ELF para poder utilizar el JDK portado por Randy Chapman, les indicaré como conseguir utilizar el compilador embebido en Netscape).

Windows La versión del JDK para Windows es un archivo autoextraible. Se necesitan alrededor de 6 Mb de espacio libre en disco. Ejecutar el fichero, que desempaquetará el contenido del archivo. El directorio donde se instale no es importante, pero supondremos que se instala en el raiz del disco C:, en cuyo caso los archivos colgarán de c:\java. Es necesario añadir c:\java\bin a la variable de entorno PATH. Además de los ficheros java, el JDK incluye dos librerías dinámicas, MSVCRT20.DLL y MFC30.DLL, que se instalarán en el directorio de Java. Si tienes ninguna copia de estos ficheros en tu ordenador (probablemente en el directorio system de Windows) copia estos ficheros en el directorio c:\java\bin. Si estos ficheros ya están en tu ordenador, elimina las copias extra que instala el JDK.

Solaris La versión del JDK para Solaris es un fichero tar comprimido. Se necesitan alrededor de 9 Mb de disco para descomprimir el JDK, aunque el doble de espacio sería una cifra más cómoda. Ejecutar los siguientes comandos: % uncompress JDK-beta-solaris2-sparc.tar.Z % tar xvf JDK-beta-solaris2-sparc-tar

Puedes descomprimir el archivo en tu directorio home, o, si tienes privilegios de supervisor, en algún sitio más conveniente de /usr/local para que todos los usuarios tengan acceso a los ficheros. Sin embargo, los privilegios del supervisor no son necesarios para instalar y ejecutar Java. Por simplicidad, supondré que has descomprimido el JDK en /usr/local, aunque el path completo

donde se haga no tiene relevancia (también es posible colocarlo en /opt que es donde residen todas las aplicaciones de Solaris). Si lo has colocado en un sitio diferente, simplemente sustituye / usr/local por ese directorio (si lo has descomprimido en tu home, puedes utilizar ~/java y ~/hotjava, en vez del path completo). Es necesario añadir /usr/local/java/bin a la variable de entorno PATH. Utiliza el siguiente comando (suponiendo que tengas el shell csh o tcsh): set path=($PATH /usr/local/java/bin)

También puedes añadir esta línea al final del fichero .profile y .cshrc, y ya tienes el sistema listo para ejecutar applets. Si quieres desembarazarte de la ventana que aparece cada vez que lances el appletviewer con la licencia de Sun, crea un directorio que se llame .hotjava en el directorio java/bin y ya no volverás a verla.

Linux Necesitas un kernel que soporte binarios ELF, por lo tanto tu Linux debe ser la versión 1.2.13 o superior, las anteriores tienen un bug que hacen que javac no funcione. Necesitas también Netscape, versión 2.0b4 o posterior. Sobre la versión 1.2.13 del kernel de Linux, hay que seguir los pasos que indico para conseguir que JDK funcione: ● ●

Bajarse el JDK, linux.jdk-1.0-try4.static-motif.tar.gz y linux.jdk-1.0-try1.common.tar.gz a / usr/local, descomprimirlo y hacer 'tar xvf' En el fichero .java_wrapper (si no existe, crearlo) cambiar las variable J_HOME y PRG, para que queden como: J_HOME=/usr/local/java PRG=/usr/local/java/bin



● ● ● ●

Bajarse la librería libc.5.2.18.bin.tar.gz a /, descomprimirla, hacer 'tar xvf'. Asegurarse de que /lib/libc.so.5 es un link simbólico a este nuevo fichero. Si no lo es, hacer el /lib 'ln -s libc.so.5.2.18 libc.so.5' Bajarse ld-so.1.7.14.tar.gz a un directorio temporal, descomprimirlo y hacer 'tar xvf'. Ejecutar 'instldso.sh' y eliminar el directorio temporal. Añadir /usr/local/java a la variable de entorno PATH. Si se desea que esté fijada para todos los usuarios, incorporar el directorio a la varible PATH que se fija en el fichero /etc/profile. Bajarse netscape-v202-export.i486-unknown-linux.tar.z a /usr/local/netscape, descomprimirlo y hacer 'tar xvf' Crear un link en /usr/local/bin a /usr/local/netscape/netscape

Esto debería ser suficiente para compilar cualquier cosa en Java/Linux. En caso de tener problemas, es el momento de recurrir a las FAQ. Siguiendo los pasos indicados ya se puede ejecutar el ejemplo del Tic-Tac-Toe que propone la hoja de instalación que Sun ha incluido en todas sus versiones y que en Linux consistiría en cambiarse al directorio de la demo: % cd /usr/local/java/demo/TicTacToe

ejecutar el visualizador de applets sobre la página html: % appletviewer example1.html

y a jugar a las tres en raya. Por cierto, que el algoritmo que usa el ordenador está falseado por lo que es posible ganarle.

Compilación sin JDK Parece raro, pero se puede conseguir. Lo único necesario es el navegador Netscape 2.0. Este navegador, junto con la máquina virtual Java (JVM) y el sistema run-time, tiene un compilador Java. Si no se dispone del Java Development Kit (JDK), que no está disponible para todas las plataformas, pero sí de la versión de Netscape para nuestra plataforma, aquí van los pasos a seguir para utilizar el compilador de Java embebido en Netscape. Como necesito partir de algún punto para tomarlo como referencia, voy a suponer que estamos sobre Linux y que vamos a prescindir del JDK de Randy Chapman. Lo que habría que hacer sería lo siguiente. Primero. Instalar Netscape en el ordenador. Asegurarse de entender perfectamente y leerse hasta el final el fichero README, para seguir las instrucciones específicas de la instalación de Netscape en la plataforma y que Netscape funcione perfectamente. En nuestro caso, en que vamos a intentar compilar código Java con Netscape sobre Linux, la pieza clave es la situación del fichero moz2_0.zip, que en mi máquina está en /usr/local/netscape/java/classes. Segundo. Extraer de una copia cualquiera del JDK (aunque sea de otra plataforma), el fichero java/lib/classes.zip y guardarlo en el mismo sitio que el fichero moz2_0.zip; esta localización no es necesaria, pero simplifica la estructura. Tercero. Fijar la variable de entorno CLASSPATH para que Netscape pueda encontrar sus propias clases además de las clases del Java de Sun. Asegurarse de incluir el "directorio actual", para poder compilar a la vez que se usan los ficheros .zip de Netscape y Sun. Por ejemplo: setenv CLASSPATH .:/usr/local/netscape/java/classes/moz2_0.zip: /usr/local/netscape/java/classes/classes.zip

Cuarto. Compilar el código Java (applet o aplicación) con el comando: netscape -java sun.tools.javac.Main [fichero].java

(sustituir el nombre del fichero con el código Java en vez de [fichero]). Esto convertirá el código fuente Java en byte-code, generándose el archivo [fichero].class. Quinto. Comprobar si se puede ejecutar la aplicación con el comando: netscape -java [clase]

(sustituir el nombre de la clase de la aplicación -la que contiene la rutina main- en vez de [clase]). Sexto. Si se ha compilado un applet Java, construir una página html que lo utilice para visualizarlo con el navegador en su forma normal. O también se puede visualizar utilizando el appletviewer, ejecutando: netscape -java sun.applet.AppletViewer [clase]

Desgraciadamente, la sentencia anterior no parece funcionar en todos los sistemas. Hay amigos míos que no han sido capaces de visualizar applets con este método. Para aprovechar el tiempo, se puede crear un script que recoja los pasos 3, 4 y 6. Si estamos utilizando el csh, el contenido del script sería: #/bin/csh -f setenv CLASSPATH .:/usr/local/netscape/java/classes/moz2_0.zip: /usr/local/netscape/java/classes/classes.zip netscape -java sun.tools.javac.Main $1

y lo almacenaríamos como javac. Se ha de hacer el script ejecutable y cambiar /bin/csh por el path completo donde esté situado el csh. De forma semejante podemos definir el intérprete java y el appletviewer, sustituyendo la línea adecuada de llamada a Netscape.

PROGRAMACION EN JAVA Cuando se programa en Java, se coloca todo el código en métodos, de la misma forma que se escriben funciones en lenguajes como C.

Comentarios En Java hay tres tipos de comentarios: // comentarios para una sola línea /* comentarios de una o más líneas */ /** comentario de documentación, de una o más líneas */

Los dos primeros tipos de comentarios son los que todo programador conoce y se utilizan del mismo modo. Los comentarios de documentación, colocados inmediatamente antes de una declaración (de variable o función), indican que ese comentario ha de ser colocado en la documentación que se genera automáticamente cuando se utiliza la herramienta de Java, javadoc. Dichos comentarios sirven como descripción del elemento declarado permitiendo generar una documentación de nuestras clases escrita al mismo tiempo que se genera el código. En este tipo de comentario para documentación, se permite la introducción de algunos tokens o palabras clave, que harán que la información que les sigue aparezca de forma diferente al resto en la documentación.

Identificadores Los identificadores nombran variables, funciones, clases y objetos; cualquier cosa que el programador necesite identificar o usar. En Java, un identificador comienza con una letra, un subrayado (_) o un símbolo de dólar ($). Los siguientes caracteres pueden ser letras o dígitos. Se distinguen las mayúsculas de las minúsculas y no hay longitud máxima. Serían identificadores válidos: identificador nombre_usuario Nombre_Usuario _variable_del_sistema $transaccion

y su uso sería, por ejemplo: int contador_principal; char _lista_de_ficheros; float $cantidad_en_Ptas;

Palabras clave Las siguientes son las palabras clave que están definidas en Java y que no se pueden utilizar como indentificadores: abstract boolean

continue default

for goto

new null

switch synchronized

break byte byvalue case catch char class const

do double else extends false final finally float

if implements import instanceof int interface long native

package private protected public return short static super

this threadsafe throw transient true try void while

Palabras Reservadas Además, el lenguaje se reserva unas cuantas palabras más, pero que hasta ahora no tienen un cometido específico. Son: cast operator

future outer

generic rest

inner var

Literales Un valor constante en Java se crea utilizando una representación literal de él. Java utiliza cinco tipos de elementos: enteros, reales en coma flotante, booleanos, caracteres y cadenas, que se pueden poner en cualquier lugar del código fuente de Java. Cada uno de estos literales tiene un tipo correspondiente asociado con él. Enteros: byte short int long Por ejemplo:

8 bits 16 bits 32 bits 64 bits 21 077

0xDC00

complemento complemento complemento complemento

a dos a dos a dos a dos

Reales en coma flotante: float double Por ejemplo:

32 bits 64 bits 3.14 2e12

3.1E12

a

\u????

IEEE 754 IEEE 754

Booleanos: true false

Caracteres: Por ejemplo:

\t

[????] es un númerounicode

Cadenas: Por ejemplo:

"Esto es una cadena literal"

Arrays Se pueden declarar en Java arrays de cualquier tipo: char s[]; int iArray[];

Incluso se pueden construir arrays de arrays: int tabla[][] = new int[4][5];

Los límites de los arrays se comprueban en tiempo de ejecución para evitar desbordamientos y la corrupción de memoria. En Java un array es realmente un objeto, porque tiene redefinido el operador []. Tiene una función miembro: length. Se puede utilizar este método para conocer la longitud de cualquier array. int a[][] = new int[10][3]; a.length; /* 10 */ a[0].length; /* 3 */

Para crear un array en Java hay dos métodos básicos. Crear un array vacío: int lista[] = new int[50];

o se puede crear ya el array con sus valores iniciales: String nombres[] = { "Juan","Pepe","Pedro","Maria" };

Esto que es equivalente a: String nombres[]; nombres = new String[4]; nombres[0] = new String( nombres[1] = new String( nombres[2] = new String( nombres[3] = new String(

"Juan" ); "Pepe" ); "Pedro" ); "Maria" );

No se pueden crear arrays estáticos en tiempo de compilación: int lista[50];

// generará un error en tiempo de compilación

Tampoco se puede rellenar un array sin declarar el tamaño con el operador new: int lista[]; for( int i=0; i < 9; i++ ) lista[i] = i;

Es decir, todos los arrays en Java son estáticos. Para convertir un array en el equivalente a un array dinámico en C/C++, se usa la clase vector, que permite operaciones de inserción, borrado, etc. en el array.

Operadores Los operadores de Java son muy parecidos en estilo y funcionamiento a los de C. En la siguiente tabla aparecen los operadores que se utilizan en Java, por orden de precedencia: . ++ ! * + > > ^ ||

()

op=

(*=

instanceof % >>> =

/=

==

%=

!=

+=

Los operadores numéricos se comportan como esperamos: int + int = int

-=

etc.)

,

Los operadores relacionales devuelven un valor booleano. Para las cadenas, se pueden utilizar los operadores relacionales para comparaciones además de + y += para la concatenación: String nombre = "nombre" + "Apellido";

El operador = siempre hace copias de objetos, marcando los antiguos para borrarlos, y ya se encargará el garbage collector de devolver al sistema la memoria ocupada por el objeto eliminado.

Separadores Sólo hay un par de secuencias con otros caracteres que pueden aparecer en el código Java; son los separadores simples, que van a definir la forma y función del código. Los separadores admitidos en Java son: () - paréntesis. Para contener listas de parámetros en la definición y llamada a métodos. También se utiliza para definir precedencia en expresiones, contener expresiones para control de flujo y rodear las conversiones de tipo. {} - llaves. Para contener los valores de matrices inicializadas automáticamente. También se utiliza para definir un bloque de código, para clases, métodos y ámbitos locales. [] - corchetes. Para declarar tipos matriz. También se utiliza cuando se referencian valores de matriz. ; - punto y coma. Separa sentencias. , - coma. Separa identificadores consecutivos en una declaración de variables. También se utiliza para encadenar sentencias dentro de una sentencia for. . - punto. Para separar nombres de paquete de subpaquetes y clases. También se utiliza para separar una variable o método de una variable de referencia.

CONTROL DE FLUJO Muchas de las sentencias de control del flujo del programa se han tomado del C:

Sentencias de Salto if/else if( Boolean ) { sentencias; } else { sentencias; }

switch switch( expr1 ) { case expr2: sentencias; break; case expr3: sentencias; break; default: sentencias; break; }

Sentencias de Bucle Bucles for for( expr1 inicio; expr2 test; expr3 incremento ) { sentencias; }

El siguiente trocito de código Java que dibuja varias líneas en pantalla alternando sus colores entre rojo, azul y verde. Este fragmento sería parte de una función Java (método): int contador; for( contador=1; contador VERTICAL || orient < HORIZONTAL )

throw new IllegalArgumentException( "Orientación ilegal" ); if( orientacion != orient ) { orientacion = orient; sepDim = new Dimension( sepDim.height,sepDim.width ); invalidate(); } } public Dimension preferredSize() { return sepDim; } public Dimension minimumSize() { return sepDim; } public void paint( Graphics g ) { int x1,y1,x2,y2; Rectangle bbox = bounds(); Color c = getBackground(); Color brillo = c.brighter(); Color oscuro = c.darker(); if( orientacion == HORIZONTAL ) { x1 = 0; x2 = bbox.width - 1; y1 = y2 = bbox.height/2 - 1; } else { x1 = x2 = bbox.width/2 - 1; y1 = 0; y2 = bbox.height - 1; } g.setColor( oscuro ); g.drawLine( x1,y1,x2,y2 );

}

g.setColor( brillo ); if( orientacion == HORIZONTAL ) g.drawLine( x1,y1+1,x2,y2+1 ); else g.drawLine( x1+1,y1,x2+1,y2 ); }

El código que mostramos a continuación EjSeparador.java, muestra un ejemplo de uso de nuestro separador recién creado: import java.awt.*; import java.applet.Applet; public class EjSeparador extends Applet { public final static int HORIZONTAL = 0; public final static int VERTICAL = 1; public void init() { setLayout( new BorderLayout() ); TextField texto1 = new TextField( "Hola",20 ); TextField texto2 = new TextField( "que tal",20 );

Separador raya = new Separador( 8,2,HORIZONTAL );

}

add( "North",texto1 ); add( "Center",raya ); add( "South",texto2 ); }

Y ahora podemos complicar nuestro Componente, o utilizarlo como base para el desarrollo de otros más complejos. Por ejemplo, vamos a implementar un Componente de Grupo, que extenderemos de la clase Panel y que la utilizaremos para enmarcar a otros Componentes, tal como se utiliza en otros entornos de ventanas. En el código fuente de la implementación del Componente, Grupo.java, podemos observar que las acciones seguidas son exactamente las mismas que en el caso anterior: sobreescribir los métodos que necesitamos y punto. import java.awt.*; public class Grupo extends Panel { String Rotulo; int altoTexto; int longTexto; int offset; static Font defFuente; FontMetrics metFuente; public Grupo( String titulo ) { altoTexto = 20; longTexto = 20; offset = 10; Rotulo = titulo; setLayout( null ); metFuente = getFontMetrics( defFuente ); } public void paint( Graphics g ) { Dimension d = size(); Font fuente = getFont(); if( fuente != null ) metFuente = getFontMetrics( fuente ); longTexto = metFuente.stringWidth( Rotulo ); altoTexto = metFuente.getHeight(); g.setColor( Color.gray ); g.drawRect( 0, altoTexto/2,d.width-3,d.height-altoTexto/2-3 ); g.setColor( Color.white ); g.drawRect( 1,altoTexto/2+1,d.width-3,d.height-altoTexto/2-3 ); g.setColor( getBackground() ); g.drawLine( offset,altoTexto/2,offset+longTexto+12,altoTexto/2 ); g.drawLine( offset,altoTexto/2+1,offset+longTexto+12,altoTexto/2+1 ); g.setColor( getForeground() ); g.drawString( Rotulo,offset+6,altoTexto-3 ); } public boolean handleEvent( Event evt ) { return( super.handleEvent( evt ) ); } public Dimension preferredSize() { return( minimumSize() ); } public Dimension minimumSize() { return( new Dimension( 100,100 ) );

} static { defFuente = new Font( "Dialog",0,12 ); } }

Un ejemplo de uso, lo podremos observar compilando y ejecutando el código que se puestra a continuación, EjGrupo.java: import java.awt.*; import java.applet.Applet public class EjGrupo extends Applet { Label etiq; Button izqda; Button dcha; Button todo; public void init() { setBackground( Color.lightGray ); setLayout( null ); Grupo g = new Grupo( "Etiqueta" ); add( g ); g.setFont( new Font("Dialog",0,14 ) ); g.reshape( insets().left + 10,insets().top + 20,290,40 ); g.setLayout( new FlowLayout( FlowLayout.CENTER,10,10 ) ); etiq = new Label( "Hola Java!" ); g.add( etiq );

}

Grupo g2 = new Grupo( "Botones" ); add( g2 ); g2.reshape( insets().left+10,80,290,60 ); izqda = new Button( "Botn 1" ); g2.add( izqda ); izqda.reshape( 20,20,70,30 ); dcha = new Button( "Bot n 2" ); g2.add( dcha ); dcha.reshape( 110,20,70,30 ); todo = new Button( "Bot n 3" ); g2.add( todo ); todo.reshape( 200,20,70,30 ); }

CONTENEDORES Container es una clase abstracta derivada de Component, que representa a cualquier componente que pueda contener otros componentes. Se trata, en esencia, de añadir a la clase Component la funcionalidad de adición, sustracción, recuperación, control y organización de otros componentes. El AWT proporciona cuatro clases de Contenedores: ● ● ● ●

Window Frame Dialog Panel

Además de estos Contenedores, la clase Applet también es un Contenedor, es un subtipo de la clase Panel y puede tener Componentes.

Window Es una superficie de pantalla de alto nivel (una ventana). Una instancia de la clase Window no puede estar enlazada o embebida en otro Contenedor. Una instancia de esta clase no tiene ni título ni borde.

Frame Es una superficie de pantalla de alto nivel (una ventana) con borde y título. Una instancia de la clase Frame puede tener una barra de menú. Una instancia de esta clase es mucho más aparente y más semejante a lo que nosotros entendemos por ventana.

Dialog Es una superficie de pantalla de alto nivel (una ventana) con borde y título. Una instancia de la clase Dialog no puede existir sin una instancia asociada de la clase Frame.

Panel Es un Contenedor genérico de Componentes. Una instancia de la clase Panel, simplemente proporciona un Contenedor al que ir añadiendo Componentes.

Crear un Contenedor Antes de poder incorporar Componentes a la interface de usuario que se desea implementar, el programador debe crear un Contenedor. Cuando se construye una aplicación, el programador debe crear en primer lugar una instancia de la clase Window o de la clase Frame. Cuando lo que se construye es un applet, ya existe un Frame (la ventana del navegador). Debido a que la clase Applet está derivada de la clase Panel, el programador puede ir añadiendo Componentes directamente a la instancia que se crea de la clase Applet. En el siguiente ejemplo se crea un Frame vacío. El título del Frame, que corresponderá al título de la ventana, se fija en la llamada al constructor. Un Frame inicialmente está invisible, para poder verlo es necesario invocar al método show(): import java.awt.*; public class Ejemplo1 {

public static void main( String args[] ) { Frame f = new Frame( "Ejemplo 1" );

}

f.show(); }

En el código de ejemplo que sigue, extendemos el código anterior para que la nueva clase sea una subclase de la clase Panel. En el método main() de esta nueva clase se crea una instancia de ella y se le incorpora un objeto Frame llamando al método add(). El resultado de ambos ejemplos es idéntico a efectos de apariencia en pantalla: import java.awt.*; public class Ejemplo2 extends Panel { public static void main( String args[] ) { Frame f = new Frame( "Ejemplo 2" ); Ejemplo2 ej = new Ejemplo2();

}

f.add( "Center",ej ); f.pack(); f.show(); }

Derivando la nueva clase directamente de la clase Applet en vez de Panel, nuestro ejemplo puede ahora ejecutarse tanto como una aplicación solitaria como dentro de una página HTML en un navegador. El código siguiente muestra esta circunstancia: import java.awt.*; public class Ejemplo3 extends java.applet.Applet { public static void main( String args[] ) { Frame f = new Frame( "Ejemplo 3" ); Ejemplo3 ej = new Ejemplo3();

}

f.add( "Center",ej ); f.pack(); f.show(); }

Un objeto Window, y en algunos casos incluso un objeto Dialog, pueden reemplazar al objeto Frame. Son Contenedores válidos y los Componentes se añaden en ellos del mismo modo que se haría sobre un Frame.

Añadir Componentes a un Contenedor Para que la interface sea útil, no debe estar compuesta solamente por Contenedores, éstos deben tener Componentes en su interior. Los Componentes se añaden al Contenedor invocando al método add() del Contenedor. Este método tiene tres formas de llamada que dependen del manejador de composición o layout manager que se vaya a utilizar sobre el Contenedor. En el código siguiente, incorporamos dos botones al código del último ejemplo. La creación se realiza en el método init() porque éste siempre es llamado automáticamente al inicializarse el applet. De todos modos, al inciarse la ejecución se crean los botones, ya que el método init() es llamado tanto por el navegador como por el método main(): import java.awt.*; public class Ejemplo4 extends java.applet.Applet {

public void init() { add( new Button( "Uno" ) ); add( new Button( "Dos" ) ); } public static void main( String args[] ) { Frame f = new Frame( "Ejemplo 4" ); Ejemplo4 ej = new Ejemplo4(); ej.init();

}

f.add( "Center",ej ); f.pack(); f.show(); }

CREACION DE APLICACIONES CON AWT Para crear una aplicación utilizando AWT, vamos a ver en principio cómo podemos generar el interface de esa aplicación, mostrando los distintos elementos del AWT, posteriormente volveremos hacia la implementación de la aplicación. Interface ● ● ● ●

Crear el Marco de la aplicación (Frame) Inicializar Fuentes, Colores, Layouts y demás recursos Crear menús y Barras de Menús Crear los controles, diálogos, ventanas, etc.

Implementación ● ● ●

Incorporar controladores de eventos Incorporar funcionalidad (threads, red, etc.) Incorporar un controlador de errores (excepciones)

CREAR EL MARCO DE LA APLICACION El Contenedor de los Componentes es el Frame. También es la ventana principal de la aplicación, lo que hace que para cambiar el icono o el cursor de la aplicación no sea necesario crear métodos nativos; al ser la ventana un Frame, ya contiene el entorno básico para la funcionalidad de la ventana principal. Vamos a empezar a crear una aplicación básica, a la que iremos incorporando Componentes. Quizás vayamos un poco de prisa en las explicaciones que siguen; no preocuparse, ya que lo que no quede claro ahora, lo estará más tarde. El problema es que para poder profundizar sobre algunos aspectos de Java, necesitamos conocer otros previos, así que proporcionaremos un ligero conocimiento sobre algunas características de Java y del AWT, para que nos permitan entrar a fondo en otras; y ya conociendo estas últimas, volveremos sobre las primeras. Un poco lioso, pero imprescindible. En el archivo AplicacionAWT.java, se encuentra el código completo de la aplicación que vamos ir construyendo a lo largo de este repaso general por las características de que dispone el AWT. Comenzaremos el desarrollo de nuestra aplicación básica con AWT a partir del código que mostramos a continuación: import java.awt.*; public class AplicacionAWT extends Frame { static final int HOR_TAMANO = 300; static final int VER_TAMANO = 200; public AplicacionAWT() { super( "Aplicación Java con AWT" ); pack(); resize( HOR_TAMANO,VER_TAMANO ); show(); } public static void main( String args[] ) { new AplicacionAWT();

}

}

La clase anterior es un Frame, ya que extiende esa clase y hereda todas sus características. Tiene un método, el constructor, que no admite parámetros. Además, se hace una llamada explícita al constructor super, es decir, al constructor de la clase padre, para pasarle como parámetro la cadena que figurará como el título de la ventana. La llamada a show() es necesaria, ya que por defecto, los Contenedores del AWT se crean ocultos y hay que mostrarlos explícitamente. La llamada a pack() hace que los Componentes se ajusten a sus tamaños correctos en función del Contenedor en que se encuentren situados. La ejecución de la aplicación mostrará la siguiente ventana en pantalla:

Los atributos fundamentales de la ventana anterior son: ● ● ● ● ● ● ●

Marco de 300x200 pixels No tiene barra de menú No tiene ningún Componente Título "Aplicación Java con AWT" Color de fondo por defecto Layout por defecto Fondo de la ventana vacío

INICIALIZAR FUENTES, COLORES Y RECURSOS Vamos a ir alterando los recursos de la ventana de la aplicación Java que estamos desarrollando con el AWT, para ir incorporando y visualizando los distintos Componentes que proporciona AWT. Insertemos algunas líneas de código en el constructor para inicializar la aplicación: ●

Cambiemos el font de caracteres a Times Roman de 12 puntos setFont( new Font( "TimesRoman",Font.PLAIN,12 ) );



Fijemos los colores de la ventana para que el fondo sea Blanco y el texto resalte en Negro setBackground( Color.white ); setForeground( Color.black );



Seleccionemos como disposición de los Componentes el BorderLayout para este Contenedor setLayout( new BorderLayout() );



Incorporemos gráficos. Usamos métodos definidos en la clase Graphics; por ejemplo, reproduzcamos el título de la ventana en medio con una fuente Time Roman de 24 puntos en color Azul. Es necesario utilizar new con Font ya que en Java, todo son objetos y no

podríamos utilizar un nuevo font de caracteres sin antes haberlo creado public void paint( Graphics g ) { g.setFont( new Font( "TimesRoman",Font.BOLD,24 ) ); g.setColor( Color.blue ); g.drawString( getTitle(),30,50 ); } ●

Incorporemos en la parte inferior de la ventana dos botones: Aceptar y Cancelar Panel p = new Panel(); p.add( new Button( "Aceptar" ) ); p.add( new Button( "Cancelar" ) ); add( "South",p );

Los Componentes se incorporan al Contenedor a través de los dos métodos add() que hay definidos: add( Component c ); add( String s,Component c );

Los Componentes también se podían haber insertado en el Frame, organizándolos en una cierta forma, teniendo en cuenta que su manejador de composición es un BorderLayout. Por ejemplo: add( "South",new Button( "Aceptar ) ); add( "South",new Button( "Cancelar" ) );

Hemos utilizado un Panel y no el segundo método, porque es más útil el organizar los Componentes en pequeñas secciones. Así, con nuestro código podemos considerar al Panel como una entidad separada del Frame, lo cual permitiría cambiar el fondo, layout, fuente, etc. del Panel sin necesidad de tocar el Frame. Si ejecutamos de nuevo la aplicación con los cambios que hemos introducido, aparecerá ante nosotros la ventana que se muestra a continuación.

Si intentásemos en esta aplicación cerrar la ventana, no sucede nada. Cuando se intenta cerrar la ventana, el sistema envía un evento que no se está tratando. Incorporemos pues un controlador de eventos y empecemos tratando el evento WINDOW_DESTROY, generado cuando se intenta cerrar la ventana: public boolean handleEvent( Event evt ) { switch( evt.id ) { case Event.WINDOW_DESTROY: { System.exit( 0 ); return true; } default: return false; } }



Si ahora ejecutamos de nuevo la aplicación y cerramos la ventana... Efectivamente se cierra, tal como se suponía.

CREAR MENUS Y BARRAS DE MENUS En la actual versión del AWT que se proporciona con el JDK, sólo se permite crear menús a través de código, ya que Java todavía no dispone de un formato de recursos y tampoco hay un diseñador como pueden ser AppStudio, Delphi o X-Designer; aunque terminará habiendo uno, con seguridad. No hay ningún método para diseñar una buena interface, todo depende del programador. Los menús son el centro de la aplicación. La diferencia entre una aplicación útil y otra que es totalmente frustrante radica en la organización de los menús, pero eso, las reglas del diseño de un buen árbol de menús, no están claras. Hay un montón de libros acerca de la ergonomía y de cómo se debe implementar la interacción con el usuario. Lo cierto es que por cada uno que defienda una idea, seguro que hay otro que defiende la contraria. Todavía no hay un acuerdo para crear un estándar, con cada Window Manager se publica una guía de estilo diferente. Así que, vamos a explicar lo básico, sin que se deba tomar como dogma de fe, para que luego cada uno haga lo que mejor le parezca. La interface MenuContainer solamente se puede implementar sobre un Frame. Un applet que desee tener un menú, debe crear un Frame en primer lugar. El código de la función que vamos a ver, crea una barra de menús y se llama desde el constructor del Frame. La función es private porque no queremos que se pueda llamar desde ninguna otra clase. private void InicializaMenus() { mbarra = new MenuBar(); Menu m = new Menu( "Archivo" ); m.add( new MenuItem( "Nuevo") ); m.add( new MenuItem( "Abrir") ); m.add( new MenuItem( "Guardar") ); m.add( new MenuItem( "Guardar como") ); m.add( new MenuItem( "Imprimir") ); m.addSeparator(); m.add( new MenuItem( "Salir") ); mbarra.add( m ); m = new Menu( "Ayuda" ); m.add( new MenuItem( "Ayuda!" ) ); m.addSeparator(); m.add( new MenuItem( "Acerca de..." ) ); mbarra.add( m ); setMenuBar( mbarra ); }

El menú que crea esta función tendrá la apariencia que muestra la figura siguiente:

Los eventos generados por las opciones de un menú se manejan del mismo modo que los Botones, Listas, etc. En el caso de menús, es el evento ACTION_EVENT de la clase java.awt.MenuItem el que se genera y en evt.target se nos indica la opción seleccionada.

case Event.ACTION_EVENT: { if( evt.target instanceof MenuItem ) { if( "Nuevo".equals( evt.arg ) ) AplicacionAWT aAwt = new AplicacionAWT();; if( "Abrir".equals( evt.arg ) ) System.out.println( "Opcion -Abrir-" ); if( "Guardar".equals( evt.arg ) ) System.out.println( "Opcion -Guardar-" ); if( "Guardar como".equals( evt.arg ) ) System.out.println( "Opcion -Guardar como-" ); if( "Imprimir".equals( evt.arg ) ) System.out.println( "Opcion -Imprimir-" ); if( "Salir".equals( evt.arg ) ) System.exit( 0 ); if( "Ayuda!".equals( evt.arg ) ) System.out.println( "No hay ayuda" ); if( "Acerca de".equals( evt.arg ) ) System.out.println( "Opcion -Acerca de-" ); } }

En el código anterior hemos tratado los eventos del menú. Para más seguridad, aunque no sea estrictamente necesario, lo primero que hacemos es asegurarnos de que el objeto evt.target es realmente un objeto MenuItem, es decir, procede de la barra de menús; y después comprobamos la opción que se ha seleccionado. Como todo, también se puede rizar el rizo y conseguir reproducir los sistemas de menús que estamos acostumbrados a ver en las aplicaciones que manejamos habitualmente. Un ejemplo de ello son los menús en cascada, semejantes al que muestra la figura y que ha sido generado mediante la aplicación Cascada.java.

Básicamente se utiliza la técnica ya descrita, pero en vez de crear un nuevo MenuItem se crea un nuevo Menu, lo que origina el menú en cascada. ●

No obstante, y volviendo al diseño de interfaces, no debe abusarse de este tipo de menús, porque pueden crear mucha más confusión al usuario. Siempre se debe tener en mente que los usuarios tienen que navegar habitualmente por una gran cantidad de menús en las más diversas aplicaciones, por lo que no debemos esconderles demasiado las opciones que les pueden interesar.

DIALOGOS Y VENTANAS Una Ventana genérica, Window, puede utilizarse simplemente para que sea la clase padre de otras clases y se puede intercambiar por un Diálogo, Dialog, sin pérdida de funcionalidad. No se puede decir lo mismo de un Frame.

Se podría crear un menú pop-up con una Ventana, pero lo cierto es que en esta versión del JDK hay un montón de bugs y no merece la pena el enfrascarse en el intento. No obstante, hay ciertos métodos que están en la clase Window y que no están presentes en otras clases que pueden resultar interesantes y necesitar una Ventana si queremos emplearlos. Son: ● ● ● ● ●

getToolkit() getWarningString() pack() toBack() toFront()

Un Diálogo es una subclase de Window, que puede tener un borde y ser modal, es decir, no permite hacer nada al usuario hasta que responda al diálogo. Esto es lo que se usa en las cajas de diálogo "Acerca de...", en la selección en listas, cuando se pide una entrada numérica, etc. El código Java que se expone a continuación, implementa el diálogo Acerca de para la aplicación. Esta clase se crea oculta y necesitaremos llamar al método show() de la propia clase para hacerla visible. class AboutDialog extends Dialog { static int HOR_TAMANO = 300; static int VER_TAMANO = 150; public AboutDialog( Frame parent ) { super( parent,"Acerca de...",true ); this.setResizable( false ); setBackground( Color.gray ); setLayout( new BorderLayout() ); Panel p = new Panel(); p.add( new Button( "Aceptar" ) ); add( "South",p ); resize( HOR_TAMANO,VER_TAMANO ); } public void paint( Graphics g ) { g.setColor( Color.white ); g.drawString( "Aplicación Java con AWT", HOR_TAMANO/4,VER_TAMANO/3 ); g.drawString( "Versión 1.00", HOR_TAMANO/3+15,VER_TAMANO/3+20 ); } public boolean handleEvent( Event evt ) { switch( evt.id ) { case Event.ACTION_EVENT: { if( "Aceptar".equals( evt.arg ) ) { hide(); return true; } } default: return false; } } }

La ventana que aparece en pantalla generada por la clase anterior es la que muestra la figura:

Las aplicaciones independientes deberían heredar tomando como padre la ventana principal de esa aplicación. Así pueden implementar la interface MenuContainer y proporcionar menús. No hay razón aparente para que sea una subclase de la clase Frame, pero si se quiere proporcionar funcionalidad extra, sí debería serlo, en vez de serlo de su padre: Window. Esto es así porque Frame implementa la interface MenuContainer, con lo cual tiene la posibilidad de proporcionar menús y cambiar el cursor, el icono de la aplicación, etc. Un ejemplo más complicado de aplicación gráfica basada en el AWT es el convertidor de decimal a binario/octal/hexadecimal/base36, Convertidor.java, cuya presentación en pantalla es la que muestra la figura siguiente.



En la construcción de esta nueva aplicación se utilizan elementos que se presentan en profundidad en secciones posteriores de este Tutorial.

PANELES La clase Panel es el más simple de los Contenedores de Componentes gráficos. En realidad, se trataba de crear una clase no-abstracta (Container sí lo es) que sirviera de base a los applet y a otras pequeñas aplicaciones. La clase Panel consta de dos métodos propios: el constructor, cuyo fin es crear un nuevo Panel con un LayoutManager de tipo FlowLayout (el de defecto), y el método addNotify() que, sobrecargando la función del mismo nombre en la clase Container, llama al método createPanel () del Toolkit adecuado, creando así un PanelPeer. El AWT enviará así al Panel (y por tanto al applet) todos los eventos que sobre él ocurran. Esto que puede parecer un poco rebuscado, obedece al esquema arquitectónico del AWT; se trata del bien conocido esquema de separación interface/implementación que establece por un lado una clase de interface y por otro distintas clases de implementación para cada una de las plataformas elegidas. El uso de Paneles permite que las aplicaciones puedan utilizar múltiples layouts, es decir, que la disposición de los componentes sobre la ventana de visualización pueda modificarse con mucha flexibilidad. Permite que cada Contenedor pueda tener su propio esquema de fuentes de caracteres, color de fondo, zona de diálogo, etc. Podemos, por ejemplo, crear una barra de herramientas para la zona superior de la ventana de la aplicación o incorporarle una zona de estado en la zona inferior de la ventana para mostrar información útil al usuario. Para ello vamos a implementar dos Paneles: class BarraHerram extends Panel { public BarraHerram() { setLayout( new FlowLayout() ); add( new Button( "Abrir" ) ); add( new Button( "Guardar" ) ); add( new Button( "Cerrar" ) );

}

Choice c = new Choice(); c.addItem( "Times Roman" ); c.addItem( "Helvetica" ); c.addItem( "System" ); add( c ); add( new Button( "Ayuda" ) ); }

class BarraEstado extends Panel { Label texto; Label mas_texto; public BarraEstado() { setLayout( new FlowLayout() ); add( texto = new Label( "Creada la barra de estado" ) ); add( mas_texto = new Label( "Información adicional" ) ); } public void verEstado( String informacion ) { texto.setText( informacion ); } }

Ahora, para dar funcionalidad, debemos crear los objetos correspondientes a la barra de herramientas y a la barra de estado con new; al contrario que en C++, en Java todos los objetos deben ser creados con el operador new:

add( "North",tb = new ToolBar() ); add( "South",sb = new StatusBar() );

También vamos a incorporar un nuevo evento a nuestro controlador, para que maneje los eventos de tipo ACTION_EVENT que le llegarán cuando se pulsen los botones de la barra de herramientas o se realice alguna selección, etc. case Event.ACTION_EVENT: { be.verEstado( evt.arg.toString() ); return true; }

Cuando la aplicación reciba este tipo de evento, alterará el contenido de la barra de estado para mostrar la información de la selección realizada o el botón pulsado.

Al final, la apariencia de la aplicación en pantalla es la que presenta la figura anterior.

LAYOUTS Los layout managers o manejadores de composición, en traducción literal, ayudan a adaptar los diversos Componentes que se desean incorporar a un Panel, es decir, especifican la apariencia que tendrán los Componentes a la hora de colocarlos sobre un Contenedor. Java dispone de varios, en la actual versión, tal como se muestra en la imagen:

¿Por qué Java proporciona estos esquemas predefinidos de disposición de componentes? La razón es simple: imaginemos que deseamos agrupar objetos de distinto tamaño en celdas de una rejilla virtual: si confiados en nuestro conocimiento de un sistema gráfico determinado, codificamos a mano tal disposición, deberemos preveer el redimensionamiento del applet, su repintado cuando sea cubierto por otra ventana, etc., además de todas las cuestiones relacionadas con un posible cambio de plataforma (uno nunca sabe a donde van a ir a parar los propios hijos, o los applets). Sigamos imaginando, ahora, que un hábil equipo de desarrollo ha previsto las disposiciones gráficas más usadas y ha creado un gestor para cada una de tales configuraciones, que se ocupará, de forma transparente para nosotros, de todas esas cuitas de formatos. Bien, pues estos gestores son instancias de las distintas clases derivadas de Layout Manager y que se utilizan en el applet que genera la figura siguiente, donde se muestran los diferentes tipos de layouts que proporciona el AWT. En el applet que genera la figura siguiente, se utilizan los diferentes tipos de layouts que proporciona el AWT.

El ejemplo AwtGui.java, ilustra el uso de paneles, listas, barras de desplazamiento, botones, selectores, campos de texto, áreas de texto y varios tipos de layouts. En el tratamiento de los Layouts se utiliza un método de validación, de forma que los Componentes son marcados como no válidos cuando un cambio de estado afecta a la geometría o cuando el Contenedor tiene un hijo incorporado o eliminado. La validación se realiza automáticamente cuando se llama a pack() o show(). Los Componentes visibles marcados como no válidos no se validan automáticamente. En el ejemplo de control de eventos que se muestra a continuación, la llamada a validate() hace que se

realicen en un bloque, en un solo paso, todos los cálculos necesarios para la validación del layout. public boolean action( Event evt,Object obj ) { if( obj.equals( "Cambia Font" ) ) { boton1.setFont( nuevoFont ); boton2.setFont( nuevoFont ); etiqueta.setFont( nuevoFont ); campoTexto.setFont( nuevoFont );

}

validate(); return true; }

FlowLayout Es el más simple y el que se utiliza por defecto en todos los Paneles si no se fuerza el uso de alguno de los otros. Los Componentes añadidos a un Panel con FlowLayout se encadenan en forma de lista. La cadena es horizontal, de izquierda a derecha, y se puede seleccionar el espaciado entre cada Componente. Por ejemplo, podemos poner un grupo de botones con la composición por defecto que proporciona FlowLayout:

import java.awt.*; import java.applet.Applet; public class AwtFlow extends Applet { Button boton1,boton2,boton3; public void init() { boton1 = new Button( "Aceptar" ); boton2 = new Button( "Abrir" ); boton3 = new Button( "Cerrar" );

}

add( boton1 ); add( boton2 ); add( boton3 ); }

Este código, AwtFlow.java, construye tres botones con un pequeño espacio de separación entre ellos.

BorderLayout La composición BorderLayout (de borde) proporciona un esquema más complejo de colocación de los Componentes en un panel. La composición utiliza cinco zonas para colocar los Componentes sobre ellas: Norte, Sur, Este, Oeste y Centro. Es el layout o composición que se utilizan por defecto Frame y Dialog. El Norte ocupa la parte superior del panel, el Este ocupa el lado derecho, Sur la zona inferior y Oeste el lado izquierdo. Centro representa el resto que queda, una vez que se hayan rellenado las otras cuatro partes. Con BorderLayout se podrían representar botones de dirección:

import java.awt.*; import java.applet.Applet; public class AwtBord extends Applet { Button botonN,botonS,botonE,botonO,botonC; public void init() { setLayout( new BorderLayout() ); botonN botonS botonE botonO botonC

}

add( add( add( add( add( }

= = = = =

new new new new new

Button( Button( Button( Button( Button(

"Norte" ); "Sur" ); "Este" ); "Oeste" ); "Centro" );

"North",botonN ); "South",botonS ); "East",botonE ); "West",botonO ); "Center",botonC );

Este es el código, AwtBord.java, que genera el applet de botones de dirección:

GridLayout La composición GridLayout proporciona gran flexibilidad para situar Componentes. El layout se crea con un número de filas y columnas y los Componentes van dentro de las celdas de la tabla así definida. En la figura siguiente se muestra un panel que usa este tipo de composición para posicionar seis botones en su interior, con tres filas y dos columnas que crearán las seis celdas necesarias para albergar los botones:

import java.awt.*; import java.applet.Applet; public class AwtGrid extends Applet { Button boton1,boton2,boton3,boton4,boton5,boton6; public void init() { setLayout( new GridLayout( 3,2 ) ); boton1 boton2 boton3 boton4 boton5 boton6

}

add( add( add( add( add( add( }

= = = = = =

new new new new new new

boton1 boton2 boton3 boton4 boton5 boton6

Button( Button( Button( Button( Button( Button(

"1" "2" "3" "4" "5" "6"

); ); ); ); ); );

); ); ); ); ); );

Este es el código, AwtGrid.java, que genera la imagen del ejemplo.

GridBagLayout Es igual que la composición de GridLayout, con la diferencia que los Componentes no necesitan tener el mismo tamaño. Es quizá el layout más sofisticado y versátil de los que actualmente soporta AWT. En la figura siguiente vemos la imagen generada por un panel que usa el GridBagLayout para soportar diez botones en su interior:

import java.awt.*; import java.applet.Applet; public class AwtGBag extends Applet {

public void init() { GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints gbc = new GridBagConstraints(); setLayout( gridbag ); gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1.0; Button boton0 = new Button( "Botón 0" ); gridbag.setConstraints( boton0,gbc ); add( boton0 ); Button boton1 = new Button( "Botón 1" ); gridbag.setConstraints( boton1,gbc ); add( boton1 ); Button boton2 = new Button( "Botón 2" ); gridbag.setConstraints( boton2,gbc ); add( boton2 ); gbc.gridwidth = GridBagConstraints.REMAINDER; Button boton3 = new Button( "Botón 3" ); gridbag.setConstraints( boton3,gbc ); add( boton3 ); gbc.weightx = 0.0; Button boton4 = new Button( "Botón 4" ); gridbag.setConstraints( boton4,gbc ); add( boton4 ); gbc.gridwidth = GridBagConstraints.RELATIVE; Button boton5 = new Button( "Botón 5" ); gridbag.setConstraints( boton5,gbc ); add( boton5 ); gbc.gridwidth = GridBagConstraints.REMAINDER; Button boton6 = new Button( "Botón 6" ); gridbag.setConstraints( boton6,gbc ); add( boton6 ); gbc.gridwidth = 1; gbc.gridheight = 2; gbc.weighty = 1.0; Button boton7 = new Button( "Botón 7" ); gridbag.setConstraints( boton7,gbc ); add( boton7 );

}

gbc.weighty = 0.0; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.gridheight = 1; Button boton8 = new Button( "Botón 8" ); gridbag.setConstraints( boton8,gbc ); add( boton8 ); Button boton9 = new Button( "Botón 9" ); gridbag.setConstraints( boton9,gbc ); add( boton9 ); }

Este es el código, AwtGBag.java, que utilizamos para generar la imagen del ejemplo. Para aprovechar de verdad todas las posibilidades que ofrece este layout, hay que pintar antes en papel como van a estar posicionados los Componentes; utilizar gridx, gridy, gridwidth y gridheight en vez de GridBagConstraints.RELATIVE, porque en el proceso de validación del layout pueden quedar todos los Componentes en posición indeseable. Además, se deberían crear métodos de conveniencia para hacer más fácil el posicionamiento de los Componentes. En el ejemplo siguiente,

AwtGBagConv.java, creamos el método de conveniencia addComponente() para la incorporación de nuevos Componentes al layout, lo que hace más sencillo el manejo de los Constraints: import java.awt.*; import java.applet.Applet; public class AwtGBagConv extends Applet { GridBagLayout gridbag = new GridBagLayout(); void addComponente( Component comp,int gridx,int gridy, int gridw,int gridh ) { GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = gridx; gbc.gridy = gridy; gbc.gridwidth = gridw; gbc.gridheight = gridh; gridbag.setConstraints( comp,gbc ); add( comp ); } public void init() { setLayout( gridbag addComponente( new addComponente( new addComponente( new addComponente( new addComponente( new }

); Label( "Nombre:" ),0,0,1,1 ); TextField( 12 ),1,0,2,1 ); TextArea( 5,20 ),0,1,2,2 ); Checkbox( "Sí?" ),2,1,1,1 ); List(),2,2,1,1 );

}

CardLayout Este es el tipo de composición que se utiliza cuando se necesita una zona de la ventana que permita colocar distintos Componentes en esa misma zona. Este layout suele ir asociado con botones de lista (Choice), de tal modo que cada selección determina el panel (grupo de componentes) que se presentarán. En la figura siguiente mostramos el efecto de la selección sobre la apriencia de la ventana que contiene el panel con la composición CardLayout:

import java.awt.*; import java.applet.Applet; public class AwtCard extends Applet { Panel card; final static String PanelBoton = "Panel con Botones"; final static String PanelTexto = "Panel con Campo de Texto"; public void init() { setLayout( new BorderLayout() );

Panel ac = new Panel(); Choice c = new Choice(); c.addItem( PanelBoton ); c.addItem( PanelTexto ); ac.add( c ); add( "North",ac ); card = new Panel(); card.setLayout( new CardLayout() ); Panel p1 = new Panel(); p1.add( new Button( "Botón 1" ) ); p1.add( new Button( "Botón 2" ) ); p1.add( new Button( "Botón 3" ) ); Panel p2 = new Panel(); p2.add( new TextField( "Texto",20 ) ); card.add( PanelBoton,p1 ); card.add( PanelTexto,p2 ); add( "Center",card ); } public boolean action( Event evt,Object arg ) { if( evt.target instanceof Choice ) { ( (CardLayout)card.getLayout() ).show( card,(String)arg ); return true; } return false; } }

Este es el código, AwtCard.java, que hemos utilizado para generar las dos imágenes que muestran el funcionamiento de la composición CardLayout.

CREAR UN LAYOUT Se puede crear un Layout personalizado en base a la interface LayoutManager. Hay que redefinir los cinco métodos que utiliza este interface, lo cual puede no resultar sencillo, así que en lo posible se deben utilizar los métodos de colocación de componentes que proporciona AWT, fundamentalmente en el momento en que parezca que ninguno de los Layouts que hemos visto satisface nuestras exigencias, deberíamos volver a comprobar que el GridBagLayout, que es el más flexible, de verdad no cumple nuestros requerimientos. No obstante, vamos a implementar un layout propio, MiLayout.java, para poder colocar los Componentes en posiciones absolutas del panel que contenga a este layout. Derivamos nuestro nuevo layout de LayoutManager y redefinimos los cinco métodos de la clase para que podamos posicionar los Componentes. import java.awt.*; public class MiLayout implements LayoutManager { public MiLayout() { } public void addLayoutComponent( String name,Component comp ) { }

public void removeLayoutComponent( Component comp ) { } public Dimension preferredLayoutSize( Container parent ) { Insets insets = parent.insets(); int numero = parent.countComponents(); int ancho = 0; int alto = 0; for( int i=0; i < numero; i++ ) { Component comp = parent.getComponent( i ); Dimension d = comp.preferredSize(); Point p = comp.location(); if( ( p.x + d.width ) > ancho ) ancho = p.x + d.width; if( ( p.y + d.height ) > alto ) alto = p.y + d.height; } return new Dimension( insets.left + insets.right + ancho, insets.top + insets.bottom + alto ); } public Dimension minimumLayoutSize( Container parent ) { Insets insets = parent.insets(); int numero = parent.countComponents(); int ancho = 0; int alto = 0; for( int i=0; i < numero; i++ ) { Component comp = parent.getComponent( i ); Dimension d = comp.preferredSize(); Point p = comp.location(); if( ( p.x + d.width ) > ancho ) ancho = p.x + d.width; if( ( p.y + d.height ) > alto ) alto = p.y + d.height; } return new Dimension( insets.left + insets.right + ancho, insets.top + insets.bottom + alto ); } public void layoutContainer( Container parent ) { int numero = parent.countComponents();

}

for( int i=0; i < numero; i++ ) { Component comp = parent.getComponent( i ); Dimension d = comp.preferredSize(); comp.resize( d.width,d.height ); } }

Y ahora vamos a ver un ejemplo en que utilicemos nuestro Layout. Posicionaremos tres botones en el panel y un campo de texto con una etiqueta precediéndolo. La apriencia que tendrá en pantalla será la que se muestra en la figura:

import java.awt.*; import java.applet.Applet; public class AwtLibre extends Applet { Button boton1,boton2,boton3; Label etiqueta; TextField texto; public void init() { setLayout( new MiLayout() ); boton1 = new Button( "Aceptar" ); boton2 = new Button( "Abrir" ); boton3 = new Button( "Cerrar" ); etiqueta = new Label( "Texto" ); texto = new TextField( "",20 ); add( add( add( add( add(

}

boton1 ); boton2 ); boton3 ); etiqueta ); texto );

boton1.move( 0,10 ); boton2.move( 70,10 ); boton3.move( 30,40 ); etiqueta.move( 75,70 ); texto.move( 120,70 ); }

Este código, AwtLibre.java, es el que genera la imagen anterior creando un applet que utiliza nuestro recién creado layout.

CONTROL DE EVENTOS En el pasado, un programa que quisiera saber lo que estaba haciendo el usuario, debía recoger la información él mismo. En la práctica, esto significaba que una vez inicializado, el programa entraba en un gran bucle en el que continuamente se bloqueaba para comprobar que el usuario estuviese haciendo algo interesante (por ejemplo, pulsar un botón, pulsar una tecla, mover una barra o mover el ratón) y tomar las acciones oportunas. Esta técnica se conoce como polling. El polling funciona, pero se vuelve demasiado difícil de manejar con las aplicaciones modernas por dos razones fundamentales: Primero, el uso de polling tiende a colocar todo el código de control de eventos en una única localización (dentro de un gran bucle); segundo, las interacciones dentro del gran bucle tienden a ser muy complejas. Además, el polling necesita que el programa esté ejecutando el bucle, consumiendo tiempo de CPU, mientras está esperando a que el usuario se decida a hacer algo, lo que supone un gran despilfarro de recursos. El AWT resuelve estos problemas abrazando un paradigma diferente, en el que están basados todos los sistemas modernos de ventanas: la orientación a eventos. Dentro del AWT, todas las acciones que pueda realizar el usuario caen dentro de un gran saco que son los eventos. Un evento describe, con suficiente detalle, una acción particular del usuario. En lugar de que el programa activamente recoja todos los eventos generados por el usuario, el sistema Java avisa al programa cuando se produce un evento interesante.

LA CLASE Event Un contenedor soltado en un entorno gráfico se convierte en rápido receptor de eventos de todo tipo, singularmente de los relacionados con el movimiento del ratón, pulsaciones de teclas, creación/movimiento/destrucción de partes gráficas y, por último, los referidos a acciones del usuario respecto de componentes (elección de un menú, pulsación de un botón, etc.). La clase Event es el jugador principal en el juego de los eventos. Intenta capturar las características fundamentales de todos los eventos que genera el usuario. Los datos miembro de la clase Event son los que se indican a continuación: id - El tipo de evento que se ha producido target - Componente sobre el que se ha producido el evento x, y - Las coordenadas en donde se ha producido el evento relativas al Componente que actualmente está procesando ese evento. El origen se toma en la esquina superior izquierda del Componente key - Para eventos de teclado, es la tecla que se ha pulsado. Su valor será el valor Unicode del carácter que representa la tecla. Otros valores que puede tomas son los de las teclas especiales como INICIO, FIN, F1, F2, etc. when - Instante en que se ha producido el evento modifiers - La combinación aritmética del estado en que se encuentran las teclas modificadoras Mays, Alt, Ctrl. clickCount - El número de clicks de ratón consecutivos. Sólo tiene importancia en los eventos MOUSE_DOWN arg - Es un argumento dependiente del evento. Para objetos Button, este objeto arg es un objeto String que contiene la etiqueta de texto del botón evt - El siguiente evento en una lista encadenada de eventos Una instancia de la clase Event será creada por el sistema Java cada vez que se genere un evento. Es posible, sin embargo, que un programa cree y envíe eventos a los Componentes a través de su método postEvent().

TIPOS DE EVENTOS Los eventos se catalogan por su naturaleza, que se indicará en el miembro id de su estructura. Los grandes grupos de eventos son:

Eventos de Ventana Son los que se generan en respuesta a los cambios de una ventana un frame o un dialogo. ● ● ● ● ●

WINDOW_DESTROY WINDOW_EXPOSE WINDOW_ICONIFY WINDOW_DEICONIFY WINDOW_MOVED

Eventos de Teclado Son generados en respuesta a cuando el usuario pulsa y suelta una tecla mientras un Componente tiene el foco de entrada. ● ● ● ●

KEY_PRESS KEY_RELEASE KEY_ACTION KEY_ACTION_RELEASE

Eventos de Ratón Son los eventos generados por acciones sobre el ratón dentro de los límites de un Componente. ● ● ● ● ● ●

MOUSE_DOWN MOUSE_UP MOUSE_MOVE MOUSE_ENTER MOUSE_EXIT MOUSE_DRAG

Eventos de Barras Son los eventos generados como respuesta a la manipulación de barras de desplazamiento (scrollbars). ● ● ● ● ●

SCROLL_LINE_UP SCROLL_LINE_DOWN SCROLL_PAGE_UP SCROLL_PAGE_DOWN SCROLL_ABSOLUTE

Eventos de Lista Son los eventos generados al seleccionar elementos de una lista. ● ●

LIST_SELECT LIST_DESELECT

Eventos Varios Son los eventos generados en función de diversas acciones. ● ● ● ● ●

ACTION_EVENT LOAD_FILE SAVE_FILE GOT_FOCUS LOST_FOCUS

El applet EventosPrnt.java está diseñado para observar los eventos que se producen sobre él. Cada vez que se genera un evento, el applet responde imprimiendo el evento que ha capturado en la línea de comandos desde donde se ha lanzado el applet. Una vez que se haya compilado el código y cargado el applet en el appletviewer o en un navegador con soporte Java, jugar un poco con el applet. Mover el cursor dentro del applet, picar con el ratón, picar y arrastrar, teclear algo, cambiar el tamaño de la ventana y taparla y destaparla con otra. Las acciones anteriores harán que en la ventana en donde se haya lanzado el appletviewer, o en la consola Java en caso de Netscape, vayan apareciendo los textos que indican los eventos que está recibiendo el applet. Lo cierto es que el uso de System.out.println() en un applet es algo que no debería utilizarse, e incluso puede llegar a no funcionar en algunos sistemas, pero tenía la ventaja de ser la forma más fácil de ver los eventos. No obstante, vamos a reescribir el código del applet utilizando una Lista. Una Lista es una lista de cadenas o Strings definidas en java.awt.List. Crearemos una lista de 25 líneas y no permitiremos selección múltiple, que son los dos parámetros que necesita el constructor del objeto List. El código EventosList.java que se muestra a continuación corresponde al anterior ejemplo un poco modificado. import java.awt.*; import java.applet.Applet; public class EventosList extends Applet { List lista; public void init() { lista = new List( 25,false ); add( lista ); lista.addItem( "Evento init" ); } public void start() { lista.addItem( "Evento start" ); } public void destroy() { lista.addItem( "Evento destroy" ); } public void paint( Graphics g ) { lista.addItem( "Evento paint" ); } public void update( Graphics g ) { lista.addItem( "Evento update" ); } public boolean mouseUp( Event evt,int x, int y ) { lista.addItem( "Evento mouseUp en ("+x+","+y+")" ); return false;

} public boolean mouseDown( Event evt,int x, int y ) { lista.addItem( "Evento mouseDown en ("+x+","+y+")" ); return false; } public boolean mouseDrag( Event evt,int x, int y ) { lista.addItem( "Evento mouseDrag en ("+x+","+y+")" ); return false; } public boolean mouseMove( Event evt,int x, int y ) { lista.addItem( "Evento mouseMove en ("+x+","+y+")" ); return false; } public boolean mouseEnter( Event evt,int x, int y ) { lista.addItem( "Evento mouseEnter en ("+x+","+y+")" ); return false; } public boolean mouseExit( Event evt,int x, int y ) { lista.addItem( "Evento mouseExit" ); return false; } public boolean keyDown( Event evt,int x ) { lista.addItem( "Evento keyDown,carácter "+(char)x ); return true; } public void getFocus() { lista.addItem( "Evento getFocus" ); } public void gotFocus() { lista.addItem( "Evento gotFocus" ); } public void lostFocus() { lista.addItem( "Evento lostFocus" ); } }

El applet VisorEventos.java muestra los datos que componen cada evento que se produce. Hemos incorporado un botón y una lista, tal como se puede apreciar en la figura, para poder generar diferentes eventos.

Moviendo el ratón o actuando sobre los dos Componentes, botón y lista, podemos observar los datos que el sistema Java envía en la recolección de esos eventos.

GENERACION Y PROPAGACION DE EVENTOS Tomemos el applet, EventosPro.java, que aparece en la figura siguiente. Consta de dos instancias de la clase Button, embebidas dentro de una instancia de la clase Panel. Esta instancia está a su vez embebida dentro de otra instancia de la clase Panel. Esta última instancia de la clase Panel está situada junto a una instancia de la clase TextArea, y ambas están embebidas dentro de una instancia de la clase Applet.

La figura siguiente presenta los elementos que conforman este applet en forma de árbol, con el TextArea y Button como hojas y la instancia de Applet como raiz.

Cuando un usuario interactúa con el applet, el sistema Java crea una instancia de la clase Event y

rellena sus datos miembro con la información necesaria para describir la acción. Es en ese momento cuando el sistema Java permite al applet controlar el evento. Este control comienza por el Componente que recibe inicialmente el evento (por ejemplo, el botón que ha sido pulsado) y se desplaza hacia arriba en el árbol de Componentes, componente a componente, hasta que alcanza al Contenedor de la raíz del árbol. Durante este camino, cada Componente tiene oportunidad de ignorar el evento o reaccionar ante él en una (o más) de las forma siguientes: ● ● ●

Modificar los datos miembros de la instancia de Event Entrar en acción y realizar cálculos basados en la información contenida en el evento Indicar al sistema Java que el evento no debería propagarse más arriba en el árbol

El sistema Java pasa información del evento a un Componente a través del método handleEvent() del Componente. Todos los métodos handleEvent() deben ser de la forma: public boolean handleEvent( Event evt )

Un controlador de eventos solamente necesita una información: una referencia a la instancia de la clase Event que contiene la información del evento que se ha producido. El valor devuelto por el método handleEvent() es importante. Indica al sistema Java si el evento ha sido o no completamente controlado por el controlador. Un valor true indica que el evento ha sido controlado y que su propagación debe detenerse. Un valor false indica que el evento ha sido ignorado, o que no ha sido controlado en su totalidad y debe continuar su propagación hacia arriba en el árbol de Componentes. Veamos la descripción de una acción con el applet de la figura anterior. El usuario pulsa el botón "Uno". El sistema run-time del lenguaje Java capturará la información sobre el evento (el número de clicks, la localización del click, la hora en que se ha producido la pulsación y el Componente que ha recibido el click) y empaqueta todos esos datos en una instancia de la clase Event. El sistema Java comienza entonces por el Componente que ha sido pulsado (en este caso, el botón "Uno") y, a través de una llamada al método handleEvent() del Componente, ofrece a éste la posibilidad de reaccionar ante el evento. Si el Componente no controla el evento, o no lo hace completamente (indicado por un valor de retorno false), el sistema Java presentará la instancia de Event al siguiente Componente por encima en el árbol (en este caso, una instancia de la clase Panel). El sistema Java continúa de este mismo modo hasta que el evento es controlado en su totalidad o ya no hay Componentes a los que informar. En la figura siguiente mostramos el camino recorrido por el evento en su intento de que algún Componente lo controle.

Cada Componente del applet añade una línea al objeto TextArea indicando que ha recibido un evento. Luego permite que el evento se propague al siguiente Componente. El código del controlador de eventos usado en el ejemplo es el que muestran las siguientes líneas: public boolean handleEvent( Event evt) { if( evt.id == Event.ACTION_EVENT ) ta.appendText( "Panel " + str + " recibe action...\n" ); else if( evt.id == Event.MOUSE_DOWN ) ta.appendText( "Panel " + str + " recibe mouse_down...\n" );

return super.handleEvent( evt ); }

METODOS DE CONTROL DE EVENTOS El método handleEvent() es un lugar para que el programador pueda insertar código para controlar los eventos. A veces, sin embargo, un Componente solamente estará interesado en eventos de un cierto tipo (por ejemplo, eventos del ratón). En estos casos, el programador puede colocar el código en un método de ayuda, en lugar de colocarlo en el método handleEvent(). No hay métodos de ayuda para ciertos tipos de eventos, aquí está la lista de los que están disponibles para los programadores: action( Event evt,Object obj ) gotFocus( Event evt,Object obj ) lostFocus( Event evt,Object obj ) mouseEnter( Event evt,int x,int y ) mouseExit( Event evt,int x,int y ) mouseMove( Event evt,int x,int y ) mouseUp( Event evt,int x,int y ) mouseDown( Event evt,int x,int y ) mouseDrag( Event evt,int x,int y ) keyDown( Event evt,int key ) keyUp( Event evt,int key )

false indicará que el método de ayuda no maneja el evento. La implementación del método handleEvent() proporcionada por la clase Component invoca a cada método de ayuda. Por esta razón, es importante que las implementaciones redefinidas del método handleEvent() en clases derivadas, siempre finalicen con la sentencia: return( super.handleEvent( evt ) );

El siguiente trozo de código ilustra esta regla. public boolean handleEvent( Event evt ) { if( evt.target instanceof MiBoton ) { // Hace algo... return true; } return( super.handleEvent( evt ) ); }

No seguir esta regla tan simple hará que no se invoquen adecuadamente los métodos de ayuda. El applet EventosRaton.java, que controla los eventos de ratón exclusivamente a través de código insertado en sus métodos de ayuda; va dibujando una línea (rubber band) entre el último punto donde se ha producido un click de ratón y la posición actual del cursor.

ACTION_EVENT Algunos de los eventos que más frecuentemente tendremos que controlar son los siguientes: ACTION_EVENT MOUSE_DOWN

KEY_PRESS WINDOW_DESTROY En la documentación de la clase Event se encuentra toda la lista de eventos que cualquier aplicación puede necesitar manejar y su documentación; como ejemplo de uso vamos a detenernos en el primero de ellos, ACTION_EVENT. Como ejemplo del manejo de eventos vamos a ver este evento que se provoca al pulsar un botón, seleccionar un menú, etc. Para su control podemos manejarlo en el método handleEvent() o en el método action(). Los dos métodos anteriores pertenecen a la clase Component por lo que todas las clases derivadas de ésta contendrán estos dos métodos y se pueden sobrecargar para que se ajuste su funcionamiento a lo que requiere nuestra aplicación. Veamos el siguiente ejemplo, en que se controla este evento a través del método handleEvent(), que es el método general de manejo de eventos: public boolean handleEvent( Event evt ) { switch( evt.id ) { case Event.ACTION_EVENT: // evt.arg contiene la etiqueta del botón pulsado // o el item del menú que se ha seleccionado if( ( "Pulsado "+n+" veces" ).equals( evt.arg ) ) return( true ); default: return( false ); } }

Pero en este caso, cuando se produce este evento se llama al método action(), que sería: public boolean action( Event evt,Object arg ) { if( ( "Pulsado "+n+" veces" ).equals( arg ) ) return( true ); return( false ); }

Como se puede comprobar, incluso si las etiquetas cambian se puede recibir el evento. Los ejemplos anteriores corresponden al control de un evento producido por un botón que cambia su etiqueta cada vez que se pulsa. Aunque esta no es la única forma de manejar eventos; de hecho se puede hacer: if( evt.target == miBoton )

en donde se comparan objetos en lugar de etiquetas.

MEJORAR EL DISEÑO DE INTERFACES La interface de usuario es el aspecto más importante de una aplicación, tal como ya hemos repetido. Un diseño pobre de la interface es un grave problema para que el usuario pueda obtener todo el partido posible de la aplicación. Para ser efectivos, no debemos limitarnos a colocar una serie de botones, etiquetas y barras de desplazamiento sobre la pantalla. Desafortunadamente, nadie ha determinado una reglas correctas para del diseño de una buena interface. Los diseñadores del AWT parece que se pusieron como meta principal que las clases del AWT funcionasen correctamente, dejando un poco de lado su apariencia. Sin embargo, han proporcionado suficientes mecanismos para poder alterar la apariencia de los Componentes del AWT, muchas veces no de forma sencilla, pero ahí están para que los programadores podamos alterar la visualización de los Componentes sobre nuestro interface. Vamos a ver unas cuantas formas sencillas de alterar y dar un mejor aspecto a nuestros diseños. ● ● ● ● ●

Cambio de Font de Caracteres Colores de Fondo y Texto Fijar el Tamaño Preferido Uso de Insets Habilitar y Deshabilitar Componentes

CAMBIO DE FUENTE DE CARACTERES El font de caracteres con el que se presenta un texto en pantalla influye mucho en el impacto de una interface. Una interface efectiva no debería utilizar una maraña de fuentes, pero sí que debería utilizar dos o tres diferentes para aumentar el atractivo y la efectividad de los textos. El applet Fuentes.java, tal como se muestra en la figura, ilustra este extremo.

Utiliza tres tipos de fonts de caracteres (en diferente estilo y diferente tamaño) para llamar la atención del usuario sobre las tres zonas de la interface. La fuente por defecto para todos los Componentes es la fuente Dialog. Java proporciona otras fuentes con propósitos más especializados, el número exacto de fuentes depende de la plataforma, por ello, se puede utilizar el applet ListaFuentes.java para obtener una lista de las fuentes de caracteres disponibles en el sistema.

ListaFuentes.class

Cuando un programador necesita presentar un Componente en pantalla, como un objeto TextArea, en

una fuente de caracteres distinta a la de defecto, la nueva fuente debe seleccionarse mediante el método setFont(): public void setFont( Font f )

El método setFont() espera como parámetro una fuente. En el siguiente trozo de código vemos cómo se usa: TextArea ta = new TextArea(); Font f = new Font( "Helvetica",Font.ITALIC,12 ); ta.setFont( f );

Este código con ligeras modificaciones funcionará para cualquier Componente. Si se cambia la fuente de un Contenedor, todos los Componentes colocados dentro del Contenedor automáticamente adoptarán la nueva fuente de caracteres. El siguiente código, CambioFuentes.java, muestra esta circunstancia: import java.awt.*; import java.applet.Applet; public class CambioFuentes extends Applet { public static void main( String args[] ) { Frame fr = new Frame( "Cambio de Fuentes" ); CambioFuentes cf = new CambioFuentes(); Font f = new Font( "Helvetica",Font.ITALIC,12 ); fr.setFont( f ); fr.setLayout( new FlowLayout() ); Button b = new Button( "Hola" ); fr.add( b ); Checkbox cb = new Checkbox( "Púlsame" ); fr.add( cb ); TextArea ta = new TextArea(); fr.add( ta );

} ●

fr.pack(); fr.show(); }

La fuente de caracteres solamente se indica para el objeto Frame, el botón, la caja y el área de texto también utilizarán esta fuente.

COLORES DE FONDO Y TEXTO El impacto visual del color nunca debe ser desestimado cuando se ataca el diseño de una interface de usuario. El color tiende a atraer la visión y puede utilizarse para llamr la atención sobre una parte importante del interface. En el ejemplo siguiente, el color rojo alrededor del botón hace que la vista se fije inmediatamente en él.

La clase Component proporciona dos métodos para modificar el color de un Componente. A través de los métodos setBackground() y setForeground(), se pueden indicar los colores del fondo y del

texto, respectivamente: public void setBackground( Color c ) public void setForeground( Color c )

Ambos métodos solamente necesitan un parámetro, un objeto Color. A continuación mostramos un ejemplo de su uso: TextArea ta = new TextArea(); ta.setBackground( Color.blue ); ta.setForeground( Color.red );

Este código funcionará con ligeras modificaciones para casi todos los Componentes del AWT. Si se cambia el Color de un Contenedor, todos los Componentes colocados dentro de ese Contenedor, automáticamente adoptan el nuevo color. El applet, CambioColor.java, ilustra este punto. El Color solamente se fija para el color de fondo del objeto Frame; el botón, la caja y el área de texto usarán ese mismo color de fondo. import java.awt.*; import java.applet.Applet; public class CambioColor extends Applet { public static void main( String args[] ) { Frame fr = new Frame( "Cambio de Color" ); CambioColor cc = new CambioColor(); fr.setBackground( Color.red ); fr.setLayout( new FlowLayout() ); Button b = new Button( "Hola" ); fr.add( b ); Checkbox cb = new Checkbox( "Púlsame" ); fr.add( cb ); TextArea ta = new TextArea(); fr.add( ta );

} ●

fr.pack(); fr.show(); }

La calidad de soporte del color varía mucho de una plataforma a otra. Bajo Windows '95, la clase Button ignora totalmente los comandos de color y se empeña en permanecer bajo un patrón de grises. Por otro lado, el fondo de la clase Label parece ser transparente. Algunos Componentes no se presentan en pantalla con un mismo color para un mismo objeto Color. Bajo Windows '95, un fondo de color naranja aparece como naranja en muchos Componentes (excepto en los botones), pero se presenta como amarillo cuando se trata de objetos TextArea o TextField. El soporte del color en Solaris parece ser mucho más consistente.

FIJAR EL TAMAÑO PREFERIDO Otro ingrediente importante de una buena interface es el tamaño con el cual aparecerá un Componente o Contenedor en la pantalla. En el corazón del control de la composición del interface está el layout manager, que es capaz de fijar el tamaño y la posición de los Componentes que se vayan incorporando al layout que está manejando. Esto, indirectamente, también influye en el tamaño del Contenedor. En este caso, en lugar de llamar a un método para indicar cuál debe ser el tamaño de un Componente,

hay que derivar una nueva clase del Componente y redefinir el método preferredSize() que devolverá el tamaño preferido. El layout manager llama al método preferredSize() para determinar cuál debe ser el tamaño preferido para cada Componente. Hay que redefinir el método: public Dimension preferredSize()

Uno puede estar tentado a utilizar el método resize() o el método reshape() para especificar un tamaño, pero no debe hacerse. Ambos métodos son usados directamente por el layout manager, y los ajustes de tamaño se reclacularán la próxima vez que el layout manager tenga que recomponer la posición de los Componentes sobre el Contenedor. Las siguientes líneas de código demuestran el uso del método preferredSize(), convenientemente redefinido en una clase derivada. Este método crea un nuevo objeto Dimension con la altura y anchura especificadas y se lo devuelve a quien lo ha llamado (normalmente el layout manager). public Dimension preferredSize() { return new Dimension( 200,100 ); }

Desde luego, no hay nada para evitar que el tamaño preferido de un botón varíe dinámicamente, como ocurre en el applet CambioBoton.java, mostrado en la figura:

Este applet contiene dos botones. Pulsando sobre uno de ellos se provoca el cambio de tamaño en el otro. El método preferredSize() del applet de la figura anterior utiliza la variable dim, que es una variable privada: public Dimension preferredSize() { return new Dimension( dim ); }

La variable dim puede ser fijada o cambiada vía una función miembro como la siguiente: public void newPreferredSize( Dimension dim ) { this.dim = new Dimension( dim ); resize( dim ); } ●

El Contenedor debe llamar al método anterior antes de su llamada al método layout() para resituar los Componentes después de que haya cambiado el tamaño de uno de ellos.

USO DE Insets Insets, o borde interior, al igual que el tamaño preferido, se puede utilizar para proporcionar a la interface de usuario una mayor ordenación espacial. El insets de un Contendor, especifica la cantidad de espacio alrededor del borde interior del Contenedor donde no se situará ningún Componente, estará vacío. El borde interior se especifica para un Contenedor, redefiniendo su método insets(): public Insets insets()

El método insets() no necesita ningún parámetro y devuelve una instancia del objeto Insets. La clase Insets tiene cuatro campos, que especifican el número de pixels que constituirán el borde interior desde arriba, izquierda, abajo y derecha, respectivamente. A continuación mostramos el uso típico de insets(). El código de este ejemplo indica que los Componentes contenidos en el layout deberán estar situados a 5 unidades desde cualquiera de los bordes del Contenedor. public Insets insets() { return new Insets( 5,5,5,5 ); }

En la figura siguiente se muestra el efecto que provoca en la apariencia del interface el uso de insets().



HABILITAR Y DESHABILITAR COMPONENTES Los Componentes de una interface de usuario que no están actualmente disponibles pero pueden estarlo, en función de alguna acción del usuario, se dice que están deshabilitados. Se presentan con su rótulo en gris y no responden a las acciones del usuario. El deshabilitar Componentes es mucho mejor que ocultarlos, porque el usuario puede ver las operaciones que puede realizar, aunque en ese momento no las tenga disponibles. La clase Component proporciona tres métodos para llevar a cabo la habilitación y deshabilitación de Componentes: public void enable() public void disable() public void enable( boolean condicionBooleana )

Los primeros dos métodos habilitan y deshabilitan un Componente. El tercer método también hace eso dependiendo del valor de un parámetro booleano. Veamos un ejemplo: Button b = new Button( "Púlsame" ); b.enable(); // o, b.disable(); // o, b.enable( true );

Este código funcionará, con muy pocas modificaciones, sobre cualquier Componente. Para un uso efectivo de estos métodos, un programa debe monitorizar el estado de la interface de usuario. Como el usuario interactúa con la interface, su estado interno cambia. Esto hay que reflejarlo en el estado de los Componentes que pasarán de habilitados a deshabilitados, o viceversa, en función de las circunstancias. Tomemos como ejemplo ahora el applet que aparece en la figura siguiente, obtenido de la ejecución de Habilitar.java:

En este applet, los botones Añadir y Borrar están deshabilitados hasta que el usuario haya entrado en el campo de texto. Este previene una activación inadvertida de los dos botones en el caso de que no haya texto en el campo. Tan pronto como se teclee el primer carácter en el campo de texto, se habilitan los dos botones, cada cual asumiendo su propio rol. Si, en cualquier momento, el campo de texto se volviese a quedar vacío, los dos botones volverían a estar deshabilitados ●

Además de que los Componentes deshabilitados tienen una apariencia visual diferente, tampoco reciben eventos desde el sistema Java; los eventos son inmediatamente propagados al Contenedor en que está situado el Componente deshabilitado.

BOTON GRAFICO AWT adolece de herramientas ya desarrolladas para implementar interfaces al uso, con alta proliferación de imágenes y facilidades para que el programador utilice directamente Componentes. Sin embargo, sí proporciona las herramientas suficientes como para que se puedan implementar cualquier tipo de Componentes, o modificar al gusto los ya existentes. Para mostrar un ejemplo, vamos a implementar un botón gráfico, BotonGrafico.java Partimos de un botón normal al cual aplicaremos tres imágenes diferentes, para cada uno de los tres estados en que puede encontrarse: pulsado, liberado e inhabilitado.

BotonGrafico.class

En el fichero fuente, podemos comprobar que las tres imágenes se pueden pasar cómo parámetro en la llamada APPLET, por ejemplo:

Observando el código, se puede comprobar que una vez cargadas la imágenes, solamente se deben controlar los eventos del ratón, para que automáticamente se presenten las imágenes del botón adecuadas y responda correctamente. import java.awt.*; import java.applet.Applet; public class BotonGrafico extends Applet { private MediaTracker tracker; private Image imagen[] = new Image[3]; private boolean BotActivo = false; private boolean BotPulsado = false; private boolean tresImg = false; private int Estado = 0; public void init() { String istr; tracker = new MediaTracker( this );

// Recogemos las tres imágenes de los parámentros de llamada // al applet for( int i=0; i < 3; i++ ) { istr = getParameter( "IMAGEN" + i ); if( istr == null ) tresImg = false; else { // Registramos las imágenes con el Media Tracker imagen[i] = getImage( getCodeBase(),istr ); tracker.addImage( imagen[i],0 ); try { tracker.waitForAll(); } catch( InterruptedException e ) { System.out.println( "Error cargando imagen " + i ); } } } } public void start() { repaint(); } public void stop(){ } // Controlamos la pulsación del ratón public boolean mouseDown( Event evt,int x,int y ) { BotPulsado = true; repaint(); return( true ); } // Controlamos cuando el usuario suelta el botón del ratón public boolean mouseUp( Event evt,int x,int y ) { if( BotPulsado && BotActivo ) { BotPulsado = true; repaint(); } else { BotPulsado = false; repaint(); } return( true ); } // Controlamos cuando el cursor del ratón entra en el // campo de acción del applet // Presentamos un mensaje en la línea de estado public boolean mouseEnter( Event evt,int x,int y ) { BotActivo = true; showStatus( "Tutorial de Java, Boton Grafico" ); repaint(); return( true ); } // Controlamos cuando el cursor del ratón abandona el // lugar ocupado por el applet

public boolean mouseExit( Event evt,int x,int y ) { BotActivo = false; showStatus( "" ); repaint(); return( true ); } public void update( Graphics g ) { // Controlamos el estado en que se queda el botón // tras la acción que se haya hecho con el ratón if( !BotActivo ) Estado = 0; else if( BotActivo && !BotPulsado ) Estado = 1; else Estado = 2; paint( g ); } public void paint( Graphics g ) { g.drawImage( imagen[Estado],0,0,this ); } }

Objetos Gráficos En páginas anteriores ya se ha mostrado cómo escribir applets, cómo lanzarlos y los fundamentos básicos de la presentación de información sobre ellos. Ahora, pues, querremos hacer cosas más interesantes que mostrar texto; ya que cualquier página HTML puede mostrar texto. Para ello, Java proporciona la clase Graphics, que permite mostrar texto a través del método drawString(), pero también tiene muchos otros métodos de dibujo. Para cualquier programador, es esencial el entendimiento de la clase Graphics, antes de adentrarse en el dibujo de cualquier cosa en Java. Esta clase proporciona el entorno de trabajo para cualquier operación gráfica que se realice dentro del AWT. Juega dos importantes papeles: por un lado, es el contexto gráfico, es decir, contiene la información que va a afectar a todas las operaciones gráficas, incluyendo los colores de fondo y texto, la fuente de caracteres, la localización y dimensiones del rectángulo en que se va a pintar, e incluso dispone de información sobre el eventual destino de las operaciones gráficas (pantalla o imagen). Por otro lado, la clase Graphics proporciona métodos que permiten el dibujo de primitivas, figuras y la manipulación de fonts de caracteres y colores. También hay clases para la manipulación de imágenes, doble-buffering, etc. Para poder pintar, un programa necesita un contexto gráfico válido, representado por una instancia de la clase Graphics. Pero, como esta clase es abstracta, no se puede instanciar directamente; así que debemos crear un componente y pasarlo al programa como un argumento a los métodos paint() o update(). Los dos métodos anteriores, paint() y update(), junto con el método repaint() son los que están involucrados en la presentación de gráficos en pantalla. El AWT, para reducir el tiempo que necesitan estos métodos para realizar el repintado en pantalla de gráficos, tiene dos axiomas: ● ●

Primero, el AWT repinta solamente aquellos Componentes que necesitan ser repintados, bien porque estuviesen cubiertos por otra ventana o porque se pida su repintado directamente Segundo, si un Componente estaba tapado y se destapa, el AWT repinta solamente la porción del Componente que estaba oculta

En la ejecución del applet que aparece a continuación, EjemploGraf.java, podemos observar como se realiza este proceso. Ignorar la zona de texto de la parte superior del applet de momento, y centrar la mirada en la parte coloreada. Utilizando otra ventana, tapar y destapar parte de la zona que ocupa el applet. Se observará que solamente el trozo de applet que estaba cubierto es el que se repinta. Yendo un poco más allá, solamente aquellos componentes que estén ocultos y se vuelvan a ver serán los que se repinten, sin tener en cuenta su posición dentro de la jerarquía de componentes.

EjemploGraf.class

La pantalla en Java se incrementa de izquierda a derecha y de arriba hacia abajo, tal como muestra la figura:

Los pixels de la pantalla son pues: posición 0 + ancho de la pantalla - 1. En los textos, el punto de inserción se encuentra en la línea base de la primera letra.

Métodos para Dibujos Vamos a presentar métodos para dibujar varias figuras geométricas. Como estos métodos funcionan solamente cuando son invocados por una instancia válida de la clase Graphics, su ámbito de aplicación se restringe a los componentes que se utilicen en los métodos paint() y update(). Normalmente los métodos de dibujo de primitivas gráficas funcionan por pares: un método pinta la figura normal y el otro pinta la figura rellena. drawLine( x1,y1,x2,y2 ) drawRect( x,y,ancho,alto ) fillRect( x,y,ancho,alto ) clearRect( x,y,ancho.alto ) drawRoundRect( x,y,ancho,alto,anchoArco,altoArco ) fillRoundRect( x,y,ancho,alto,anchoArco,altoArco ) draw3DRect( x,y,ancho,alto,boolean elevado ) fill3DRect( x,y,ancho,alto,boolean elevado ) drawOval( x,y,ancho,alto ) fillOval( x,y,ancho,alto ) drawArc( x,y,ancho,alto,anguloInicio,anguloArco ) fillArc( x,y,ancho,alto,anguloInicio,anguloArco ) drawPolygon( int[] puntosX,int[] puntosY[],numPuntos ) fillPolygon( int[] puntosX,int[] puntosY[],numPuntos ) drawString( string s,x,y ) drawChars( char data[],offset,longitud,x,y ) drawBytes( byte data[],offset,longitud,x,y ) copyArea( xSrc,ySrc,ancho,alto,xDest,yDest )

METODOS PARA IMAGENES Los objetos Graphics pueden mostrar imágenes a través del método: drawImage( Image img,int x,int y,ImageObserver observador );

Hay que tener en cuenta que el método drawImage() necesita un objeto Image y un objeto ImageObserver. Podemos cargar una imagen desde un fichero de dibujo (actualmente sólo se soportan formatos GIF y JPEG) con el método getImage(): Image img = getImage( getDocumentBase(),"fichero.gif" );

La forma de invocar al método getImage() es indicando un URL donde se encuentre el fichero que contiene la imagen que queremos presentar y el nombre de ese fichero: getImage( URL directorioImagen,String ficheroImagen );

Un URL común para el método getImage() es el directorio donde está el fichero HTML. Se puede acceder a esa localización a través del método getDocumentBase() de la clase Applet, como ya se ha indicado. Normalmente, se realiza el getImage() en el método init() del applet y se muestra la imagen cargada en el método paint(), tal como se muestra en el ejemplo siguiente: public void init() { img = getImage( getDocumentBase(),"pepe.gif" ); } public void paint( Graphics g ) { g.drawImage( img,x,y,this ); }

En el applet Imagen.java, podemos ver el ejemplo completo. Su ponemos en él la existencia del fichero "Imagenes/pepe.gif": import java.awt.*; import sun.awt.image.URLImageSource; import java.applet.Applet; public class Imagen extends Applet { Imagen pepe; public void init() { pepe = getImage( getDocumentBase(),"Imagenes/pepe.gif" ); } public void paint( Graphics g ) { g.drawString( pepe,25,25,this ); } }

DOBLE-BUFFERING DE GRAFICOS Al mostrar gráficos con las técnicas estándar, las imágenes suelen aparecer a trozos o con parpadeo. Las aplicaciones Java permiten que los programas dibujen en memoria, para luego ir mostrando la imagen completa de forma suave. Este es el proceso conocido como doble-buffering, y tiene dos ventajas fundamentales sobre el

proceso normal de pintar o dibujar directamente sobre la pantalla: Primero, el usuario ve aparecer de golpe la imagen en la pantalla. Mientras el usuario está viendo esa imagen, el programa está generando la siguiente para mostrarla de golpe a continuación, y así una y otra vez. Segundo, la técnica de doble-buffering involucra un objeto Image, que se puede pasar directamente a varios métodos. Esta capacidad para manipular objetos Image permite descomponer las rutinas de dibujo en componentes funcionales, en lugar de un enorme método paint(). No obstante, el doble-buffering sólo debe usarse para animaciones gráficas, no como método normal. Lo usual en otras aplicaciones sería repintar la zona que interese solamente.

Contextos gráficos Para entender el doble-buffering, primero se necesita comprender qué es un contexto gráfico. Un contexto gráfico es simplemente una estructura de datos que el sistema sabe utilizar como tablero de dibujo, es decir, es la zona en que se va a pintar. Ya hemos visto y utilizado contextos gráfico en las declaraciones del método paint(): public void paint( Graphics g ) {

El objeto Graphics g es el contexto gráfico. Se utiliza g para realizar todo el dibujo en el applet. Por ejemplo: g.drawString( "¡Hola!",25,25 ); g.drawRect( 15,15,50,10 );

Entonces, Java traduce todo lo que se dibuja en g en imágenes sobre la pantalla. Para realizar doblebuffering, se necesita pues, primero crear un contexto gráfico que no es presentado inmediatamente en la pantalla.

Creación de Contextos Gráficos Crear contextos gráficos tiene dos pasos: Crear una imagen vacía con las dimensiones adecuadas y obtener un objeto Graphics de esa imagen. El objeto Graphics que se construye en el segundo paso realiza la función de contexto gráfico. Por ejemplo, CGrafico.java: import java.awt.*; import java.applet.Applet; public class CGrafico extends Applet { Image dobleBuffer; Graphics miCG; public void init() { // Inicializa el doble buffer dobleBuffer = createImage( 300,300 ); miCG = dobleBuffer.getGraphics(); // Construye un área gráfica de trabajo miCG .setColor( Color.white ); miCG.fillRect( 0,0,300,300 ); resize( 500,450 ); }

Podemos utilizar miCG para dibujar cualquier cosa. Las imágenes se trazarán en doble buffer. Cuando el dibujo esté terminado, se puede presentar el doble buffer en pantalla:

public void paint( Graphics g ) { // Sólo se tiene que presentar la imagen del buffer g.drawImage( dobleBuffer,0,0,this ); }

Utilización de Contextos Gráficos Una vez definido un contexto gráfico, podemos usarlo en cualquier parte de nuestro programa. Por ejemplo, podemos repartir la responsabilidad para dibujar sobre varias funciones: public void titulo() { // Obtiene la fuente de texto actual y la guardamos Font f = miCG.getFont(); // Seleccionamos otra fuente para el título miCG.setFont( new Font( "TimesRoman".Font.BOLD,36 ) ); miCG.drawString( "Ejemplo de Espiral",15,50 ); miCG.drawString( "Círculos",15,90 ); // Recuperamos la fuente original miCG.setFont( f ); } public void espiral() { int x,y; // Dibujamos circulos en los lados horizontales y = 100; for( x=100; x incremnto ) { g.drawLine( cX,cY,(cX+iniAncho),cY ); g.drawLine( (cX+iniAncho),Cy,(cX+iniAncho),(cY+iniAlto) ); cX += iniAncho; cY += iniAlto; iniAncho -= incremento; iniAlto -= incremento; g.drawLine( cX,cY,(cX-iniAncho),cY ); g.drawLine( (cX-iniAncho),Cy,(cX-iniAncho),(cY-iniAlto) ); cX -= iniAncho; cY -= iniAlto; iniAncho -= incremento; iniAlto -= incremento;

}

}

}

// Esta es la llamada a la clase desde un Applet Html public class EspiralTest extends Applet { Image buffer; Espiral spiral; boolean cargado = false; synchronized public void init() { // crea un buffer donde dibujar buffer = createImage( 300,300 ); // crea una nueva espiral spiral = new Espiral( buffer.getGraphics() ); // establece el punto inicial y dimensiones spiral.setTamInicial( 10,10,200,200 ); // establece el incremento spiral.setTamEspiral( 10 ); // construye la espiral spiral.creaEspiral(); } synchronized public void paint( Graphics g ) { g.drawImage( buffer,25,25,this ); } }

LA CLASE MediaTracker Si nuestro applet tiene que tratar con imágenes almacenadas en ficheros gif/jpeg, tendremos que recurrir a la clase MediaTracker. Esta clase proporciona muchos métodos para manejar objetos multimedia y grupos de objetos.

Manejo de Imágenes para Animación Combinando MediaTracker con la técnica de doble-buffering, se pueden conseguir animaciones. Estos son algunos métodos de MediaTracker útiles para ver gráficos void addImage( Image img,int id )

Marca la imagen como parte de un grupo de identificadores id. boolean checkID( int id )

Comprueba las imágenes pertenecientes al grupo id. Devuelve true si han sido cargadas todas, o false en otro caso (no carga los ficheros que falten). Se utiliza para saber cuando se han cargado todas las imágenes que componen una animación, antes de echarla a andar sobre la pantalla. boolean checkID( int id,boolean load )

Comprueba las imágenes pertenecientes al grupo id. Devuelve true si han sido cargadas todas, o false en otro caso. Carga las imágenes que falten si load es true. boolean checkAll( boolean load )

Comprueba todas las imágenes. Devuelve true si todas están cargadas, o false en otro caso. Carga las imágenes que falten si load es true. void waitForID( int id )

Espera a que se carguen todas las imágenes del grupo id. void waitForAll()

Espera a que se carguen todas las imágenes. Se debería utilizar addImage() con cada imagen que necesite un applet. MediaTracker sólo supervisa imágenes asociadas mediante el método addImage(). Si se quiere saber si un determinado gráfico ha sido cargado, se pueden utilizar los métodos check. Si se quiere que un gráfico sea cargado antes de hacer cualquier otra cosa, usar los métodos waitFor. El uso de estos métodos es especialmente útil ya que cuando realizamos la carga de una imagen con getImage(), esta carga se realiza en un thread aparte del de nuestro applet, con lo cual, aunque la imagen se encuentre en el otro lado del mundo, getImage() devuelve inmediatamente el control con lo cual podemos comenzar la ejecución de la animación, sin haberse cargado todavía todas las imágenes.

Creación de un Objeto MediaTracker Los objetos MediaTracker necesitan saber qué ImageObserver verá las imágenes que se están supervisando. Para los applets, el ImageObserver es el propio applet: miTracker = new MediaTracker( this );

Ahora ya se puede usar el objeto miTracker para manejar todas las imágenes de este applet.

Ejemplo de animación Veamos ahora un ejemplo de animación, Taza.java. Utilizaremos MediaTracker para asegurarnos de que se cargan todas las imágenes. De este modo, cuando se ejecute la animación, no se verá parpadeo o que falta algún fotograma.

Taza.class

Y el código siguiente es el que corresponde a la animación anterior: import java.awt.*; import java.applet.Applet; public class Taza extends Applet { Image Images[]; MediaTracker tracker; int index = 0; int maxAncho,maxAlto; // Componentes off-screen para el doble buffering Image offScrImage; Graphics offScrCG; boolean cargado = false; // Inicializa el applet. Establece el tamaño y carga las imágenes public void init() { // Establece el supervisor de imágenes y dimensiones tracker = new MediaTracker( this ); maxAncho = 78; maxAlto = 128; imagenes = new Image[6]; // Establece el doble buffer y cambia el tamaño del applet try { offScrImage = createImage( maxAnho,maxAlto ); offScrCG = offScrImage.getGraphics(); offScrCG.setColor( Color.lightGray ); offScrCG.fillRect( 0,0,maxAncho,maxAlto ); resize( maxAncho,maxAlto ); } catch( exception e ) { e.printStackTrace(); } // Carga las imágenes en un array for( int i=0; i < 33; i++ ) {

String imageFich = new String( "taza"+String.valueOf(i+1)+".gif" ); imagenes[i] = getImage( getDocumentBase(),imageFich ); // Pasamos esta imagen al tracker tracker.addImage( imagenes[i],i ); } try { // Utilizamos el tracker para asegurar que se // cargaran todas las imágenes tracker.waitForAll(); } catch( InterruptedException e ) { } cargado = true; } // Pinta el fotograma actual public void paint( Graphics g ) { if( cargado ) { // Copia del doble buffer a la pantalla g.drawImage( offScrImage,0,0,this ); // Hacemos una pausa y cogemos la siguiente imagen timerloop(); } } // Establecemos la primera imagen public void start() { index = 0; if( tracker.checkID( index ) ) // Pintamos en el doble buffer offScrCG.drawImage( imagenes[index],0,0,this ); } // Actualiza los fotogramas para que avance la animación public void timerloop() { // Se asegura que la imagen esté presente y la mete en el buffer if( tracker.checkID( index ) ) { // Borra el fondo y obtiene la siguiente imagen offScrCG.fillRect( 0,0,100,100 ); offScrCG.drawImage( imagenes[index],0,0,this ); index++;

}

// Vuelve al principio de la animación if( index javac melon.java > java melon java.lang.ArithmeticException: / by zero at melon.main(melon.java:5)

Las excepciones predefinidas, como ArithmeticException, se conocen como excepciones runtime. Actualmente, como todas las excepciones son eventos runtime, sería mejor llamarlas excepciones irrecuperables. Esto contrasta con las excepciones que generamos explícitamente, que suelen ser mucho menos severas y en la mayoría de los casos podemos recuperarnos de ellas. Por ejemplo, si un fichero no puede abrirse, preguntamos al usuario que nos indique otro fichero; o si una estructura de datos se encuentra completa, podremos sobreescribir algún elemento que ya no se necesite.

EXCEPCIONES PREDEFINIDAS Las excepciones predefinidas y su jerarquía de clases es la que se muestra en la figura:

Los nombres de las excepciones indican la condición de error que representan. Las siguientes son las excepciones predefinidas más frecuentes que se pueden encontrar: ArithmeticException Las excepciones aritméticas son típicamente el resultado de una división por 0: int i = 12 / 0;

NullPointerException Se produce cuando se intenta acceder a una variable o método antes de ser definido: class Hola extends Applet { Image img;

paint( Graphics g ) { g.drawImage( img,25,25,this ); } }

IncompatibleClassChangeException El intento de cambiar una clase afectada por referencias en otros objetos, específicamente cuando esos objetos todavía no han sido recompilados. ClassCastException El intento de convertir un objeto a otra clase que no es válida. y = (Prueba)x; // donde x no es de tipo Prueba

NegativeArraySizeException Puede ocurrir si hay un error aritmético al intentar cambiar el tamaño de un array. OutOfMemoryException ¡No debería producirse nunca! El intento de crear un objeto con el operador new ha fallado por falta de memoria. Y siempre tendría que haber memoria suficiente porque el garbage collector se encarga de proporcionarla al ir liberando objetos que no se usan y devolviendo memoria al sistema. NoClassDefFoundException Se referenció una clase que el sistema es incapaz de encontrar. ArrayIndexOutOfBoundsException Es la excepción que más frecuentemente se produce. Se genera al intentar acceder a un elemento de un array más allá de los límites definidos inicialmente para ese array. UnsatisfiedLinkException Se hizo el intento de acceder a un método nativo que no existe. Aquí no existe un método a.kk class A { native void kk(); }

y se llama a a.kk(), cuando debería llamar a A.kk(). InternalException Este error se reserva para eventos que no deberían ocurrir. Por definición, el usuario nunca debería ver este error y esta excepción no debería lanzarse.

CREAR EXCEPCIONES PROPIAS También podemos lanzar nuestras propias excepciones, extendiendo la clase System.exception. Por ejemplo, consideremos un programa cliente/servidor. El código cliente se intenta conectar al servidor, y durante 5 segundos se espera a que conteste el servidor. Si el servidor no responde, el servidor lanzaría la excepción de time-out: class ServerTimeOutException extends Exception {} public void conectame( String nombreServidor ) throws Exception { int exito; int puerto = 80; exito = open( nombreServidor,puerto ); if( exito == -1 ) throw ServerTimeOutException; }

Si se quieren capturar las propias excepciones, se deberá utilizar la sentencia try: public void encuentraServidor() { ... try { conectame( servidorDefecto ); catch( ServerTimeOutException e ) { g.drawString( "Time-out del Servidor, intentando alternativa", 5,5 ); conectame( servidorAlterno ); } ... }

Cualquier método que lance una excepción también debe capturarla, o declararla como parte de la interface del método. Cabe preguntarse entonces, el porqué de lanzar una excepción si hay que capturarla en el mismo método. La respuesta es que las excepciones no simplifican el trabajo del control de errores. Tienen la ventaja de que se puede tener muy localizado el control de errores y no tenemos que controlar millones de valores de retorno, pero no van más allá.

CAPTURAR EXCEPCIONES Las excepciones lanzadas por un método que pueda hacerlo deben recoger en bloque try/catch o try/finally. int valor; try { for( x=0,valor = 100; x < 100; x ++ ) valor /= x; } catch( ArithmeticException e ) { System.out.println( "Matemáticas locas!" ); } catch( Exception e ) { System.out.println( "Se ha producido un error" ); }

try Es el bloque de código donde se prevé que se genere una excepción. Es como si dijésemos "intenta estas sentencias y mira a ver si se produce una excepción". El bloque try tiene que ir seguido, al menos, por una cláusula catch o una cláusula finally

catch Es el código que se ejecuta cuando se produce la excepción. Es como si dijésemos "controlo cualquier excepción que coincida con mi argumento". En este bloque tendremos que asegurarnos de colocar código que no genere excepciones. Se pueden colocar sentencias catch sucesivas, cada una controlando una excepción diferente. No debería intentarse capturar todas las excepciones con una sola cláusula, como esta: catch( Excepcion e ) { ...

Esto representaría un uso demasiado general, podrían llegar muchas más excepciones de las esperadas. En este caso es mejor dejar que la excepción se propague hacia arriba y dar un mensaje de error al usuario. Se pueden controlar grupos de excepciones, es decir, que se pueden controlar, a través del argumento, excepciones semejantes. Por ejemplo: class Limites extends Exception {} class demasiadoCalor extends Limites {} class demasiadoFrio extends Limites {} class demasiadoRapido extends Limites {} class demasiadoCansado extends Limites {} . . . try { if( temp > 40 ) throw( new demasiadoCalor() ); if( dormir < 8 ) throw( new demasiado Cansado() } catch( Limites lim ) { if( lim instanceof demasiadoCalor ) { System.out.println( "Capturada return; } if( lim instanceof demasiadoCansado { System.out.println( "Capturada return; } } finally System.out.println( "En la clausula

);

excesivo calor!" ); ) excesivo cansancio!" );

finally" );

La cláusula catch comprueba los argumentos en el mismo orden en que aparezcan en el programa. Si hay alguno que coincida, se ejecuta el bloque. El operador instanceof se utiliza para identificar exactamente cual ha sido la identidad de la excepción.

finally Es el bloque de código que se ejecuta siempre, haya o no excepción. Hay una cierta controversia entre su utilidad, pero, por ejemplo, podría servir para hacer un log o un seguimiento de lo que está pasando, porque como se ejecuta siempre puede dejarnos grabado si se producen excepciones y nos

hemos recuperado de ellas o no. Este bloque finally puede ser útil cuando no hay ninguna excepción. Es un trozo de código que se ejecuta independientemente de lo que se haga en el bloque try. Cuando vamos a tratar una excepción, se nos plantea el problema de qué acciones vamos a tomar. En la mayoría de los casos, bastará con presentar una indicación de error al usuario y un mensaje avisándolo de que se ha producido un error y que decida si quiere o no continuar con la ejecución del programa. Por ejemplo, podríamos disponer de un diálogo como el que se presenta en el código siguiente: public class DialogoError extends Dialog { DialogoError( Frame padre ) { super( padre,true ); setLayout( new BorderLayout() ); // Presentamos un panel con continuar o salir Panel p = new Panel(); p.add( new Button( "¿Continuar?" ) ); p.add( new Button( "Salir" ) ); add( "Center",new Label( "Se ha producido un error. ¿Continuar?" ) ) add( "South",p ); } public boolean action( Event evt,Object obj ) { if( "Salir".equals( obj ) ) { dispose(); System.exit( 1 ); } return false; } }

Y la invocación, desde algún lugar en que se suponga que se generarán errores, podría ser como sigue: try { // Código peligroso } catch( AlgunaExcepcion e ) { VentanaError = new DialogoError( this ); VentanaError.show(); }

PROPAGACION DE EXCEPCIONES La cláusula catch comprueba los argumentos en el mismo orden en que aparezcan en el programa. Si hay alguno que coincida, se ejecuta el bloque y sigue el flujo de control por el bloque finally (si lo hay) y concluye el control de la excepción. Si ninguna de las cláusulas catch coincide con la excepción que se ha producido, entonces se ejecutará el código de la cláusula finally (en caso de que la haya). Lo que ocurre en este caso, es exactamente lo mismo que si la sentencia que lanza la excepción no se encontrase encerrada en el bloque try. El flujo de control abandona este método y retorna prematuramente al método que lo llamó. Si la llamada estaba dentro del ámbito de una sentencia try, entonces se vuelve a intentar el control de la

excepción, y así continuamente. Veamos lo que sucede cuando una excepción no es tratada en la rutina en donde se produce. El sistema Java busca un bloque try..catch más allá de la llamada, pero dentro del método que lo trajo aquí. Si la excepción se propaga de todas formas hasta lo alto de la pila de llamadas sin encontrar un controlador específico para la excepción, entonces la ejecución se detendrá dando un mensaje. Es decir, podemos suponer que Java nos está proporcionando un bloque catch por defecto, que imprime un mensaje de error y sale. No hay ninguna sobrecarga en el sistema por incorporar sentencias try al código. La sobrecarga se produce cuando se genera la excepción. Hemos dicho ya que un método debe capturar las excepciones que genera, o en todo caso, declararlas como parte de su llamada, indicando a todo el mundo que es capaz de generar excepciones. Esto debe ser así para que cualquiera que escriba una llamada a ese método esté avisado de que le puede llegar una excepción, en lugar del valor de retorno normal. Esto permite al programador que llama a ese método, elegir entre controlar la excepción o propagarla hacia arriba en la pila de llamadas. La siguiente línea de código muestra la forma general en que un método declara excepciones que se pueden propagar fuera de él: tipo_de_retorno( parametros ) throws e1,e2,e3 { }

Los nombres e1,e2,... deben ser nombres de excepciones, es decir, cualquier tipo que sea asignable al tipo predefinido Throwable. Observar que, como en la llamada al método se especifica el tipo de retorno, se está especificando el tipo de excepción que puede generar (en lugar de un objeto exception). He aquí un ejemplo, tomado del sistema Java de entrada/salida: byte readByte() throws IOException; short readShort() throws IOException; char readChar() throws IOException; void writeByte( int v ) throws IOException; void writeShort( int v ) throws IOException; void writeChar( int v ) throws IOException;

Lo más interesante aquí es que la rutina que lee un char, puede devolver un char; no el entero que se requiere en C. C necesita que se devuelva un int, para poder pasar cualquier valor a un char, y además un valor extra (-1) para indicar que se ha alcanzado el final del fichero. Algunas de las rutinas Java lanzan una excepción cuando se alcanza el fin del fichero. En el siguiente diagrama se muestra gráficamente cómo se propaga la excepción que se genera en el código, a través de la pila de llamadas durante la ejecución del código:

Cuando se crea una nueva excepción, derivando de una clase Exception ya existente, se puede cambiar el mensaje que lleva asociado. La cadena de texto puede ser recuperada a través de un método. Normalmente, el texto del mensaje proporcionará información para resolver el problema o sugerirá una acción alternativa. Por ejemplo: class SinGasolina extends Exception { SinGasolina( String s ) { // constructor super( s ); } .... // Cuando se use, aparecerá algo como esto try { if( j < 1 ) throw new SinGasolina( "Usando deposito de reserva" ); } catch( SinGasolina e ) { System.out.println( o.getMessage() ); }

Esto, en tiempo de ejecución originaría la siguiente salida por pantalla: > Usando deposito de reserva

Otro método que es heredado de la superclase Throwable es printStackTrace(). Invocando a este método sobre una excepción se volcará a pantalla todas las llamadas hasta el momento en donde se generó la excepción (no donde se maneje la excepción). Por ejemplo: // Capturando una excepción en un método class testcap { static int slice0[] = { 0,1,2,3,4 }; public static void main( String a[] ) { try { uno(); } catch( Exception e ) { System.out.println( "Captura de la excepcion en main()" ); e.printStackTrace(); } }

static void uno() { try { slice0[-1] = 4; } catch( NullPointerException e ) { System.out.println( "Captura una excepcion diferente" ); } } }

Cuando se ejecute ese código, en pantalla observaremos la siguiente salida: > Captura de la excepcion en main() > java.lang.ArrayIndexOutOfBoundsException: -1 at testcap.uno(test5p.java:19) at testcap.main(test5p.java:9)

Con todo el manejo de excepciones podemos concluir que proporciona un método más seguro para el control de errores, además de representar una excelente herramienta para organizar en sitios concretos todo el manejo de los errores y, además, que podemos proporcionar mensajes de error más decentes al usuario indicando qué es lo que ha fallado y por qué, e incluso podemos, a veces, recuperarnos de los errores. La degradación que se produce en la ejecución de programas con manejo de excepciones está ampliamente compensada por las ventajas que representa en cuanto a seguridad de funcionamiento de esos mismos programas.

THEREADS Y MULTRITHREADING Considerando el entorno multithread, cada thread (hilo, flujo de control del programa) representa un proceso individual ejecutándose en un sistema. A veces se les llama procesos ligeros o contextos de ejecución. Típicamente, cada thread controla un único aspecto dentro de un programa, como puede ser supervisar la entrada en un determinado periférico o controlar toda la entrada/salida del disco. Todos los threads comparten los mismos recursos, al contrario que los procesos en donde cada uno tiene su propia copia de código y datos (separados unos de otros). Gráficamente, los threads se parecen en su funcionamiento a lo que muestra la figura siguiente:

FLUJO EN PROGRAMAS Programas de flujo único Un programa de flujo único o mono-hilvanado (single-thread) utiliza un único flujo de control (thread) para controlar su ejecución. Muchos programas no necesitan la potencia o utilidad de múltiples flujos de control. Sin necesidad de especificar explícitamente que se quiere un único flujo de control, muchos de los applets y aplicaciones son de flujo único. Por ejemplo, en nuestra aplicación estándar de saludo: public class HolaMundo { static public void main( String args[] ) { System.out.println( "Hola Mundo!" ); } }

Aquí, cuando se llama a main(), la aplicación imprime el mensaje y termina. Esto ocurre dentro de un único thread.

Programas de flujo múltiple En nuestra aplicación de saludo, no vemos el thread que ejecuta nuestro programa. Sin embargo, Java posibilita la creación y control de threads explícitamente. La utilización de threads en Java, permite una enorme flexibilidad a los programadores a la hora de plantearse el desarrollo de aplicaciones. La simplicidad para crear, configurar y ejecutar threads, permite que se puedan implementar muy poderosas y portables aplicaciones/applets que no se puede con otros lenguajes de tercera generación. En un lenguaje orientado a Internet como es Java, esta herramienta es vital. Si se ha utilizado un navegador con soporte Java, ya se habrá visto el uso de múltiples threads en

Java. Habrá observado que dos applet se pueden ejecutar al mismo tiempo, o que puede desplazar la página del navegador mientras el applet continúa ejecutándose. Esto no significa que el applet utilice múltiples threads, sino que el navegador es multithreaded. Las aplicaciones (y applets) multithreaded utilizan muchos contextos de ejecución para cumplir su trabajo. Hacen uso del hecho de que muchas tareas contienen subtareas distintas e independientes. Se puede utilizar un thread para cada subtarea. Mientras que los programas de flujo único pueden realizar su tarea ejecutando las subtareas secuencialmente, un programa multithreaded permite que cada thread comience y termine tan pronto como sea posible. Este comportamiento presenta una mejor respuesta a la entrada en tiempo real. Vamos a modificar nuestro programa de saludo creando tres threads individuales, que imprimen cada uno de ellos su propio mensaje de saludo, MultiHola.java: // Definimos unos sencillos threads. Se detendrán un rato // antes de imprimir sus nombres y retardos class TestTh extends Thread { private String nombre; private int retardo; // Constructor para almacenar nuestro nombre // y el retardo public TestTh( String s,int d ) { nombre = s; retardo = d; } // El metodo run() es similar al main(), pero para // threads. Cuando run() termina el thread muere public void run() { // Retasamos la ejecución el tiempo especificado try { sleep( retardo ); } catch( InterruptedException e ) { ; }

}

// Ahora imprimimos el nombre System.out.println( "Hola Mundo! "+nombre+" "+retardo ); }

public class MultiHola { public static void main( String args[] ) { TestTh t1,t2,t3; // t1 t2 t3

}

Creamos los threads = new TestTh( "Thread 1",(int)(Math.random()*2000) ); = new TestTh( "Thread 2",(int)(Math.random()*2000) ); = new TestTh( "Thread 3",(int)(Math.random()*2000) );

// Arrancamos los threads t1.start(); t2.start(); t3.start(); }

CREACION Y CONTROL DE THREADS Creación de un Thread Hay dos modos de conseguir threads en Java. Una es implementando la interface Runnable, la otra es extender la clase Thread. La implementación de la interface Runnable es la forma habitual de crear threads. Las interfaces proporcionan al programador una forma de agrupar el trabajo de infraestructura de una clase. Se utilizan para diseñar los requerimientos comunes al conjunto de clases a implementar. La interface define el trabajo y la clase, o clases, que implementan la interface realizan ese trabajo. Los diferentes grupos de clases que implementen la interface tendrán que seguir las mismas reglas de funcionamiento. Hay una cuantas diferencias entre interface y clase. Primero, una interface solamente puede contener métodos abstractos y/o variables estáticas y finales (constantes). Las clases, por otro lado, pueden implementar métodos y contener variables que no sean constantes. Segundo, una interface no puede implementar cualquier método. Una clase que implemente una interface debe implementar todos los métodos definidos en esa interface. Una interface tiene la posibilidad de poder extenderse de otras interfaces y, al contrario que las clases, puede extenderse de múltiples interfaces. Además, una interface no puede ser instanciada con el operador new; por ejemplo, la siguiente sentencia no está permitida: Runnable a = new Runnable();

// No se permite

El primer método de crear un thread es simplemente extender la clase Thread: class MiThread extends Thread { public void run() { . . . }

El ejemplo anterior crea una nueva clase MiThread que extiende la clase Thread y sobrecarga el método Thread.run() por su propia implementación. El método run() es donde se realizará todo el trabajo de la clase. Extendiendo la clase Thread, se pueden heredar los métodos y variables de la clase padre. En este caso, solamente se puede extender o derivar una vez de la clase padre. Esta limitación de Java puede ser superada a través de la implementación de Runnable: public class MiThread implements Runnable { Thread t; public void run() { // Ejecución del thread una vez creado } }

En este caso necesitamos crear una instancia de Thread antes de que el sistema pueda ejecutar el proceso como un thread. Además, el método abstracto run() está definido en la interface Runnable tiene que ser implementado. La única diferencia entre los dos métodos es que este último es mucho más flexible. En el ejemplo anterior, todavía tenemos oportunidad de extender la clase MiThread, si fuese necesario. La mayoría de las clases creadas que necesiten ejecutarse como un thread , implementarán la interface Runnable, ya que probablemente extenderán alguna de su funcionalidad a otras clases. No pensar que la interface Runnable está haciendo alguna cosa cuando la tarea se está ejecutando. Solamente contiene métodos abstractos, con lo cual es una clase para dar idea sobre el diseño de la clase Thread. De hecho, si vemos los fuentes de Java, podremos comprobar que solamente contiene un método abstracto: package java.lang;

public interface Runnable { public abstract void run(); }

Y esto es todo lo que hay sobre la interface Runnable. Como se ve, una interface sólo proporciona un diseño para las clases que vayan a ser implementadas. En el caso de Runnable, fuerza a la definición del método run(), por lo tanto, la mayor parte del trabajo se hace en la clase Thread. Un vistazo un poco más profundo a la definición de la clase Thread nos da idea de lo que realmente está pasando: public class Thread implements Runnable { ... public void run() { if( tarea!= null ) tarea.run(); } } ... }

De este trocito de código se desprende que la clase Thread también implemente la interface Runnable. tarea.run() se asegura de que la clase con que trabaja (la clase que va a ejecutarse como un thread) no sea nula y ejecuta el método run() de esa clase. Cuando esto suceda, el método run() de la clase hará que corra como un thread.

Arranque de un Thread Las aplicaciones ejecutan main() tras arrancar. Esta es la razón de que main() sea el lugar natural para crear y arrancar otros threads. La línea de código: t1 = new TestTh( "Thread 1",(int)(Math.random()*2000) );

crea un nuevo thread. Los dos argumentos pasados representan el nombre del thread y el tiempo que queremos que espere antes de imprimir el mensaje. Al tener control directo sobre los threads, tenemos que arrancarlos explícitamente. En nuestro ejemplo con: t1.start();

start(), en realidad es un método oculto en el thread que llama al método run().

Manipulación de un Thread Si todo fue bien en la creación del thread, t1 debería contener un thread válido, que controlaremos en el método run(). Una vez dentro de run(), podemos comenzar las sentencias de ejecución como en otros programas. run() sirve como rutina main() para los threads; cuando run() termina, también lo hace el thread. Todo lo que queramos que haga el thread ha de estar dentro de run(), por eso cuando decimos que un método es Runnable, nos obliga a escribir un método run(). En este ejemplo, intentamos inmediatamente esperar durante una cantidad de tiempo aleatoria (pasada a través del constructor): sleep( retardo );

El método sleep() simplemente le dice al thread que duerma durante los milisegundos especificados. Se debería utilizar sleep() cuando se pretenda retrasar la ejecución del thread. sleep() no consume recursos del sistema mientras el thread duerme. De esta forma otros threads pueden seguir funcionando. Una vez hecho el retardo, se imprime el mensaje "Hola Mundo!" con el nombre del

thread y el retardo.

Suspensión de un Thread Puede resultar útil suspender la ejecución de un thread sin marcar un límite de tiempo. Si, por ejemplo, está construyendo un applet con un thread de animación, querrá permitir al usuario la opción de detener la animación hasta que quiera continuar. No se trata de terminar la animación, sino desactivarla. Para este tipo de control de thread se puede utilizar el método suspend(). t1.suspend();

Este método no detiene la ejecución permanentemente. El thread es suspendido indefinidamente y para volver a activarlo de nuevo necesitamos realizar una invocación al método resume(): t1.resume();

Parada de un Thread El último elemento de control que se necesita sobre threads es el método stop(). Se utiliza para terminar la ejecución de un thread: t1.stop();

Esta llamada no destruye el thread, sino que detiene su ejecución. La ejecución no se puede reanudar ya con t1.start(). Cuando se desasignen las variables que se usan en el thread, el objeto thread (creado con new) quedará marcado para eliminarlo y el garbage collector se encargará de liberar la memoria que utilizaba. En nuestro ejemplo, no necesitamos detener explícitamente el thread. Simplemente se le deja terminar. Los programas más complejos necesitarán un control sobre cada uno de los threads que lancen, el método stop() puede utilizarse en esas situaciones. Si se necesita, se puede comprobar si un thread está vivo o no; considerando vivo un thread que ha comenzado y no ha sido detenido. t1.isAlive();

Este método devolverá true en caso de que el thread t1 esté vivo, es decir, ya se haya llamado a su método run() y no haya sido parado con un stop() ni haya terminado el método run() en su ejecución.

ARRANCAR Y PARAR THREADS Ahora que ya hemos visto por encima como se arrancan, paran y manipulan threads, vamos a mostrar un ejemplo un poco más gráfico, se trata de un contador, cuyo código (App1Thread.java) es el siguiente: import java.awt.*; import java.applet.Applet; public class App1Thread extends Applet implements Runnable { Thread t; int contador; public void init() { contador = 0; t = new Thread( this ); t.start(); } public void run() { while( true ) { contador++; repaint(); try { t.sleep( 10 ); } catch( InterruptedException e ) { ; }; } } public boolean mouseDown( Event evt,int x,int y ) { t.stop(); return( true ); } public void paint( Graphics g ) { g.drawString( Integer.toString( contador ),10,10 ); System.out.println( "Contador = "+contador ); } public void stop() { t.stop(); } }

Este applet arranca un contador en 0 y lo incrementa, presentando su salida tanto en la pantalla gráfica como en la consola. Una primera ojeada al código puede dar la impresión de que el programa empezará a contar y presentará cada número, pero no es así. Una revisión más profunda del flujo de ejecución del applet, nos revelará su verdadera identidad. En este caso, la clase App1Thread está forzada a implementar Runnable sobre la clase Applet que extiende. Como en todos los applets, el método init() es el primero que se ejecuta. En init(), la variable contador se inicializa a cero y se crea una nueva instancia de la clase Thread. Pasándole this al constructor de Thread, el nuevo thread ya conocerá al objeto que va a correr. En este caso this es una referencia a App1Thread. Después de que hayamos creado el thread, necesitamos

arrancarlo. La llamada a start(), llamará a su vez al método run() de nuestra clase, es decir, a App1Thread.run(). La llamada a start() retornará con éxito y el thread comenzará a ejecutarse en ese instante. Observar que el método run() es un bucle infinito. Es infinito porque una vez que se sale de él, la ejecución del thread se detiene. En este método se incrementará la variable contador, se duerme 10 milisegundos y envía una petición de refresco del nuevo valor al applet. Es muy importante dormirse en algún lugar del thread, porque sino, el thread consumirá todo el tiempo de la CPU para su proceso y no permitirá que entren otros métodos de otros threads a ejecutarse. Otra forma de detener la ejecución del thread es hacer una llamada al método stop(). En el contador, el thread se detiene cuando se pulsa el ratón mientras el cursor se encuentre sobre el applet. Dependiendo de la velocidad del ordenador, se presentarán los números consecutivos o no, porque el incremento de la variable contador es independiente del refresco en pantalla. El applet no se refresca a cada petición que se le hace, sino que el sistema operativo encolará las peticiones y las que sean sucesivas las convertirán en un único refresco. Así, mientras los refescos se van encolando, la variable contador se estará todavía incrementando, pero no se visualiza en pantalla.

SUSPENDER Y REANUDAR THREADS Una vez que se para un thread, ya no se puede rearrancar con el comando start(), debido a que stop() concluirá la ejecución del thread. Por ello, en ver de parar el thread, lo que podemos hacer es dormirlo, llamando al método sleep(). El thread estará suspendido un cierto tiempo y luego reanudará su ejecución cuando el límite fijado se alcance. Pero esto no es útil cuando se necesite que el thread reanude su ejecución ante la presencia de ciertos eventos. En estos casos, el método suspend() permite que cese la ejecución del thread y el método resume() permite que un método suspendido reanude su ejecución. En la siguiente versión de nuestra clase contador, App2Thread.java, modificamos el applet para que utilice los métodos suspend() y resume(): public class App2Thread extends Applet implements Runnable { Thread t; int contador; boolean suspendido; ... public boolean mouseDown( Event evt,int x,int y ) { if( suspendido ) t.resume(); else t.suspend(); suspendido = !suspendido;

...

return( true ); }

Para controlar el estado del applet, hemos introducido la variable suspendido. Diferenciar los distintos estados de ejecución del applet es importante porque algunos métodos pueden generar excepciones si se llaman desde un estado erróneo. Por ejemplo, si el applet ha sido arrancado y se detiene con stop(), si se intenta ejecutar el método start(), se generará una excepción IllegalThreadStateException.

ESTADOS DE UN THREAD Durante el ciclo de vida de un thread, éste se puede encontrar en diferentes estados. La figura siguiente muestra estos estados y los métodos que provocan el paso de un estado a otro. Este diagrama no es una máquina de estados finita, pero es lo que más se aproxima al funcionamiento real de un thread .

Nuevo Thread La siguiente sentencia crea un nuevo thread pero no lo arranca, lo deja en el estado de "Nuevo Thread": Thread MiThread = new MiClaseThread();

Cuando un thread está en este estado, es simplemente un objeto Thread vacío. El sistema no ha destinado ningún recurso para él. Desde este estado solamente puede arrancarse llamando al método start(), o detenerse definitivamente, llamando al método stop(); la llamada a cualquier otro método carece de sentido y lo único que provocará será la generación de una excepción de tipo IllegalThreadStateException.

Ejecutable Ahora veamos las dos línea de código que se presentan a continuación: Thread MiThread = new MiClaseThread(); MiThread.start();

La llamada al método start() creará los recursos del sistema necesarios para que el thread puede ejecutarse, lo incorpora a la lista de procesos disponibles para ejecución del sistema y llama al método run() del thread. En este momento nos encontramos en el estado "Ejecutable" del diagrama. Y este estado es Ejecutable y no En Ejecución, porque cuando el thread está aquí no esta corriendo. Muchos ordenadores tienen solamente un procesador lo que hace imposible que todos los threads estén corriendo al mismo tiempo. Java implementa un tipo de scheduling o lista de procesos, que permite que el procesador sea compartido entre todos los procesos o threads que se encuentran en la lista. Sin embargo, para nuestros propósitos, y en la mayoría de los casos, se puede considerar que este estado es realmente un estado "En Ejecución", porque la impresión que produce ante nosotros es que todos los procesos se ejecutan al mismo tiempo. Cuando el thread se encuentra en este estado, todas las instrucciones de código que se encuentren dentro del bloque declarado para el método run(), se ejecutarán secuencialmente.

Parado El thread entra en estado "Parado" cuando alguien llama al método suspend(), cuando se llama al método sleep(), cuando el thread está bloqueado en un proceso de entrada/salida o cuando el thread utiliza su método wait() para esperar a que se cumpla una determinada condición. Cuando ocurra cualquiera de las cuatro cosas anteriores, el thread estará Parado. Por ejemplo, en el trozo de código siguiente: Thread MiThread = new MiClaseThread(); MiThread.start(); try { MiThread.sleep( 10000 ); } catch( InterruptedException e ) { ; }

la línea de código que llama al método sleep(): MiThread.sleep( 10000 );

hace que el thread se duerma durante 10 segundos. Durante ese tiempo, incluso aunque el procesador estuviese totalmente libre, MiThread no correría. Después de esos 10 segundos. MiThread volvería a estar en estado "Ejecutable" y ahora sí que el procesador podría hacerle caso cuando se encuentre disponible. Para cada una de los cuatro modos de entrada en estado Parado, hay una forma específica de volver a estado Ejecutable. Cada forma de recuperar ese estado es exclusiva; por ejemplo, si el thread ha sido puesto a dormir, una vez transcurridos los milisegundos que se especifiquen, él solo se despierta y vuelve a estar en estado Ejecutable. Llamar al método resume() mientras esté el thread durmiendo no serviría para nada. Los métodos de recuperación del estado Ejecutable, en función de la forma de llegar al estado Parado del thread, son los siguientes: ● ● ● ●

Si un thread está dormido, pasado el lapso de tiempo Si un thread está suspendido, luego de una llamada al método resume() Si un thread está bloqueado en una entrada/salida, una vez que el comando E/S concluya su ejecución Si un thread está esperando por una condición, cada vez que la variable que controla esa condición varíe debe llamarse a notify() o notifyAll()

Muerto Un thread se puede morir de dos formas: por causas naturales o porque lo maten (con stop()). Un thread muere normalmente cuando concluye de forma habitual su método run(). Por ejemplo, en el siguiente trozo de código, el bucle while es un bucle finito -realiza la iteración 20 veces y termina-: public void run() { int i=0; while( i < 20 ) { i++; System.out.println( "i = "+i ); } }

Un thread que contenga a este método run(), morirá naturalmente después de que se complete el bucle y run() concluya.

También se puede matar en cualquier momento un thread, invocando a su método stop(). En el trozo de código siguiente: Thread MiThread = new MiClaseThread(); MiThread.start(); try { MiThread.sleep( 10000 ); } catch( InterruptedException e ) { ; } MiThread.stop();

se crea y arranca el thread MiThread, lo dormimos durante 10 segundos y en el momento de despertarse, la llamada a su método stop(), lo mata. El método stop() envía un objeto ThreadDeath al thread que quiere detener. Así, cuando un thread es parado de este modo, muere asíncronamente. El thread morirá en el momento en que reciba la excepción ThreadDeath. Los applets utilizarán el método stop() para matar a todos sus threads cuando el navegador con soporte Java en el que se están ejecutando le indica al applet que se detengan, por ejemplo, cuando se minimiza la ventana del navegador o cuando se cambia de página.

El método isAlive() La interface de programación de la clase Thread incluye el método isAlive(), que devuelve true si el thread ha sido arrancado (con start()) y no ha sido detenido (con stop()). Por ello, si el método isAlive() devuelve false, sabemos que estamos ante un "Nuevo Thread" o ante un thread "Muerto". Si nos devuelve true, sabemos que el thread se encuentra en estado "Ejecutable" o "Parado". No se puede diferenciar entre "Nuevo Thread" y "Muerto", ni entre un thread "Ejecutable" o "Parado".

SCHEDULING Java tiene un Scheduler, una lista de procesos, que monitoriza todos los threads que se están ejecutando en todos los programas y decide cuales deben ejecutarse y cuales deben encontrarse preparados para su ejecución. Hay dos características de los threads que el scheduler identifica en este proceso de decisión. Una, la más importante, es la prioridad del thread; la otra, es el indicador de demonio. La regla básica del scheduler es que si solamente hay threads demonio ejecutándose, la Máquina Virtual Java (JVM) concluirá. Los nuevos threads heredan la prioridad y el indicador de demonio de los threads que los han creado. El scheduler determina qué threads deberán ejecutarse comprobando la prioridad de todos los threads, aquellos con prioridad más alta dispondrán del procesador antes de los que tienen prioridad más baja. El scheduler puede seguir dos patrones, preemptivo y no-preemptivo. Los schedulers preemtivos proporcionan un segmento de tiempo a todos los threads que están corriendo en el sistema. El scheduler decide cual será el siguiente thread a ejecutarse y llama a resume() para darle vida durante un período fijo de tiempo. Cuando el thread ha estado en ejecución ese período de tiempo, se llama a suspend() y el siguiente thread en la lista de procesos será relanzado (resume()). Los schedulers nopreemtivos deciden que thread debe correr y lo ejecutan hasta que concluye. El thread tiene control total sobre el sistema mientras esté en ejecución. El método yield() es la forma en que un thread fuerza al scheduler a comenzar la ejecución de otro thread que esté esperando. Dependiendo del sistema en que esté corriendo Java, el scheduler será preemtivo o no-preemptivo. En el siguiente ejemplo, SchThread.java, mostramos la ejecución de dos threads con diferentes prioridades. Un thread se ejecuta a prioridad más baja que el otro. Los threads incrementarán sus contadores hasta que el thread que tiene prioridad más alta alcance al contador que corresponde a

la tarea con ejecución más lenta.

PRIORIDADES, SERVICIOS... Prioridades El scheduler determina el thread que debe ejecutarse en función de la prioridad asignada a cada uno de ellos. El rango de prioridades oscila entre 1 y 10. La prioridad por defecto de un thread es Thread.NORM_PRIORITY, que tiene asignado un valor de 5. Hay otras dos variables estáticas disponibles, que son Thread.MIN_PRORITY, fijada a 1, y Thread.MAX_PRIORITY, aque tiene un valor de 10. El método getPriority() puede utilizarse para conocer el valor actual de la prioridad de un thread.

Threads Demonio Los threads demonio también se llaman servicios, porque se ejecutan, normalmente, con prioridad baja y proporcionan un servicio básico a un programa o programas cuando la actividad de la máquina es reducida. Un ejemplo de thread demonio que está ejecutándose continuamente es el recolector de basura (garbage collector). Este thread, proporcionado por la Máquina Virtual Java, comprueba las variables de los programas a las que no se accede nunca y libera estos recursos, devolviéndolos al sistema. Un thread puede fijar su indicador de demonio pasando un valor true al método setDaemon(). Si se pasa false a este método, el thread será devuelto por el sistema como un thread de usuario. No obstante, esto último debe realizarse antes de que se arranque el thread (start()).

Diferencia de threads con fork() fork() en Unix crea un proceso hijo que tiene su propia copia de datos y código del padre. Esto funciona correctamente si estamos sobrados de memoria y disponemos de una CPU poderosa, y siempre que mantengamos el número de procesos hijos dentro de un límite manejable, porque se hace un uso intensivo de los recursos del sistema. Los applets Java no pueden lanzar ningún proceso en el cliente, porque eso sería una fuente de inseguridad y no está permitido. Las aplicaciones y los applets deben utilizar threads. La multi-tarea pre-emptiva tiene sus problemas. Un thread puede interrumpir a otro en cualquier momento, de ahí lo de pre-emptive. Imaginarse lo que pasaría si un thread está escribiendo en un array, mientras otro thread lo interrumpe y comienza a escribir en el mismo array. Los lenguajes como C y C++ necesitan de las funciones lock() y unlock() para antes y después de leer o escribir datos. Java también funciona de este modo, pero oculta el bloqueo de datos bajo la sentencia synchronized: synchronized int MiMetodo();

Otro área en que los threads son muy útiles es en los interfaces de usuario. Permiten incrementar la respuesta del ordenador ante el usuario cuando se encuentra realizando complicados cálculos y no puede atender a la entrada de usuario. Estos cálculos se pueden realizar en segundo plano, o realizar varios en primer plano (música y animaciones) sin que se dé apariencia de pérdida de rendimiento.

EJEMPLO DE ANIMACION Este es un ejemplo de un applet, Animacion.java, que crea un thread de animación que nos presenta el globo terráqueo en rotación. Aquí podemos ver que estamos creando un thread de sí mismo, concurrencia. Además, animacion.start() llama al start() del thread, no del applet, que automáticamente llamará a run(): import java.awt.*; import java.applet.Applet; public class Animacion extends Applet implements Runnable { Image imagenes[]; MediaTracker tracker; int indice = 0; Thread animacion; int maxAncho,maxAlto; Image offScrImage; // Componente off-screen para doble buffering Graphics offScrGC; // Nos indicará si ya se puede pintar boolean cargado = false; // Inicializamos el applet, establecemos su tamaño y // cargamos las imágenes public void init() { // Establecemos el supervisor de imágenes tracker = new MediaTracker( this ); // Fijamos el tamaño del applet maxAncho = 100; maxAlto = 100; imagenes = new Image[36]; // Establecemos el doble buffer y dimensionamos el applet try { offScrImage = createImage( maxAncho,maxAlto ); offScrGC = offScrImage.getGraphics(); offScrGC.setColor( Color.lightGray ); offScrGC.fillRect( 0,0,maxAncho,maxAlto ); resize( maxAncho,maxAlto ); } catch( Exception e ) { e.printStackTrace(); } // Cargamos las imágenes en un array for( int i=0; i < 36; i++ ) { String fichero = new String( "Tierra"+String.valueOf(i+1)+".gif" ); imagenes[i] = getImage( getDocumentBase(),fichero ); // Registramos las imágenes con el tracker tracker.addImage( imagenes[i],i ); } try { // Utilizamos el tracker para comprobar que todas las // imágenes están cargadas tracker.waitForAll(); } catch( InterruptedException e ) { ; }

cargado = true; } // Pintamos el fotograma que corresponda public void paint( Graphics g ) { if( cargado ) g.drawImage( offScrImage,0,0,this ); } // Arrancamos y establecemos la primera imagen public void start() { if( tracker.checkID( indice ) ) offScrGC.drawImage( imagenes[indice],0,0,this ); animacion = new Thread( this ); animacion.start(); } // Aquí hacemos el trabajo de animación // Muestra una imagen, para, muestra la siguiente... public void run() { // Obtiene el identificador del thread Thread thActual = Thread.currentThread(); // Nos aseguramos de que se ejecuta cuando estamos en un // thread y además es el actual while( animacion != null && animacion == thActual ) { if( tracker.checkID( indice ) ) { // Obtenemos la siguiente imagen offScrGC.drawImage( imagenes[indice],0,0,this ); indice++; // Volvemos al principio y seguimos, para el bucle if( indice >= imagenes.length ) indice = 0; }

}

}

// Ralentizamos la animación para que parezca normal try { animacion.sleep( 200 ); } catch( InterruptedException e ) { ; } // Pintamos el siguiente fotograma repaint(); }

En el ejemplo podemos observar más cosas. La variable thActual es propia de cada thread que se lance, y la variable animacion la estarán viendo todos los threads. No hay duplicidad de procesos, sino que todos comparten las mismas variables; cada thread, sin embargo, tiene su pila local de variables, que no comparte con nadie y que son las que están declaradas dentro de las llaves del método run(). La excepción InterruptedExcepcion salta en el caso en que se haya tenido al thread parado más tiempo del debido. Es imprescindible recoger esta excepción cuando se están implementando threads, tanto es así, que en el caso de no recogerla, el compilador generará un error.

COMUNICACION ENTRE THREADS Otra clave para el éxito y la ventaja de la utilización de múltiples threads en una aplicación, o aplicación multithreaded, es que pueden comunicarse entre sí. Se pueden diseñar threads para utilizar objetos comunes, que cada thread puede manipular independientemente de los otros threads. El ejemplo clásico de comunicación de threads es un modelo productor/consumidor. Un thread produce una salida, que otro thread usa (consume), sea lo que sea esa salida. Vamos entonces a crear un productor, que será un thread que irá sacando caracteres por su salida; crearemos también un consumidor que ira recogiendo los caracteres que vaya sacando el productor y un monitor que controlará el proceso de sincronización entre los threads. Funcionará como una tubería, insertando el productor caracteres en un extremos y leyéndolos el consumidor en el otro, con el monitor siendo la propia tubería.

Productor El productor extenderá la clase Thread, y su código es el siguiente: class Productor extends Thread { private Tuberia tuberia; private String alfabeto = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; public Productor( Tuberia t ) { // Mantiene una copia propia del objeto compartido tuberia = t; } public void run() { char c;

}

// Mete 10 letras en la tubería for( int i=0; i < 10; i++ ) { c = alfabeto.charAt( (int)(Math.random()*26 ) ); tuberia.lanzar( c ); // Imprime un registro con lo añadido System.out.println( "Lanzado "+c+" a la tuberia." ); // Espera un poco antes de añadir más letras try { sleep( (int)(Math.random() * 100 ) ); } catch( InterruptedException e ) { ; } } }

Notar que creamos una instancia de la clase Tuberia, y que se utiliza el método tuberia.lanzar() para que se vaya construyendo la tubería, en principio de 10 caracteres.

Consumidor Veamos ahora el código del consumidor, que también extenderá la clase Thread:

class Consumidor extends Thread { private Tuberia tuberia; public Consumidor( Tuberia t ) { // Mantiene una copia propia del objeto compartido tuberia = t; } public void run() { char c;

}

// Consume 10 letras de la tubería for( int i=0; i < 10; i++ ) { c = tuberia.recoger(); // Imprime las letras retiradas System.out.println( "Recogido el caracter "+c ); // Espera un poco antes de coger más letras try { sleep( (int)(Math.random() * 2000 ) ); } catch( InterruptedException e ) { ; } } }

En este caso, como en el del productor, contamos con un método en la clase Tuberia, tuberia.recoger (), para manejar la información.

Monitor Una vez vistos el productor de la información y el consumidor, nos queda por ver qué es lo que hace la clase Tuberia. Lo que realiza la clase Tuberia, es una función de supervisión de las transacciones entre los dos threads, el productor y el consumidor. Los monitores, en general, son piezas muy importantes de las aplicaciones multithreaded, porque mantienen el flujo de comunicación entre los threads. class Tuberia { private char buffer[] = new char[6]; private int siguiente = 0; // Flags para saber el estado del buffer private boolean estaLlena = false; private boolean estaVacia = true; // Método para retirar letras del buffer public synchronized char recoger() { // No se puede consumir si el buffer está vacío while( estaVacia == true ) { try { wait(); // Se sale cuando estaVacia cambia a false } catch( InterruptedException e ) { ; } } // Decrementa la cuenta, ya que va a consumir una letra siguiente--; // Comprueba si se retiró la última letra if( siguiente == 0 ) estaVacia = true; // El buffer no puede estar lleno, porque acabamos de consumir estaLlena = false;

notify(); // Devuelve la letra al thread consumidor return( buffer[siguiente] ); } // Método para añadir letras al buffer public synchronized void lanzar( char c ) { // Espera hasta que haya sitio para otra letra while( estaLlena == true ) { try { wait(); // Se sale cuando estaLlena cambia a false } catch( InterruptedException e ) { ; } } // Añade una letra en el primer lugar disponible buffer[siguiente] = c; // Cambia al siguiente lugar disponible siguiente++; // Comprueba si el buffer está lleno if( siguiente == 6 ) estaLlena = true; estaVacia = false; notify(); } }

En la clase Tuberia vemos dos características importantes: los miembros dato (buffer[]) son privados, y los métodos de acceso (lanzar() y recoger()) son sincronizados. Aquí vemos que la variable estaVacia es un semáforo, como los de toda la vida. La naturaleza privada de los datos evita que el productor y el consumidor accedan directamente a éstos. Si se permitiese el acceso directo de ambos threads a los datos, se podrían producir problemas; por ejemplo, si el consumidor intenta retirar datos de un buffer vacío, obtendrá excepciones innecesarias, o se bloqueará el proceso. Los métodos sincronizados de acceso impiden que los productores y consumidores corrompan un objeto compartido. Mientras el productor está añadiendo una letra a la tubería, el consumidor no la puede retirar y viceversa. Esta sincronización es vital para mantener la integridad de cualquier objeto compartido. No sería lo mismo sincronizar la clase en vez de los métodos, porque esto significaría que nadie puede acceder a las variables de la clase en paralelo, mientras que al sincronizar los métodos, sí pueden acceder a todas las variables que están fuera de los métodos que pertenecen a la clase. Se pueden sincronizar incluso variables, para realizar alguna acción determinada sobre ellas, por ejemplo: sincronized( p ) { // aquí se colocaría el código // los threads que estén intentando acceder a p se pararán // y generarán una InterruptedException }

El método notify() al final de cada método de acceso avisa a cualquier proceso que esté esperando por el objeto, entonces el proceso que ha estado esperando intentará acceder de nuevo al objeto. En el método wait() hacemos que el thread se quede a la espera de que le llegue un notify(), ya sea enviado por el thread o por el sistema. Ahora que ya tenemos un productor, un consumidor y un objeto compartido, necesitamos una aplicación que arranque los threads y que consiga que todos hablen con el mismo objeto que están compartiendo. Esto es lo que hace el siguiente trozo de código, del fuente TubTest.java:

class TubTest { public static void main( String args[] ) { Tuberia t = new Tuberia(); Productor p = new Productor( t ); Consumidor c = new Consumidor( t );

}

p.start(); c.start(); }

Compilando y ejecutando esta aplicación, podremos observar nuestro modelo el pleno funcionamiento.

Monitorización del Productor Los programas productor/consumidor a menudo emplean monitorización remota, que permite al consumidor observar el thread del productor interaccionando con un usuario o con otra parte del sistema. Por ejemplo, en una red, un grupo de threads productores podrían trabajar cada uno en una workstation. Los productores imprimirían documentos, almacenando una entrada en un registro (log). Un consumidor (o múltiples consumidores) podría procesar el registro y realizar durante la noche un informe de la actividad de impresión del día anterior. Otro ejemplo, a pequeña escala podría ser el uso de varias ventanas en una workstation. Una ventana se puede usar para la entrada de información (el productor), y otra ventana reaccionaría a esa información (el consumidor). Peer, es un observador general del sistema.

METODOS NATIVOS Un método nativo es un método Java (una instancia de un objeto o una clase) cuya implementación se ha realizado en otro lenguaje de programación, por ejemplo, C. Vamos a ver cómo se integran métodos nativos en clases Java. Actualmente, el lenguaje Java solamente proporciona mecanismos para integrar código C en programas Java. Veamos pues los pasos necesarios para mezclar código nativo C y programas Java. Recurriremos (¡Cómo no!) a nuestro saludo; en este caso, el programa HolaMundo tiene dos clases Java: la primera implementa el método main() y la segunda, HolaMundo, tiene un método nativo que presenta el mensaje de saludo. La implementación de este segundo método la realizaremos en C.

ESCRIBIR CODIGO JAVA En primer lugar, debemos crear una clase Java, HolaMundo, que declare un método nativo. También debemos crear el programa principal que cree el objeto HolaMundo y llame al método nativo. Las siguientes líneas de código definen la clase HolaMundo, que consta de un método y un segmento estático de código: class HolaMundo { public native void presentaSaludo(); static { System.loadLibrary( "hola" ); } }

Podemos decir que la implementación del método presentaSaludo() de la clase HolaMundo está escrito en otro lenguaje, porque la palabra reservada native aparece como parte de la definición del método. Esta definición, proporciona solamente la definición para presentaSaludo() y no porporciona ninguna implementación para él. La implementación la proporcionaremos desde un fichero fuente separado, escrito en lenguaje C. La definición para presentaSaludo() también indica que el método es un método público, no acepta argumentos y no devuelve ningún valor. Al igual que cualquier otro método, los métodos nativos deben estar definidos dentro de una clase Java. El código C que implementa el método presentaSaludo() debe ser compilado en una librería dinámica y cargado en la clase Java que lo necesite. Esta carga, mapea la implementación del método nativo sobre su definición. El siguiente bloque de código carga la librería dinámica, en este caso hola. El sistema Java ejecutará un bloque de código estático de la clase cuando la cargue. Todo el código anterior forma parte del fichero HolaMundo.java, que contiene la clase HolaMundo. En un fichero separado, Main.java, vamos a crear una aplicación Java que instancie a la clase HolaMundo y llame al método nativo presentaSaludo(). class Main { public static void main( String args[] ) { new HolaMundo().presentaSaludo(); } }

Como se puede observar, llamamos al método nativo del mismo modo que a cualquier otro método Java; añadimos el nombre del método al final del nombre del objeto con un punto ("."). El conjunto de paréntesis que sigue al nombre del método encierra los argumentos que se le pasen. En este caso, el método presentaSaludo() no recibe ningún tipo de argumento.

Compilar el Código Java Utilizaremos ahora el compilador javac para compilar el código Java que hemos desarrollado. Compilaremos los dos ficheros fuentes de código Java que hemos creado, tecleando los siguientes comandos: > javac HolaMundo.java > javac Main.java

CREAR EL FICHERO DE CABECERA Ahora debemos utilizar la aplicación javah para conseguir el fichero de cabecera .h. El fichero de cabecera define una estructura que representa la clase HolaMundo sobre código C y proporciona la definición de una función C para la implementación del método nativo presentaSaludo() definido en ese clase. Ejecutamos javah sobre la clase HolaMundo, con el siguiente comando: > javah HolaMundo

Por defecto, javah creará el nuevo fichero .h en el mismo directorio en que se encuentra el fichero . class, obtenido al compilar con javac el código fuente Java correspondiente a la clase. El fichero que creará, será un fichero de cabecera del mismo nombre que la clase y con extensión .h. Por ejemplo, el comando anterior habrá creado el fichero HolaMundo.h, cuyo contenido será el siguiente: /* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class HolaMundo */ #ifndef _Included_HolaMundo #define _Included_HolaMundo typedef struct ClassHolaMundo { char PAD; /* ANSI C requires structures to have a least one member */ } ClassHolaMundo; HandleTo(HolaMundo); #ifdef __cplusplus extern "C" { #endif __declspec(dllexport) void HolaMundo_presentaSaludo(struct HHolaMundo *); #ifdef __cplusplus } #endif #endif

Este fichero de cabecera contiene la definición de una estructura llamada ClassHolaMundo. Los miembros de esta estructura son paralelos a los miembros de la clase Java correspondiente; es decir, los campos en la estructura corresponden a las variables de la clase. Pero como HolaMundo no tiene ninguna variable, la estructura se encuentra vacía. Se pueden utilizar los miembros de la estructura para referenciar a variables instanciadas de la clase desde las funciones C. Además de la estructura C similar a la clase Java, vemos que la llamada de la función C está declarada como: extern void HolaMundo_presentaSaludo( struct HHolaMundo *);

Esta es la definición de la función C que deberemos escribir para implementar el método nativo presentaSaludo() de la clase HolaMundo. Debemos utilizar esa definición cuando lo implementemos. Si HolaMundo llamase a otros métodos nativos, las definiciones de las funciones también aparecerían

aquí. El nombre de la función C que implementa el método nativo está derivado del nombre del paquete, el nombre de la clase y el nombre del método nativo. Así, el método nativo presentaSaludo() dentro de la clase HolaMundo es HolaMundo_presentaSaludo(). En este ejemplo, no hay nombre de paquete porque HolaMundo se considera englobado dentro del paquete por defecto. La función C acepta un parámetro, aunque el método nativo definido en la clase Java no acepte ninguno. Se puede pensar en este parámetro como si fuese la variable this de C++. En nuestro caso, ignoramos el parámetro this.

CREAR EL FICHERO DE STUBS Volvemos a utilizar la aplicación javah para crear el fichero de stubs, que contiene todas las declaraciones de métodos, con sus llamadas y argumentos, listos para que nosotros rellenemos el cuerpo de los métodos con los algoritmos que necesitemos implementar. Proporciona la unión entre la clase Java y su estructura C paralela. Para generar este fichero, debemos indicar el parámetro .stubs al ejecutar la aplicación javah sobre la clase HolaMundo, de la siguiente forma: > javah -stubs HolaMundo

Del mismo modo que se generaba el fichero .h; el nombre del fichero de stubs será el nombre de la clase con la extensión .c. En nuestro ejemplo, será HolaMundo.c, y su contenido será el siguiente: /* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Stubs for class HolaMundo */ /* SYMBOL: "HolaMundo/presentaSaludo()V", Java_HolaMundo_presentaSaludo_stub */ __declspec(dllexport) stack_item *Java_HolaMundo_presentaSaludo_stub(stack_item *_P_,struct execenv *_EE_) { extern void HolaMundo_presentaSaludo(void *); (void) HolaMundo_presentaSaludo(_P_[0].p); return _P_;

}

ESCRIBIR LA FUNCION C Escribiremos la función C para el método nativo en un fichero fuente de código C. La implementación será una función habitual C, que luego integraremos con la clase Java. La definición de la función C debe ser la misma que la que se ha generado con javah en el fichero HolaMundo.h. La implementación que proponemos la guardaremos en el fichero HolaImp.c, y contendrá las siguientes línea de código: #include #include "HolaMundo.h> #include void HolaMundo_presentaSaludo( struct HHolaMundo *this ) { printf( "Hola Mundo, desde el Tutorial de Java\n" ); return; }

Como se puede ver, la implementación no puede ser más sencilla: hace una llamada a la función printf() para presentar el saludo y sale.

En el código se incluyen tres ficheros de cabecera: StubsPreamble.h



Proporciona la información para que el código C pueda interactuar con el sistema Java. Cuando se escriben métodos nativos, siempre habrá que incluir este fichero en el código fuente C. HolaMundo.h



Es el fichero de cabecera que hemos generado para nuestra clase. Contiene la estructura C que representa la clase Java para la que estamos escribiendo el método nativo y la definición de la función para ese método nativo. stdio.h



Es necesario incluirlo porque utilizamos la función printf() de la librería estándar de C, cuya declaración se encuentra en este fichero de cabecera.

CREAR LA LIBRERIA DINAMICA Utilizaremos el compilador C para compilar el fichero .h, el fichero de stubs y el fichero fuente .c; para crear una librería dinámica. Para crearla, utilizaremos el compilador C de nuestro sistema, haciendo que los ficheros HolaMundo.c y HolaImp.c generen una librería dinámica de nombre hola, que será la que el sistema Java cargue cuando ejecute la aplicación que estamos construyendo. Vamos a ver cómo generamos esta librería en Unix y en Windows '95.

Unix Teclearemos el siguiente comando: % cc -G HolaMundo.c HolaImp.c -o libhola.so

En caso de que no encuentre el compilador los ficheros de cabecera, se puede utilizar el flag -I para indicarle el camino de búsqueda, por ejemplo: % cc -G -I$JAVA_HOME/include HolaMundo.c HolaImp.c -o libhola.so

donde $JAVA_HOME es el directorio donde se ha instalado la versión actual del Java Development Kit.

Windows El comando a utilizar en este caso es el siguiente: c:\>cl HolaMundo.c HolaImp.c -Fhola.dll -MD -LD javai.lib

Este comando funciona con Microsoft Visual C++ 2.x y posteriores. Si queremos indicar al compilador donde se encuentran los ficheros de cabecera y las librerías, tendremos que fijar dos variables de entorno: c:\>SET INCLUDE=%JAVAHOME%\include;%INCLUDE% c:\>SET LIB=%JAVAHOME%\lib;%LIB%

donde %JAVAHOME% es el directorio donde se ha instalado la versión actual del Java Development Kit.

EJECUTAR EL PROGRAMA Y, por fin, utilizaremos el intérprete de Java, java, para ejecutar el programa que hemos construido siguiendo todos los pasos anteriormente descritos. Si tecleamos el comando: > java Main

obtendremos el resultado siguiente: % Hola Mundo, desde el Tutorial de Java

Si no aparece este mensaje de saludo y lo que aparece en pantalla son expresiones como UnsatisfiedLinkError, es porque no tenemos fijado correctamente el camino de la librería dinámica que hemos generado. Este camino es la lista de directorios que el sistema Java utilizará para buscar las librerías que debe cargar. Debemos asegurarnos de que el directorio donde se encuentra nuestra librería hola recién creada, figura entre ellos. Si fijamos el camino correcto y ejecutamos de nuevo el programa, veremos que ahora sí obtenemos el mensaje de saludo que esperábamos. Con ello, hemos visto como integrar código C en programas Java.

ENTRADA / SALIDA STANDARD 1. La clase System ● Stdin ● Stdout ● Stderr 2. Clases comunes de Entrada/Salida Los usuarios de Unix, y aquellos familiarizados con las líneas de comandos de otros sistemas como DOS, han utilizado un tipo de entrada/salida conocida comúnmente por entrada/salida estándar. El fichero de entrada estándar (stdin) es simplemente el teclado. El fichero de salida estándar (stdout) es típicamente la pantalla (o la ventana del terminal). El fichero de salida de error estándar (stderr) también se dirige normalmente a la pantalla, pero se implementa como otro fichero de forma que se pueda distinguir entre la salida normal y (si es necesario) los mensajes de error.

La clase System Java tiene acceso a la entrada/salida estándar a través de la clase System. En concreto, los tres ficheros que se implementan son: Stdin System.in implementa stdin como una instancia de la clase InputStream. Con System.in, se accede a los métodos read() y skip(). El método read() permite leer un byte de la entrada. skip( long n ), salta n bytes de la entrada. Stdout System.out implementa stdout como una instancia de la clase PrintStream. Se pueden utilizar los métodos print() y println() con cualquier tipo básico Java como argumento. Stderr System.err implementa stderr de la misma forma que stdout. Como con System.out, se tiene acceso a los métodos de PrintStream. Vamos a ver un pequeño ejemplo de entrada/salida en Java. El código siguiente, miType.java, reproduce, o funciona como la utilidad cat de Unix o type de DOS: import java.io.*; class miType { public static void main( String args[] ) throws IOException { int c; int contador = 0;

}

while( (c = System.in.read() ) != '\n' ) { contador++; System.out.print( (char)c ); } System.out.println(); // Línea en blanco System.err.println( "Contados "+ contador +" bytes en total." ); }

Clases comunes de Entrada/Salida Además de la entrada por teclado y salida por pantalla, se necesita entrada/salida por fichero, como son: FileInputStream DataInputStream FileOutputStream DataOutputStream

También existen otras clases para aplicaciones más específicas, que no vamos a tratar, por ser de un uso muy concreto: PipedInputStream BufferedInputStream PushBackInputStream StreamTokenizer PipedOutputStream BufferedOutputStream

FICHEROS EN JAVA 1. Ficheros ● Creación de un objeto File ● Comprobaciones y Utilidades 2. Streams de Entrada ● Objetos FileInputStream 1. Apertura de un FileInputStream 2. Lectura de un FileInputStream 3. Cierre de FileInputStream 4. Ejemplo: Visualización de un fichero ● Objetos DataInputStream 1. Apertura y cierre de DataInputStream 2. Lectura de un DataInputStream ● Streams de entrada de URLs 1. Apertura de un Stream de entrada 3. Streams de Salida ● Objetos FileOutputStream 1. Apertura de un FileOutputStream 2. Escritura en un FileOutputStream 3. Cierre de FileOutputStream 4. Ejemplo: Almacenamiento de Información ● Streams de salida con buffer 1. Creación de Streams de salida con buffer 2. Volcado y Cierre de Streams de salida con buffer ● Streams DataOutput 1. Apertura y cierre de objetos DataOutputStream 2. Escritura en un objeto DataOutputStream 3. Contabilidad de la salida 4. Ficheros de Acceso Aleatorio ● Creación de un Fichero de Acceso Aleatorio ● Acceso a la Información ● Actualización de Información Todos los lenguajes de programación tienen alguna forma de interactuar con los sistemas de ficheros locales; Java no es una excepción. Cuando se desarrollan applets para utilizar en red, hay que tener en cuenta que la entrada/salida

directa a fichero es una violación de seguridad de acceso. Muchos usuarios configurarán sus navegadores para permitir el acceso al sistema de ficheros, pero otros no. Por otro lado, si se está desarrollando una aplicación Java para uso interno, probablemente será necesario el acceso directo a ficheros.

Ficheros Antes de realizar acciones sobre un fichero, necesitamos un poco de información sobre ese fichero. La clase File proporciona muchas utilidades relacionadas con ficheros y con la obtención de información básica sobre esos ficheros. Creación de un objeto File Para crear un objeto File nuevo, se puede utilizar cualquiera de los tres constructores siguientes: File miFichero; miFichero = new File( "/etc/kk" );

o miFichero = new File( "/etc","kk" );

o File miDirectorio = new File( "/etc" ); miFichero = new File( miDirectorio,"kk" );

El constructor utilizado depende a menudo de otros objetos File necesarios para el acceso. Por ejemplo, si sólo se utiliza un fichero en la aplicación, el primer constructor es el mejor. Si en cambio, se utilizan muchos ficheros desde un mismo directorio, el segundo o tercer constructor serán más cómodos. Y si el directorio o el fichero es una variable, el segundo constructor será el más útil. Comprobaciones y Utilidades Una vez creado un objeto File, se puede utilizar uno de los siguientes métodos para reunir información sobre el fichero: ●

Nombres de fichero String getName() String getPath() String getAbsolutePath() String getParent() boolean renameTo( File nuevoNombre )



Comprobaciones boolean boolean boolean boolean boolean boolean



exists() canWrite() canRead() isFile() isDirectory() isAbsolute()

Información general del fichero long lastModified() long length()



Utilidades de directorio

boolean mkdir() String[] list()

Vamos a desarrollar una pequeña aplicación que muestra información sobre los ficheros pasados como argumentos en la línea de comandos, InfoFichero.java: import java.io.*; class InfoFichero {

}

public static void main( String args[] ) throws IOException { if( args.length > 0 ) { for( int i=0; i < args.length; i++ ) { File f = new File( args[i] ); System.out.println( "Nombre: "+f.getName() ); System.out.println( "Camino: "+f.getPath() ); if( f.exists() ) { System.out.print( "Fichero existente " ); System.out.print( (f.canRead() ? " y se puede Leer" : "" ) ); System.out.print( (f.canWrite() ? " y se puese Escribir" : "" ) ); System.out.println( "." ); System.out.println( "La longitud del fichero son "+ f.length()+" bytes" ); } else System.out.println( "El fichero no existe." ); } } else System.out.println( "Debe indicar un fichero." ); }

STREAMS DE ENTRADA Hay muchas clases dedicadas a la obtención de entrada desde un fichero. Este es el esquema de la jerarquía de clases de entrada por fichero:

Objetos FileInputStream Los objetos FileInputStream típicamente representan ficheros de texto accedidos en orden secuencial,

byte a byte. Con FileInputStream, se puede elegir acceder a un byte, varios bytes o al fichero completo. Apertura de un FileInputStream Para abrir un FileInputStream sobre un fichero, se le da al constructor un String o un objeto File: FileInputStream mi FicheroSt; miFicheroSt = new FileInputStream( "/etc/kk" );

También se puede utilizar: File miFichero FileInputStream miFicheroSt; miFichero = new File( "/etc/kk" ); miFicheroSt = new FileInputStream( miFichero );

Lectura de un FileInputStream Una vez abierto el FileInputStream, se puede leer de él. El método read() tiene muchas opciones: int read(); Lee un byte y devuelve -1 al final del stream. int read( byte b[] ); Llena todo el array, si es posible. Devuelve el número de bytes leídos o -1 si se alcanzó el final del stream. int read( byte b[],int offset,int longitud ); Lee longitud bytes en b comenzando por b[offset]. Devuelve el número de bytes leídos o -1 si se alcanzó el final del stream. Cierre de FileInputStream Cuando se termina con un fichero, existen dos opciones para cerrarlo: explícitamente, o implícitamente cuando se recicla el objeto (el garbage collector se encarga de ello). Para cerrarlo explícitamente, se utiliza el método close(): miFicheroSt.close();

Ejemplo: Visualización de un fichero Si la configuración de la seguridad de Java permite el acceso a ficheros, se puede ver el contenido de un fichero en un objeto TextArea. El código siguiente contiene los elementos necesarios para mostrar un fichero: FileInputStream fis; TextArea ta; public void init() { byte b[] = new byte[1024]; int i; // El buffer de lectura se debe hacer lo suficientemente grande // o esperar a saber el tamaño del fichero String s;

try { fis = new FileInputStream( "/etc/kk" ); } catch( FileNotFoundException e ) { /* Hacer algo */ } try { i = fis.read( b ); } catch( IOException e ) { /* Hacer algo */ } s = new String( b,0 ); ta = new TextArea( s,5,40 ); add( ta ); }

Hemos desarrollado un ejemplo, Agenda.java, en el que partimos de un fichero agenda que dispone de los datos que nosotros deseamos de nuestros amigos, como son: nombre, teléfono y dirección. Si tecleamos un nombre, buscará en el fichero de datos si existe ese nombre y presentará la información que se haya introducido. Para probar, intentar que aparezca la información de Pepe.

Objetos DataInputStream Los objetos DataInputStream se comportan como los FileInputStream. Los streams de datos pueden leer cualquiera de las variables de tipo nativo, como floats, ints o chars. Generalmente se utilizan DataInputStream con ficheros binarios. Apertura y cierre de DataInputStream Para abrir y cerrar un objeto DataInputStream, se utilizan los mismos métodos que para FileInputStream: DataInputStream miDStream; FileInputStream miFStream; // Obtiene un controlador de fichero miFStream = new FileInputStream "/etc/ejemplo.dbf" ); //Encadena un fichero de entrada de datos miDStream = new DataInputStream( miFStream ); // Ahora se pueden utilizar los dos streams de entrada para // acceder al fichero (si se quiere...) miFStream.read( b ); i = miDStream.readInt(); // Cierra el fichero de datos explícitamente //Siempre se cierra primero el fichero stream de mayor nivel miDStream.close(); miFStream.close();

Lectura de un DataInputStream Al acceder a un fichero como DataInputStream, se pueden utilizar los mismos métodos read() de los objetos FileInputStream. No obstante, también se tiene acceso a otros métodos diseñados para leer cada uno de los tipos de datos: byte readByte() int readUnsignedByte() short readShort() int readUnsignedShort()

char readChar() int readInt() long readLong() float readFloat() double readDouble() String readLine()

Cada método leerá un objeto del tipo pedido. Para el método String readLine(), se marca el final de la cadena con \n, \r, \r\n o con EOF. Para leer un long, por ejemplo: long numeroSerie; ... numeroSerie = miDStream.readLong();

Streams de entrada de URLs Además del acceso a ficheros, Java proporciona la posibilidad de acceder a URLs como una forma de acceder a objetos a través de la red. Se utiliza implícitamente un objeto URL al acceder a sonidos e imágenes, con el método getDocumentBase() en los applets: String imagenFich = new String( "imagenes/pepe.gif" ); imagenes[0] = getImage( getDocumentBase(),imagenFich );

No obstante, se puede proporcionar directamente un URL, si se quiere: URL imagenSrc; imagenSrc = new URL( "http://enterprise.com/~info" ); imagenes[0] = getImage( imagenSrc,"imagenes/pepe.gif" );

Apertura de un Stream de entrada de URL También se puede abrir un stream de entrada a partir de un URL. Por ejemplo, se puede utilizar un fichero de datos para un applet: ImputStream is; byte buffer[] = new byte[24]; is = new URL( getDocumentBase(),datos).openStream();

Ahora se puede utilizar is para leer información de la misma forma que se hace con un objeto FileInputStream: is.read( buffer,0,buffer.length ); NOTA: Debe tenerse muy en cuenta que algunos usuarios pueden haber configurado la seguridad de sus navegadores para que los applets no accedan a ficheros.

STREAMS DE SALIDA La contrapartida necesaria de la lectura de datos es la escritura de datos. Como con los Streams de entrada, las clases de salida están ordenadas jerárquicamente:

Examinaremos las clases FileOutputStream y DataOutputStream para complementar los streams de entrada que se han visto. En los ficheros fuente del directorio $JAVA_HOME/src/java/io se puede ver el uso y métodos de estas clases, así como de los streams de entrada ($JAVA_HOME es el directorio donde se haya instalado el Java Development Kit, en sistemas UNIX).

Objetos FileOutputStream Los objetos FileOutputStream son útiles para la escritura de ficheros de texto. Como con los ficheros de entrada, primero se necesita abrir el fichero para luego escribir en él. Apertura de un FileOutputStream Para abrir un objeto FileOutputStream, se tienen las mismas posibilidades que para abrir un fichero stream de entrada. Se le da al constructor un String o un objeto File. FileOutputStream miFicheroSt; miFicheroSt = new FileOutputStream( "/etc/kk" );

Como con los streams de entrada, también se puede utilizar: File miFichero FileOutputStream miFicheroSt; miFichero = new File( "/etc/kk" ); miFicheroSt = new FileOutputStream( miFichero );

Escritura en un FileOutputStream Una vez abierto el fichero, se pueden escribir bytes de datos utilizando el método write(). Como con el método read() de los streams de entrada, tenemos tres posibilidades: void write( int b ); Escribe un byte. void write( byte b[] ); Escribe todo el array, si es posible. void write( byte b[],int offset,int longitud ); Escribe longitud bytes en b comenzando por b[offset]. Cierre de FileOutputStream Cerrar un stream de salida es similar a cerrar streams de entrada. Se puede utilizar el método explícito: miFicheroSt.close();

O, se puede dejar que el sistema cierre el fichero cuando se recicle miFicheroSt.

Ejemplo: Almacenamiento de Información Este programa, Telefonos.java, pregunta al usuario una lista de nombres y números de teléfono. Cada nombre y número se añade a un fichero situado en una localización fija. Para indicar que se ha introducido toda la lista, el usuario especifica "Fin" ante la solicitud de entrada del nombre. Una vez que el usuario ha terminado de teclear la lista, el programa creará un fichero de salida que se mostrará en pantalla o se imprimirá. Por ejemplo: 95-4751232,Juanito 564878,Luisa 123456,Pepe 347698,Antonio 91-3547621,Maria

El código fuente del programa es el siguiente: import java.io.*; class Telefonos { static FileOutputStream fos; public static final int longLinea = 81; public static void main( String args[] ) throws IOException { byte tfno[] = new byte[longLinea]; byte nombre[] = new byte[longLinea]; fos = new FileOutputStream( "telefono.dat" ); while( true ) { System.err.println( "Teclee un nombre ('Fin' termina)" ); leeLinea( nombre ); if( "fin".equalsIgnoreCase( new String( nombre,0,0,3 ) ) ) break; System.err.println( "Teclee el numero de telefono" ); leeLinea( tfno ); for( int i=0; tfno[i] != 0; i++ ) fos.write( tfno[i] ); fos.write( ',' ); for( int i=0; nombre[i] != 0; i++ ) fos.write( nombre[i] ); fos.write( '\n' ); } fos.close(); } private static void leeLinea( byte linea[] ) throws IOException { int b = 0; int i = 0;

}

while( (i < ( longLinea-1) ) && ( ( b = System.in.read() ) != '\n' ) ) linea[i++] = (byte)b; linea[i] = (byte)0; }

Streams de salida con buffer Si se trabaja con gran cantidad de datos, o se escriben muchos elementos pequeños, será una buena idea utilizar un stream de salida con buffer. Los streams con buffer ofrecen los mismos métodos de la

clase FileOutputStream, pero toda salida se almacena en un buffer. Cuando se llena el buffer, se envía a disco con una única operación de escritura; o, en caso necesario, se puede enviar el buffer a disco en cualquier momento. Creación de Streams de salida con buffer Para crear un stream BufferedOutput, primero se necesita un stream FileOutput normal; entonces se le añade un buffer al stream: FileOutputStream miFileStream; BufferdOutpurStream miBufferStream; // Obtiene un controlador de fichero miFileStream = new FileOutputStream( "/tmp/kk" ); // Encadena un stream de salida con buffer miBufferStream = new BufferedOutputStream( miFileStream );

Volcado y Cierre de Streams de salida con buffer Al contrario que los streams FileOutput, cada escritura al buffer no se corresponde con una escritura en disco. A menos que se llene el buffer antes de que termine el programa, cuando se quiera volcar el buffer explícitamente se debe hacer mediante una llamada a flush(): // Se fuerza el volcado del buffer a disco miBufferStream.flush(); // Cerramos el fichero de datos. Siempre se ha de cerrar primero el // fichero stream de mayor nivel miBufferStream.close(); miFileStream.close();

Streams DataOutput Java también implementa una clase de salida complementaria a la clase DataInputStream. Con la clase DataOutputStream, se pueden escribir datos binarios en un fichero. Apertura y cierre de objetos DataOutputStream Para abrir y cerrar objetos DataOutputStream, se utilizan los mismos métodos que para los objetos FileOutputStream: DataOutputStream miDataStream; FileOutputStream miFileStream; BufferedOutputStream miBufferStream; // Obtiene un controlador de fichero miFileStream = new FileOutputStream( "/tmp/kk" ); // Encadena un stream de salida con buffer (por eficiencia) miBufferStream = new BufferedOutputStream( miFileStream ); // Encadena un fichero de salida de datos miDataStream = new DataOutputStream( miBufferStream ); // Ahora se pueden utilizar los dos streams de entrada para // acceder al fichero (si se quiere) miBufferStream.write( b ); miDataStream.writeInt( i ); // Cierra el fichero de datos explícitamente. Siempre se cierra // primero el fichero stream de mayor nivel miDataStream.close(); miBufferStream.close(); miFileStream.close();

Escritura en un objeto DataOutputStream Cada uno de los métodos write() accesibles por los FileOutputStream también lo son a través de los DataOutputStream. También encontrará métodos complementarios a los de DataInputStream: void void void void void void void void void

writeBoolean( boolean b ); writeByte( int i ); writeShort( int i ); writeChar( int i ); writeInt( int i ); writeFloat( float f ); writeDouble( double d ); writeBytes( String s ); writeChars( string s );

Para las cadenas, se tienen dos posibilidades: bytes y caracteres. Hay que recordar que los bytes son objetos de 8 bits y los caracteres lo son de 16 bits. Si nuestras cadenas utilizan caracteres Unicode, debemos escribirlas con writeChars(). Contabilidad de la salida Otra función necesaria durante la salida es el método size(). Este método simplemente devuelve el número total de bytes escritos en el fichero. Se puede utilizar size() para ajustar el tamaño de un fichero a múltiplo de cuatro. Por ejemplo, de la forma siguiente: . . . int numBytes = miDataStream.size() % 4; for( int i=0; i < numBytes; i++ ) miDataStream.write( 0 ); . . .

FICHEROS DE ACCESO ALEATORIO A menudo, no se desea leer un fichero de principio a fin; sino acceder al fichero como una base de datos, donde se salta de un registro a otro; cada uno en diferentes partes del fichero. Java proporciona una clase RandomAccessFile para este tipo de entrada/salida.

Creación de un Fichero de Acceso Aleatorio Hay dos posibilidades para abrir un fichero de acceso aleatorio: Con el nombre del fichero: miRAFile = new RandomAccessFile( String nombre,String modo );

Con un objeto File: miRAFile = new RandomAccessFile( File fichero,String modo );

El argumento modo determina si se tiene acceso de sólo lectura (r) o de lectura/escritura (r/w). Por ejemplo, se puede abrir un fichero de una base de datos para actualización: RandomAccessFile miRAFile; miRAFile = new RandomAccessFile( "/tmp/kk.dbf","rw" );

Acceso a la Información Los objetos RandomAccessFile esperan información de lectura/escritura de la misma manera que los objetos DataInput/DataOutput. Se tiene acceso a todas las operaciones read() y write() de las clases DataInputStream y DataOutputStream. También se tienen muchos métodos para moverse dentro de un fichero: long getFilePointer(); Devuelve la posición actual del puntero del fichero void seek( long pos ); Coloca el puntero del fichero en una posición determinada. La posición se da como un desplazamiento en bytes desde el comienzo del fichero. La posición 0 marca el comienzo de ese fichero. long length(); Devuelve la longitud del fichero. La posición length() marca el final de ese fichero.

Actualización de Información Se pueden utilizar ficheros de acceso aleatorio para añadir información a ficheros existentes: miRAFile = new RandomAccessFile( "/tmp/kk.log","rw" ); miRAFile.seek( miRAFile.length() ); // Cualquier write() que hagamos a partir de este punto del código // añadirá información al fichero Vamos a ver un pequeño ejemplo, Log.java, que añade una cadena a un fichero existente: import java.io.*; // Cada vez que ejecutemos este programita, se incorporara una nueva // linea al fichero de log que se crea la primera vez que se ejecuta // class Log { public static void main( String args[] ) throws IOException { RandomAccessFile miRAFile; String s = "Informacion a incorporar\nTutorial de Java\n";

}

// Abrimos el fichero de acceso aleatorio miRAFile = new RandomAccessFile( "/tmp/java.log","rw" ); // Nos vamos al final del fichero miRAFile.seek( miRAFile.length() ); // Incorporamos la cadena al fichero miRAFile.writeBytes( s ); // Cerramos el fichero miRAFile.close(); }

COMUNICACIONES EN JAVA 1. Comunicaciones en Unix 2. Sockets ● Sockets Stream ● Sockets Datagrama ● Sockets Raw 3. Diferencias entre Sockets Stream y Datagrama 4. Uso de Sockets ● Puertos y Servicios ● La clase URL 5. Dominios de Comunicaciones ● Dominio Unix ● Dominio Internet 6. Modelo de Comunicaciones con Java ● Apertura de Sockets ● Creación de Streams de Entrada ● Creación de Streams de Salida ● Cierre de Sockets ● Mínimo Cliente SMTP ● Servidor de Eco ● Mínimo Servidor TCP/IP ● Mínimo Cliente TCP/IP ● Servidor Simple de HTTP ● Red en Windows '95 (sin conexión) 1. Configuración del TCP/IP de Windows '95 2. Crear una entrada en la Red 3. Comprobación de la Red 4. Problemas más frecuentes 7. Clases Utiles en Comunicaciones En este capítulo no nos vamos a extender demasiado en profundidades sobre la comunicación y funcionamiento de redes, aunque sí proporcionaremos un breve baño inicial para sentar, o recordar, los fundamentos de la comunicación en red, tomando como base Unix. Presentaremos un ejemplo básico de cliente/servidor sobre sockets TCP/IP, proporcionando un punto de partida para el desarrollo de otras aplicaciones cliente/servidor basadas en sockets, que posteriormente implementaremos.

COMUNICACIONES EN UNIX El sistema de Entrada/Salida de Unix sigue el paradigma que normalmente se designa como AbrirLeer-Escribir-Cerrar. Antes de que un proceso de usuario pueda realizar operaciones de entrada/salida, debe hacer una llamada a Abrir (open) para indicar, y obtener permisos para su uso, el fichero o dispositivo que quiere utilizar. Una vez que el objeto está abierto, el proceso de usuario realiza una o varias llamadas a Leer (read) y Escribir (write), para conseguir leer y escribir datos. Leer coge datos desde el objeto y los transfiere al proceso de usuario, mientras que Escribir transfiere datos desde el proceso de usuario al objeto. Una vez que todos estos intercambios de información estén concluidos, el proceso de usuario llamará a Cerrar (close) para informar al sistema operativo que ha finalizado la utilización del objeto que antes había abierto. Cuando se incorporan las características a Unix de comunicación entre procesos (IPC) y el manejo de redes, la idea fue implementar la interface con IPC similar a la que se estaba utilizando para la

entrada/salida de ficheros, es decir, siguiendo el paradigma del párrafo anterior. En Unix, un proceso tiene un conjunto de descriptores de entrada/salida desde donde Leer y por donde Escribir. Estos descriptores pueden estar referidos a ficheros, dispositivos, o canales de comunicaciones (sockets). El ciclo de vida de un descriptor, aplicado a un canal de comunicación (socket), está determinado por tres fases (siguiendo el paradigma): ● ● ●

Creación, apertura del socket Lectura y Escritura, recepción y envío de datospor el socket Destrucción, cierre del socket

La interface IPC en Unix-BSD está implementada sobre los protocolos de red TCP y UDP. Los destinatarios de los mensajes se especifican como direcciones de socket; cada dirección de socket es un identificador de comunicación que consiste en una dirección Internet y un número de puerto. Las operaciones IPC se basan en pares de sockets. Se intercambian información transmitiendo datos a través de mensajes que circulan entre un socket en un proceso y otro socket en otro proceso. Cuando los mensajes son enviados, se encolan en el socket hasta que el protocolo de red los haya transmitido. Cuando llegan, los mensajes son encolados en el socket de recepción hasta que el proceso que tiene que recibirlos haga las llamadas necesarias para recoger esos datos.

SOCKETS Los sockets son puntos finales de enlaces de comunicaciones entre procesos. Los procesos los tratan como descriptores de ficheros, de forma que se pueden intercambiar datos con otros procesos transmitiendo y recibiendo a través de sockets. El tipo de sockets describe la forma en la que se transfiere información a través de ese socket. Sockets Stream (TCP, Transport Control Protocol) Son un servicio orientado a conexión donde los datos se transfieren sin encuadrarlos en registros o bloques. Si se rompe la conexión entre los procesos, éstos serán informados. El protocolo de comunicaciones con streams es un protocolo orientado a conexión, ya que para establecer una comunicación utilizando el protocolo TCP, hay que establecer en primer lugar una conexión entre un par de sockets. Mientras uno de los sockets atiende peticiones de conexión (servidor), el otro solicita una conexión (cliente). Una vez que los dos sockets estén conectados, se pueden utilizar para transmitir datos en ambas direcciones. Sockets Datagrama (UDP, User Datagram Protocol) Son un servicio de transporte sin conexión. Son más eficientes que TCP, pero no está garantizada la fiabilidad. Los datos se envían y reciben en paquetes, cuya entrega no está garantizada. Los paquetes pueden ser duplicados, perdidos o llegar en un orden diferente al que se envió. El protocolo de comunicaciones con datagramas es un protocolo sin conexión, es decir, cada vez que se envíen datagramas es necesario enviar el descriptor del socket local y la dirección del socket que debe recibir el datagrama. Como se puede ver, hay que enviar datos adicionales cada vez que se realice una comunicación. Sockets Raw Son sockets que dan acceso directo a la capa de software de red subyacente o a protocolos de más bajo nivel. Se utilizan sobre todo para la depuración del código de los protocolos.

Diferencias entre Sockets Stream y Datagrama Ahora se nos presenta un problema, ¿qué protocolo, o tipo de sockets, debemos usar - UDP o TCP? La decisión depende de la aplicación cliente/servidor que estemos escribiendo. Vamos a ver algunas diferencias entre los protocolos para ayudar en la decisión. En UDP, cada vez que se envía un datagrama, hay que enviar también el descriptor del socket local y la dirección del socket que va a recibir el datagrama, luego éstos son más grandes que los TCP. Como el protocolo TCP está orientado a conexión, tenemos que establecer esta conexión entre los dos sockets antes de nada, lo que implica un cierto tiempo empleado en el establecimiento de la conexión, que no existe en UDP. En UDP hay un límite de tamaño de los datagramas, establecido en 64 kilobytes, que se pueden enviar a una localización determinada, mientras que TCP no tiene límite; una vez que se ha establecido la conexión, el par de sockets funciona como los streams: todos los datos se leen inmediatamente, en el mismo orden en que se van recibiendo. UDP es un protocolo desordenado, no garantiza que los datagramas que se hayan enviado sean recibidos en el mismo orden por el socket de recepción. Al contrario, TCP es un protocolo ordenado, garantiza que todos los paquetes que se envíen serán recibidos en el socket destino en el mismo orden en que se han enviado. Los datagramas son bloques de información del tipo lanzar y olvidar. Para la mayoría de los programas que utilicen la red, el usar un flujo TCP en vez de un datagrama UDP es más sencillo y hay menos posibilidades de tener problemas. Sin embargo, cuando se requiere un rendimiento óptimo, y está justificado el tiempo adicional que supone realizar la verificación de los datos, los datagramas son un mecanismo realmente útil. En resumen, TCP parece más indicado para la implementación de servicios de red como un control remoto (rlogin, telnet) y transmisión de ficheros (ftp); que necesitan transmitir datos de longitud indefinida. UDP es menos complejo y tiene una menor sobrecarga sobre la conexión; esto hace que sea el indicado en la implementación de aplicaciones cliente/servidor en sistemas distribuidos montados sobre redes de área local.

USO DE SOCKETS Podemos pensar que un Servidor Internet es un conjunto de sockets que proporciona capacidades adicionales del sistema, los llamados servicios. Puertos y Servicios Cada servicio está asociado a un puerto. Un puerto es una dirección numérica a través de la cual se procesa el servicio. Sobre un sistema Unix, los servicios que proporciona ese sistema se indican en el fichero /etc/services, y algunos ejemplos son: daytime ftp telnet smtp http

13/udp 21/tcp 23/tcp 25/tcp 80/tcp

telnet mail

La primera columna indica el nombre del servicio. La segunda columna indica el puerto y el protocolo que está asociado al servicio. La tercera columna es un alias del servicio; por ejemplo, el servicio smtp, también conocido como mail, es la implementación del servicio de correo electrónico. Las comunicaciones de información relacionada con Web tienen lugar a través del puerto 80 mediante protocolo TCP. Para emular esto en Java, usaremos la clase Socket. La fecha (daytime).

Sin embargo, el servicio que coge la fecha y la hora del sistema, está ligado al puerto 13 utilizando el protocolo UDP. Un servidor que lo emule en Java usaría un objeto DatagramSocket.

DOMINIOS DE COMUNICACIONES El mecanismo de sockets está diseñado para ser todo lo genérico posible. El socket por sí mismo no contiene información suficiente para describir la comunicación entre procesos. Los sockets operan dentro de dominios de comunicación, entre ellos se define si los dos procesos que se comunican se encuentran en el mismo sistema o en sistemas diferentes y cómo pueden ser direccionados. Dominio Unix Bajo Unix, hay dos dominios, uno para comunicaciones internas al sistema y otro para comunicaciones entre sistemas. Las comunicaciones intrasistema (entre dos procesos en el mismo sistema) ocurren (en una máquina Unix) en el dominio Unix. Se permiten tanto los sockets stream como los datagrama. Los sockets de dominio Unix bajo Solaris 2.x se implementan sobre TLI (Transport Level Interface). En el dominio Unix no se permiten sockets de tipo Raw. Dominio Internet Las comunicaciones intersistemas proporcionan acceso a TCP, ejecutando sobre IP (Internet Protocol). De la misma forma que el dominio Unix, el dominio Internet permite tanto sockets stream como datagrama, pero además permite sockets de tipo Raw. Los sockets stream permiten a los procesos comunicarse a través de TCP. Una vez establecidas las conexiones, los datos se pueden leer y escribir a/desde los sockets como un flujo (stream) de bytes. Algunas aplicaciones de servicios TCP son: ● ● ●

File Tranfer Protocol, FTP Simple Mail Transfer Protocol, SMTP TELNET, servicio de conexión de terminal remoto

Los sockets datagrama permiten a los procesos utilizar el protocolo UDP para comunicarse a y desde esos sockets por medio de bloques. UDP es un protocolo no fiable y la entrega de los paquetes no está garantizada. Servicios UDP son: ● ● ●

Simple Network Management Protocol, SNMP Trivial File Transfer Protocol, TFTP (versión de FTP sin conexión) Versatile Message Transaction Protocol, VMTP (servicio fiable de entrega punto a punto de datagramas independiente de TCP)

Los sockets raw proporcionan acceso al Internet Control Message Protocol, ICMP, y se utiliza para comunicarse entre varias entidades IP.

MODELO DE COMUNICACIONES CON JAVA En Java, crear una conexión socket TCP/IP se realiza directamente con el paquete java.net. A continuación mostramos un diagrama de lo que ocurre en el lado del cliente y del servidor:

El modelo de sockets más simple es: ●

● ●

El servidor establece un puerto y espera durante un cierto tiempo (timeout segundos), a que el cliente establezca la conexión. Cuando el cliente solicite una conexión, el servidor abrirá la conexión socket con el método accept(). El cliente establece una conexión con la máquina host a través del puerto que se designe en puerto# El cliente y el servidor se comunican con manejadores InputStream y OutputStream

Hay una cuestión al respecto de los sockets, que viene impuesta por la implementación del sistema de seguridad de Java. Actualmente, los applets sólo pueden establecer conexiones con el nodo desde el cual se transfirió su código. Esto está implementado en el JDK y en el intérprete de Java de Netscape. Esto reduce en gran manera la flexibilidad de las fuentes de datos disponibles para los applets. El problema si se permite que un applet se conecte a cualquier máquina de la red, es que entonces se podrían utilizar los applets para inundar la red desde un ordenador con un cliente Netscape del que no se sospecha y sin ninguna posibilidad de rastreo.

APERTURA DE SOCKETS Si estamos programando un cliente, el socket se abre de la forma: Socket miCliente; miCliente = new Socket( "maquina",numeroPuerto );

Donde maquina es el nombre de la máquina en donde estamos intentando abrir la conexión y numeroPuerto es el puerto (un número) del servidor que está corriendo sobre el cual nos queremos conectar. Cuando se selecciona un número de puerto, se debe tener en cuenta que los puertos en el rango 0-1023 están reservados para usuarios con muchos privilegios (superusuarios o root). Estos puertos son los que utilizan los servicios estándar del sistema como email, ftp o http. Para las aplicaciones que se desarrollen, asegurarse de seleccionar un puerto por encima del 1023. En el ejemplo anterior no se usan excepciones; sin embargo, es una gran idea la captura de excepciones cuando se está trabajando con sockets. El mismo ejemplo quedaría como: Socket miCliente; try { miCliente = new Socket( "maquina",numeroPuerto );

} catch( IOException e ) { System.out.println( e ); }

Si estamos programando un servidor, la forma de apertura del socket es la que muestra el siguiente ejemplo: Socket miServicio; try { miServicio = new ServerSocket( numeroPuerto ); } catch( IOException e ) { System.out.println( e ); }

A la hora de la implementación de un servidor también necesitamos crear un objeto socket desde el ServerSocket para que esté atento a las conexiones que le puedan realizar clientes potenciales y poder aceptar esas conexiones: Socket socketServicio = null; try { socketServicio = miServicio.accept(); } catch( IOException e ) { System.out.println( e ); }

CREACION DE STREAMS Creación de Streams de Entrada En la parte cliente de la aplicación, se puede utilizar la clase DataInputStream para crear un stream de entrada que esté listo a recibir todas las respuestas que el servidor le envíe. DataInputStream entrada; try { entrada = new DataInputStream( miCliente.getInputStream() ); } catch( IOException e ) { System.out.println( e ); }

La clase DataInputStream permite la lectura de líneas de texto y tipos de datos primitivos de Java de un modo altamente portable; dispone de métodos para leer todos esos tipos como: read(), readChar(), readInt(), readDouble() y readLine(). Deberemos utilizar la función que creamos necesaria dependiendo del tipo de dato que esperemos recibir del servidor. En el lado del servidor, también usaremos DataInputStream, pero en este caso para recibir las entradas que se produzcan de los clientes que se hayan conectado: DataInputStream entrada; try { entrada = new DataInputStream( socketServicio.getInputStream() ); } catch( IOException e ) { System.out.println( e ); }

Creación de Streams de Salida En el lado del cliente, podemos crear un stream de salida para enviar información al socket del

servidor utilizando las clases PrintStream o DataOutputStream: PrintStream salida; try { salida = new PrintStream( miCliente.getOutputStream() ); } catch( IOException e ) { System.out.println( e ); }

La clase PrintStream tiene métodos para la representación textual de todos los datos primitivos de Java. Sus métodos write y println() tienen una especial importancia en este aspecto. No obstante, para el envío de información al servidor también podemos utilizar DataOutputStream: DataOutputStream salida; try { salida = new DataOutputStream( miCliente.getOutputStream() ); } catch( IOException e ) { System.out.println( e ); }

La clase DataOutputStream permite escribir cualquiera de los tipos primitivos de Java, muchos de sus métodos escriben un tipo de dato primitivo en el stream de salida. De todos esos métodos, el más útil quizás sea writeBytes(). En el lado del servidor, podemos utilizar la clase PrintStream para enviar información al cliente: PrintStream salida; try { salida = new PrintStream( socketServicio.getOutputStream() ); } catch( IOException e ) { System.out.println( e ); }

Pero también podemos utilizar la clase DataOutputStream como en el caso de envío de información desde el cliente.

CIERRE DE SOCKETS Siempre deberemos cerrar los canales de entrada y salida que se hayan abierto durante la ejecución de la aplicación. En la parte del cliente: try { salida.close(); entrada.close(); miCliente.close(); } catch( IOException e ) { System.out.println( e ); }

Y en la parte del servidor: try { salida.close(); entrada.close(); socketServicio.close(); miServicio.close(); } catch( IOException e ) { System.out.println( e ); }

MINIMO CLIENTE SMTP Vamos a desarrollar un mínimo cliente SMTP (simple mail transfer protocol), de forma que podamos encapsular todos los datos en la aplicación. El código es libre de modificación para las necesidades que sean; por ejemplo, una modificación interesante sería que aceptase argumentos desde la línea de comandos y también capturase el texto del mensaje desde la entrada estándar del sistema. Con estas modificaciones tendríamos casi la misma aplicación de correo que utiliza Unix. Veamos el código de nuestro cliente, smtpCliente.java: import java.net.*; import java.io.*; class smtpCliente { public static void main( String args[] ) { Socket s = null; DataInputStream sIn = null; DataOutputStream sOut = null; // Abrimos una conexión con breogan en el puerto 25 // que es el correspondiente al protocolo smtp, e intentamos // abrir los streams de entrada y salida try { s = new Socket( "breogan",25 ); sIn = new DataInputStream( s.getInputStream() ); sOut = new DataOutputStream( s.getOutputStream() ); } catch( UnknownHostException e ) { System.out.println( "No conozco el host" ); } catch( IOException e ) { System.out.println( e ); } // Si todo está inicializado correctamente, vamos a escribir // algunos datos en el canal de salida que se ha establecido // con el puerto del protocolo smtp del servidor if( s != null && sIn != null && sOut != null ) { try { // Tenemos que respetar la especificación SMTP dada en // RFC1822/3, de forma que lo que va en mayúsculas // antes de los dos puntos tiene un significado especial // en el protocolo sOut.writeBytes( "MAIL From: [email protected]\n" ); sOut.writeBytes( "RCPT To: [email protected]\n" ); sOut.writeBytes( "DATA\n" ); sOut.writeBytes( "From: [email protected]\n" ); sOut.writeBytes( "Subject: Pruebas\n" ); // Ahora el cuerpo del mensaje sOut.writeBytes( "Hola, desde el Tutorial de Java\n" ); sOut.writeBytes( "\n.\n" ); // Nos quedamos a la espera de recibir el "Ok" del // servidor para saber que ha recibido el mensaje // correctamente, momento en el cual cortamos

String respuesta; while( ( respuesta = sIn.readLine() ) != null ) { System.out.println( "Servidor: "+respuesta ); if( respuesta.indexOf( "Ok" ) != -1 ) break; }

}

}

// Cerramos todo lo que hemos abierto sOut.close(); sIn.close(); s.close(); } catch( UnknownHostException e ) { System.out.println( "Intentando conectar: "+e ); } catch( IOException e ) { System.out.println( e ); } }

SERVIDOR DE ECO En el siguiente ejemplo, vamos a desarrollar un servidor similar al que se ejecuta sobre el puerto 7 de las máquinas Unix, el servidor echo. Básicamente, este servidor recibe texto desde un cliente y reenvía ese mismo texto al cliente. Desde luego, este es el servidor más simple de los simples que se pueden escribir. El ejemplo que presentamos, ecoServidor.java, maneja solamente un cliente. Una modificación interesante sería adecuarlo para que aceptase múltiples clientes simultáneos mediante el uso de threads. import java.net.*; import java.io.*; class ecoServidor { public static void main( String args[] ) { ServerSocket s = null; DataInputStream sIn; PrintStream sOut; Socket cliente = null; String texto; // Abrimos una conexión con breogan en el puerto 9999 // No podemos elegir un puerto por debajo del 1023 si no somos // usuarios con los máximos privilegios (root) try { s = new ServerSocket( 9999 ); } catch( IOException e ) { } // Creamos el objeto desde el cual atenderemos y aceptaremos // las conexiones de los clientes y abrimos los canales de // comunicación de entrada y salida try { cliente = s.accept(); sIn = new DataInputStream( cliente.getInputStream() ); sOut = new PrintStream( cliente.getOutputStream() ); // Cuando recibamos datos, se los devolvemos al cliente // que los haya enviado

}

while( true ) { texto = sIn.readLine(); sOut.println( texto ); } } catch( IOException e ) { System.out.println( e ); } }

CLIENTE/SERVIDOR TCP/IP Mínimo Servidor TCP/IP Veamos el código que presentamos en el siguiente ejemplo, minimoServidor.java, donde desarrollamos un mínimo servidor TCP/IP, para el cual desarrollaremos después su contrapartida cliente TCP/IP. La aplicación servidor TCP/IP depende de una clase de comunicaciones proporcionada por Java: ServerSocket. Esta clase realiza la mayor parte del trabajo de crear un servidor. import java.awt.*; import java.net.*; import java.io.*; class minimoServidor { public static void main( String args[] ) { ServerSocket s = (ServerSocket)null; Socket s1; String cadena = "Tutorial de Java!"; int longCad; OutputStream s1out; // Establece el servidor en el socket 4321 (espera 300 segundos) try { s = new ServerSocket( 4321,300 ); } catch( IOException e ) { System.out.println( e ); } // Ejecuta un bucle infinito de listen/accept while( true ) { try { // Espera para aceptar una conexión s1 = s.accept(); // Obtiene un controlador de fichero de salida asociado // con el socket s1out = s1.getOutputStream(); // Enviamos nuestro texto longCad = sendString.length(); for( int i=0; i < longCad; i++ ) s1out.write( (int)sendString.charAt( i ) );

}

// Cierra la conexión, pero no el socket del servidor s1.close(); } catch( IOException e ) { System.out.println( e ); } }

}

Mínimo Cliente TCP/IP El lado cliente de una aplicación TCP/IP descansa en la clase Socket. De nuevo, mucho del trabajo necesario para establecer la conexión lo ha realizado la clase Socket. Vamos a presentar ahora el código de nuestro cliente más simple, minimoCliente.java, que encaja con el servidor presentado antes. El trabajo que realiza este cliente es que todo lo que recibe del servidor lo imprime por la salida estándar del sistema. import java.awt.*; import java.net.*; import java.io.*; class minimoCliente { public static void main( String args[] ) throws IOException { int c; Socket s; InputStream sIn; // Abrimos una conexión con breogan en el puerto 4321 try { s = new Socket( "breogan",4321 ); } catch( IOException e ) { System.out.println( e ); } // Obtenemos un controlador de fichero de entrada del socket y // leemos esa entrada sIn = s.getInputStream(); while( ( c = sIn.read() ) != -1 ) System.out.print( (char)c );

}

// Cuando se alcance el fin de fichero, cerramos la conexión y // abandonamos s.close(); }

SERVIDOR SIMPLE DE HTTP Vamos a implementar un servidor de HTTP básico, sólo le permitiremos admitir operaciones GET y un rango limitado de tipos MIME codificados. Los tipos MIME son los descriptores de tipo para contenido multimedia. Esperamos que este ejemplo sirva como base para un entretenido ejercicio de ampliación y exploración porque, desde luego, lo que no pretendemos es inquietar a los Consejos de Dirección de Microsoft o Netscape. La aplicación va a crear un ServerSocket conectado al puerto 80, que en caso de no tener privilegios para su uso, podemos cambiar, por ejemplo al 8080; y después entra en un bucle infinito. Dentro del bucle, espera dentro del método accept() del ServerSocket hasta que se establece una conexión cliente. Después asigna un flujo de entrada y salida al socket. A continuación lee la solicitud del cliente utilizando el método getRawRequest(), que devolverá un null si hay un error de entrada/salida o el cliente corta la conexión. Luego se identifica el tipo de solicitud y se gestiona mediante el método handlget() o handleUnsup(). Finalmente se cierran los sockets y se comienza de nuevo. Cuando se ejecuta el programa completo, se escribe en pantalla lo que el navegador cliente envía al servidor. Aunque se capturan varias condiciones de error, en la práctica no aparecen. El ampliar este servidor para que soporte una carga de millones de visitas al día requiere bastante trabajo; no obstante, en el ordenador en que estoy escribiendo esto, no se enlenteció demasiado con una carga de hasta diez

entradas por segundo, lo que permitiría alrededor de un millón de visitas al día. Se podría mejorar mediante el uso de threads y control de la memoria caché para gestionar esas visitas, pero eso ya forma parte del ejercicio sobre el que se puede trabajar. El código fuente de nuestro mini servidor de HTTP se encuentra en el fichero TutHttp.java, que reproducimos a continuación: import java.net.*; import java.io.*; import java.util.*; // Clase de utilidades donde declaramos los tipos MIME y algunos gestores // de los errores que se pueden generar en HTML class HttpUtilidades { final static String version = "1.0"; final static String mime_text_plain = "text/plain"; final static String mime_text_html = "text/html"; final static String mime_image_gif = "image/gif"; final static String mime_image_jpg = "image/jpg"; final static String mime_app_os = "application/octet-stream"; final static String CRLF = "\r\n"; // Método que convierte un objeto String en una matriz de bytes. // Java gestiona las cadenas como objetos, por lo que es necesario // convertir las matrices de bytes que se obtienen a Strings y // viceversa public static byte aBytes( String s )[] { byte b[] = new byte[ s.length() ]; s.getBytes( 0,b.length,b,0 ); return( b ); } // Este método concatena dos matrices de bytes. El método // arraycopy() asombra por su rapidez public static byte concatenarBytes( byte a[],byte b[] )[] { byte ret[] = new byte[ a.length+b.length ]; System.arraycopy( a,0,ret,0,a.length ); System.arraycopy( b,0,ret,a.length,b.length ); return( ret ); } // Este método toma un tipo de contenido y una longitud, para // devolver la matriz de bytes que contiene el mensaje de cabecera // MIME con formato public static byte cabMime( String ct,int tam )[] { return( cabMime( 200,"OK",ct,tam ) ); } // Es el mismo método anterior, pero permite un ajuste más fino // del código que se devuelve y el mensaje de error de HTTP public static byte cabMime(int codigo,String mensaje,String ct, int tam )[] { Date d = new Date(); return( aBytes( "HTTP/1.0 "+codigo+" "+mensaje+CRLF+ "Date: "+d.toGMTString()+CRLF+ "Server: Java/"+version +CRLF+ "Content-type: "+ct+CRLF+ ( tam > 0 ? "Content-length: "+tam+CRLF : "" )+CRLF ) ); } // Este método construye un mensaje HTML con un formato decente // para presentar una condición de error y lo devuelve como // matriz de bytes public static byte error( int codigo,String msg,String fname)[] {

String ret = ""+CRLF+""+codigo+" "+msg+""+CRLF; if( fname != null ) ret += "Error al buscar el URL: "+fname+CRLF; ret += ""+CRLF; byte tmp[] = cabMime( codigo,msg,mime_text_html,0 ); return( concatenarBytes( tmp,aBytes( ret ) ) ); } // Devuelve el tipo MIME que corresponde a un nombre de archivo dado public static String mimeTypeString( String fichero ) { String tipo;

}

if( fichero.endsWith( ".html" ) || fichero.endsWith( ".htm" ) ) tipo = mime_text_html; else if( fichero.endsWith( ".class" ) ) tipo = mime_app_os; else if( fichero.endsWith( ".gif" ) ) tipo = mime_image_gif; else if( fichero.endsWith( ".jpg" ) ) tipo = mime_image_jpg; else tipo = mime_text_plain; return( tipo ); }

// Esta clase sirve para que nos enteremos de lo que está haciendo // nuestro servidor. En una implementación real, todos estos mensajes // deberían registrarse en algún fichero class HTTPlog { public static void error( String entrada ) { System.out.println( "Error: "+entrada ); } public static void peticion( String peticion ) { System.out.println( peticion ); } } // Esta es la clase principal de nuestro servidor Http class TutHttp { public static final int puerto = 80; final static String docRaiz = "/html"; final static String fichIndice = "index.html"; final static int buffer = 2048; public static final int RT_GET=1; public static final int RT_UNSUP=2; public static final int RT_END=4; // Indica que la petición no está soportada, por ejemplo POST y HEAD private static void ctrlNoSop(String peticion,OutputStream sout) { HTTPlog.error( "Peticion no soportada: "+peticion ); } // Este método analiza gramaticalmente la solicitud enviada con el // GET y la descompone en sus partes para extraer el nombre del // archivo que se está solicitando. Entonces lee el fichero que // se pide private static void ctrlGet( String peticion,OutputStream sout ) { int fsp = peticion.indexOf( ' ' ); int nsp = peticion.indexOf( ' ',fsp+1 ); String fich = peticion.substring( fsp+1,nsp );

fich = docRaiz+fich+( fich.endsWith("/") ? fichIndice : "" ); try { File f = new File( fich ); if( !f.exists() ) { sout.write( HttpUtilidades.error( 404, "No Encontrado",fich ) ); return; } if( !f.canRead() ) { sout.write( HttpUtilidades.error( 404, "Permiso Denegado",fich ) ); return; } // Ahora lee el fichero que se ha solicitado InputStream sin = new FileInputStream( f ); String cabmime = HttpUtilidades.mimeTypeString( fich ); int n = sin.available(); sout.write( HttpUtilidades.cabMime( cabmime,n ) ); byte buf[] = new byte[buffer]; while( ( n = sin.read( buf ) ) >= 0 ) sout.write( buf,0,n ); sin.close(); } catch( IOException e ) { HTTPlog.error( "Excepcion: "+e ); } } // Devuelve la cabecera de la solicitud completa del cliente al // método main de nuestro servidor private static String getPeticion( InputStream sin ) { try { byte buf[] = new byte[buffer]; boolean esCR = false; int pos = 0; int c; while( ( c = sin.read() ) != -1 ) { switch( c ) { case '\r': break; case '\n': if( esCR ) return( new String( buf,0,0,pos ) ); esCR = true; // Continúa, se ha puesto el primer \n en la cadena default: if( c != '\n' ) esCR = false; buf[pos++] = (byte)c; } } } catch( IOException e ) { HTTPlog.error( "Error de Recepcion" ); } return( null ); } private static int tipoPeticion( String peticion ) {

return( peticion.regionMatches( true,0,"get ",0,4 ) ? RT_GET : RT_UNSUP ); } // Función principal de nuestro servidor, que se conecta al socket // y se embucla indefinidamente public static void main( String args[] ) throws Exception { ServerSocket ss = new ServerSocket( puerto ); while( true ) { String peticion; Socket s = ss.accept(); OutputStream sOut = s.getOutputStream(); InputStream sIn = s.getInputStream();

}

}

if( ( peticion = getPeticion( sIn ) ) != null ) { switch( tipoPeticion( peticion ) ) { case RT_GET: ctrlGet( peticion,sOut ); break; case RT_UNSUP: default: ctrlNoSop( peticion,sOut ); break; } HTTPlog.peticion( peticion ); } sIn.close(); sOut.close(); s.close(); }

CLASES UTILES EN COMUNICACIONES Vamos a exponer otras clases que resultan útiles cuando estamos desarrollando programas de comunicaciones, aparte de las que ya se han visto. El problema es que la mayoría de estas clases se prestan a discusión, porque se encuentran bajo el directorio sun. Esto quiere decir que son implementaciones Solaris y, por tanto, específicas del Unix Solaris. Además su API no está garantizada, pudiendo cambiar. Pero, a pesar de todo, resultan muy interesantes y vamos a comentar un grupo de ellas solamente que se encuentran en el paquete sun.net Socket

Es el objeto básico en toda comunicación a través de Internet, bajo el protocolo TCP. Esta clase proporciona métodos para la entrada/salida a través de streams que hacen la lectura y escritura a través de sockets muy sencilla. ServerSocket

Es un objeto utilizado en las aplicaciones servidor para escuchar las peticiones que realicen los clientes conectados a ese servidor. Este objeto no realiza el servicio, sino que crea un objeto Socket en función del cliente para realizar toda la comunicación a través de él.

DatagramSocket

La clase de sockets datagrama puede ser utilizada para implementar datagramas no fiables (sockets UDP), no ordenados. Aunque la comunicación por estos sockets es muy rápida porque no hay que perder tiempo estableciendo la conexión entre cliente y servidor. DatagramPacket

Clase que representa un paquete datagrama conteniendo información de paquete, longitud de paquete, direcciones Internet y números de puerto. MulticastSocket

Clase utilizada para crear una versión multicast de las clase socket datagrama. Múltiples clientes/servidores pueden transmitir a un grupo multicast (un grupo de direcciones IP compartiendo el mismo número de puerto). NetworkServer

Una clase creada para implementar métodos y variables utilizadas en la creación de un servidor TCP/IP. NetworkClient

Una clase creada para implementar métodos y variables utilizadas en la creación de un cliente TCP/IP. SocketImpl

Es un Interface que nos permite crearnos nuestro propio modelo de comunicación. Tendremos que implementar sus métodos cuando la usemos. Si vamos a desarrollar una aplicación con requerimientos especiales de comunicaciones, como pueden se la implementación de un cortafuegos (TCP es un protocolo no seguro), o acceder a equipos especiales (como un lector de código de barras o un GPS diferencial), necesitaremos nuestra propia clase Socket. Vamos a ver un ejemplo de utilización, presentando un sencillo ejemplo, servidorUDP.java, de implementación de sockets UDP utilizando la clase DatagramSocket. import java.net.*; import java.io.*; import sun.net.*; // Implementación del servidor de datagramas UDP. Envía una cadena // tras petición // class servidorUDP { public static void main( String args[] ) { DatagramSocket s = (DatagramSocket)null; DatagramPacket enviap,recibep; byte ibuffer[] = new byte[100]; String cadena = "Hola Tutorial de Java!\n"; InetAddress IP = (InetAddress)null; int longitud = sendString.length(); int puertoEnvio = 4321; int puertoRecep = 4322; int puertoRemoto;

// Intentamos conseguir la dirección IP del host try { IP = InetAddress.getByName( "bregogan" ); } catch( UnknownHostException e ) { System.out.println( "No encuentro al host breogan" ); System.exit( -1 ); } // Establecemos el servidor para escuchar en el socket 4322 try { s = new DatagramSocket( puertoRecep ); } catch( SocketException e ) { System.out.println( "Error - "+e.toString() ); } // Creamos un paquete de solicitud en el cliente // y nos quedamos esperando a sus peticiones recibep = new DatagramPacket( ibuffer,longitud ); try { s.receive( recibep ); } catch( IOException e ) { System.out.println( "Error - "+e.toString() ); } // Creamos un paquete para enviar al cliente y lo enviamos sendString.getBytes( 0,longitud,ibuffer,0 ); enviap = new DatagramPacket( ibuffer,longitud,IP,puertoEnvio ); try { s.send( enviap ); } catch( IOException e ) { System.out.println( "Error - "+e.toString() ); System.exit( -1 ); }

}

// Cerramos el socket s.close(); }

Y también vamos a implementar el cliente, clienteUDP.java, del socket UDP correspondiente al servidor que acabamos de presentar: import java.net.*; import java.io.*; import sun.net.*; // Implementación del cliente de datagramas UDP. Devuelve la salida // de los servidores // class clienteUDP { public static void main( String args[] ) { int longitud = 100; DatagramSocket s = (DatagramSocket)null; DatagramPacket enviap,recibep; byte ibuffer[] = new byte[100]; InetAddress IP = (InetAddress)null; int puertoEnvio = 4321; int puertoRecep = 4322; // Abre una conexión y establece el cliente para recibir // una petición en el socket 4321 try { s = new DatagramSocket( puertoRecep ); } catch( SocketException e ) {

System.out.println( "Error - "+e.toString() ); } // Crea una petición para enviar bytes. Intenta conseguir // la dirección IP del host try { IP = InetAddress.getByName( "depserver" ); } catch( UnknownHostException e ) { System.out.println( "No encuentro el host depserver" ); System.exit( -1 ); } // Envía una petición para que responda el servidor try { enviap = new DatagramPacket( ibuffer,ibuffer.length, IP,4322 ); s.send( enviap ); } catch( IOException e ) { System.out.println( "Error - "+e.toString() ); } // Consigue un controlador de fichero de entrada del socket y lee // dicha entrada. Creamos un paquete descriptor para recibir el // paquete UDP recibep = new DatagramPacket( ibuffer,longitud ); // Espera a recibir un paquete try { s.receive( recibep ); } catch( IOException e ) { System.out.println( "Error - "+e.toString() ); System.exit( -1 ); } // Imprimimos los resultados de lo que conseguimos System.out.println( "Recibido: "+recibep.getLength()+" bytes" ); String datos = new String( recibep.getData(),0 ); System.out.println( "Datos: "+datos ); System.out.println( "Recibido por puerto: "+recibep.getPort() );

}

// Cerramos la conexión y abandonamos s.close(); }

La salida que se producirá cuando ejecutemos primero el servidor y luego el cliente será la misma que reproducimos a continuación: %java clienteUDP Recibido: 17 bytes Datos: Hola Tutorial de Java! Recibido por puerto: 4322

MVC en JAVA 1. La Arquitectura MVC ● Definición de las partes 2. Observador y Observable ● Funciones de Observer y Observable 1. Observer 2. Observable ● Cómo utilizar Observer y Observable 1. Extender un Observable 2. Implementar un Observador 3. Usando Observador y Observable 3. Ejemplo de aplicación MVC Vamos a presentar, por curiosidad más que por otra cosa, una introducción a la interface Observer y a la clase Observable que proporciona Java. Vamos a implementar programas basados en la arquitectura Modelo/Vista/Controlador, popularizada por el lenguaje Smalltalk. Presentamos el problema: queremos diseñar un programa que visualice los datos de una escena en tres dimensiones y lo pase a dos dimensiones. El programa debe ser modular y permitir vistas múltiples y simultáneas de la misma escena. Cada vista debe ser capaz de presentar la escena desde diferentes puntos de vista y distintas condiciones de iluminación. Y, más importante todavía, si alguna porción de una escena cambia, las vistas deben actualizarse automáticamente. Otra versión del problema sería el de una hoja de cálculo, en donde tenemos una serie de datos que queremos ver representados gráficamente, para ello dispondremos de varias vistas con gráficos de línea, de barra o de tarta, y deben actualizarse automáticamente según vayan cambiando los datos que figuran en la hoja de cálculo. Ninguno de los requerimientos anteriores presenta una carga de programación imposible. Si el código que controla cada uno de los requerimientos ha de ser escrito de nuevo, sin embargo, si que supondría un esfuerzo significativo. Afortunadamente, el soporte para estas tareas ya esta proporcionado por la librería de Java, a través del interface Observer y la clase Observable. Las funcionalidades de las dos se han inspirado, en parte, en la arquitectura Modelo/Vista/Controlador.

ARQUITECTURA Modelo/Vista/Controlador La arquitectura MVC (Model/View/Controller) fue introducida como parte de la versión Smalltalk-80 del lenguaje de programación Smalltalk. Fue diseñada para reducir el esfuerzo de programación necesario en la implementación de sistemas múltiples y sincronizados de los mismos datos. Sus características principales son que el Modelo, las Vistas y los Controladores se tratan como entidades separadas; esto hace que cualquier cambio producido en el Modelo se refleje automáticamente en cada una de las Vistas. Además del programa ejemplo que hemos presentado al principio y que posteriormente implementaremos, este modelo de arquitectura se puede emplear en sistemas de representación gráfica de datos, como se ha citado, o en sistemas CAD, en donde se presentan partes del diseño con diferente escala de aumento, en ventanas separadas. En la figura siguiente, vemos la arquitectura MVC en su forma más general. Hay un Modelo, múltiples Controladores que manipulan ese Modelo, y hay varias Vistas de los datos del Modelo, que cambian cuando cambia el estado de ese Modelo.

Este modelo de arquitectura presenta varias ventajas: ● ● ●

Hay una clara separación entre los componentes de un programa; lo cual nos permite implementarlos por separado Hay un API muy bien definido; cualquiera que use el API, podrá reemplazar el Modelo, la Vista o el Controlador, sin aparente dificultad. La conexión entre el Modelo y sus Vistas es dinámica; se produce en tiempo de ejecución, no en tiempo de compilación.

Al incorporar el modelo de arquitectura MVC a un diseño, las piezas de un programa se pueden construir por separado y luego unirlas en tiempo de ejecución. Si uno de los Componentes, posteriormente, se observa que funciona mal, puede reemplazarse sin que las otras piezas se vean afectadas. Este escenario contrasta con la aproximación monolítica típica de muchos programas Java. Todos tienen un Frame que contiene todos los elementos, un controlador de eventos, un montón de cálculos y la presentación del resultado. Ante esta perspectiva, hacer un cambio aquí no es nada trivial.

Definición de las partes El Modelo es el objeto que representa los datos del programa. Maneja los datos y controla todas sus transformaciones. El Modelo no tiene conocimiento específico de los Controladores o de las Vistas, ni siquiera contiene referencias a ellos. Es el propio sistema el que tiene encomendada la responsabilidad de mantener enlaces entre el Modelo y sus Vistas, y notificar a las Vistas cuando cambia el Modelo. La Vista es el objeto que maneja la presentación visual de los datos representados por el Modelo. Genera una representación visual del Modelo y muestra los datos al usuario. Interactúa con el Modelo a través de una referencia al propio Modelo. El Controlador es el objeto que proporciona significado a las ordenes del usuario, actuando sobre los datos representados por el Modelo. Cuando se realiza algún cambio, entra en acción, bien sea por cambios en la información del Modelo o por alteraciones de la Vista. Interactúa con el Modelo a través de una referencia al propio Modelo. Vamos a mostrar un ejemplo concreto. Consideremos como tal el sistema descrito en la introducción a este capítulo, una pieza geométrica en tres dimensiones, que representamos en la figura siguiente:

En este caso, la pieza central de la escena en tres dimensiones es el Modelo. El Modelo es una descripción matemática de los vértices y las caras que componen la escena. Los datos que describen cada vértice o cara pueden modificarse (quizás como resultado de una acción del usuario, o una distorsión de la escena, o un algoritmo de sombreado). Sin embargo, no tiene noción del punto de vista, método de presentación, perspectiva o fuente de luz. El Modelo es una representación pura de los elementos que componen la escena.

La porción del programa que transforma los datos dentro del Modelo en una presentación gráfica es la Vista. La Vista incorpora la visión del Modelo a la escena; es la representación gráfica de la escena desde un punto de vista determinado, bajo condiciones de iluminación determinadas. El Controlador sabe que puede hacer el Modelo e implementa el interface de usuario que permite iniciar la acción. En este ejemplo, un panel de datos de entrada es lo único que se necesita, para permitir añadir, modificar o borrar vértices o caras de la figura.

OBSERVADOR Y OBSERVABLE El lenguaje de programación Java proporciona soporte para la arquitectura MVC mediante dos clases: ● ●

Observer: Es cualquier objeto que desee ser notificado cuando el estado de otro objeto sea alterado Observable: Es cualquier objeto cuyo estado puede representar interés y sobre el cual otro objeto ha demostrado ese interés

Estas dos clases se pueden utilizar para muchas más cosas que la implementación de la arquitectura MVC. Serán útiles en cualquier sistema en que se necesite que algunos objetos sean notificados cuando ocurran cambios en otros objetos. El Modelo es un subtipo de Observable y la Vista es un subtipo de Observer. Estas dos clases manejan adecuadamente la función de notificación de cambios que necesita la arquitectura MVC. Proporcionan el mecanismo por el cual las Vistas pueden ser notificadas automáticamente de los cambios producidos en el Modelo. Referencias al objeto Modelo tanto en el Controlador como en la Vista permiten acceder a los datos de ese objeto Modelo.

Funciones Observer y Observable Vamos a enumerar las funciones que intervienen en el control de Observador y Observable: Observer public void update( Observableobs,Object obj )

Llamada cuando se produce un cambio en el estado del objeto Observable Observable public void addObserver( Observer obs )

Añade un observador a la lista interna de observadores public void deleteObserver( Observer obs )

Borra un observador de la lista interna de observadores public void deleteObservers()

Borra todos los observadores de la lista interna public int countObserver()

Devuelve el número de observadores en la lista interna protected void setChanged()

Levanta el flag interno que indica que el Observable ha cambiado de estado protected void clearChanged()

Baja el flag interno que indica que el Observable ha cambiado de estado protected boolean hasChanged()

Devuelve un valor booleano indicando si el Observable ha cambiado de estado public void notifyObservers()

Comprueba el flag interno para ver si el Observable ha cambiado de estado y lo notifica a todos los observadores public void notifyObservers( Object obj )

Comprueba el flag interno para ver si el Observable ha cambiado de estado y lo notifica a todos los observadores. Les pasa el objeto especificado en la llamada para que lo usen los observadores en su método notify().

UTILIZAR OBSERVADOR Y OBSERVABLE Vamos a describir en los siguientes apartados, como crear una nueva clase Observable y una nueva clase Observer y como utilizar las dos conjuntamente.

Extender un Observable Una nueva clase de objetos observables se crea extendiendo la clase Observable. Como la clase Observable ya implementa todos los métodos necesarios para proporcionar el funcionamiento de tipo Observador/Observable, la clase derivada solamente necesita proporcionar algún tipo de mecanismo que lo ajuste a su funcionamiento particular y proporcionar acceso al estado interno del objeto Observable. En la clase ValorObservable que mostramos a continuación, el estado interno del Modelo es capturado en el entero n. A este valor se accede (y más importante todavía, se modifica) solamente a través de sus métodos públicos. Si el valor cambia, el objeto invoca a su propio método setChanged() para indicar que el estado del Modelo ha cambiado. Luego, invoca a su propio método notifyObservers() para actualizar a todos los observadores registrados. import java.util.Observable; public class ValorObservable extends Observable { private int nValor = 0; // Constructor al que indicamos el valor en que comenzamos y los // limites inferior y superior que no deben sobrepasarse public ValorObservable( int nValor,int nInferior,int nSuperior ) { this.nValor = nValor; } // Fija el valor que le pasamos y notifica a los observadores que // estan pendientes del cambio de estado de los objetos de esta // clase, que su etado se ha visto alterado public void setValor(int nValor) { this.nValor = nValor;

setChanged(); notifyObservers(); } // Devuelve el valor actual que tiene el objeto public int getValor() { return( nValor ); } }

Implementar un Observador Una nueva clase de objetos que observe los cambios en el estado de otro objeto se puede crear implementando la interface Observer. Esta interface necesita un método update() que se debe proporcionar en la nueva clase. Este método será llamado siempre que el Observable cambie de estado, que anuncia este cambio llamando a su método notifyObservers(). El observador entonces, debería interrogar al objeto Observable para determinar su nuevo estado; y, en el caso de la arquitectura MVC, ajustar su Vista adecuadamente. En la clase ObservadorDeTexto, que muestra el código siguiente, el método notify() primero realiza una comprobación para asegurarse de que el Observable que ha anunciado un cambio es el Observable que él esta observando. Si lo es, entonces lee su estado e imprime el nuevo valor. import java.util.Observer; import java.util.Observable; public class TextoObservador extends Frame implements Observer { private ValorObservable vo = null; public TextoObservador( ValorObservable vo ) { this.vo = vo; } public void update( Observable obs,Object obj ) { if( obs == vo ) tf.setText( String.valueOf( vo.getValor() ) ); } }

Usando Observador y Observable Un programa indica a un objeto Observable que hay un objeto observador que debe ser notificado cuando se produzca un cambio en su estado, llamando al método addObserver() del objeto Observable. Este método añade el Observador a la lista de observadores que el objeto Observable ha de notificar cuando su estado se altere. En el ejemplo siguiente, en donde mostramos la clase ControlValor, ControlValor.java, vemos como se usa el método addObserver() para añadir una instancia de la clase TextoObservador a la lista que mantiene la clase ValorObservable. public class ControlValor { // Constructor de la clase que nos permite crear los objetos de // observador y observable public ControlValor() { ValorObservable vo = new ValorObservable( 100,0,500 ); TextoObservador to = new TextoObservador( vo ); vo.addObserver( to ); }

public static void main( String args[] ) { ControlValor m = new ControlValor(); } }

En la siguiente secuencia, vamos a describir como se realiza la interacción entre un Observador y un objeto Observable, durante la ejecución de un programa: 1. En primer lugar el usuario manipula un elemento del interface de usuario representado por el Controlador. Este Controlador realiza un cambio en el Modelo a través de uno de sus métodos públicos de acceso; en nuestro caso, llama a setValue(). 2. El método público de acceso modifica el dato privado, ajusta el estado interno del Modelo y llama al método setChanged() para indicar que su estado ha cambiado. Luego llama al método notifyObservers() para notificar a los observadores que su estado no es el mismo. La llamada a este método puede realizarse en cualquier lugar, incluso desde un bucle de actualización que se esté ejecutando en otro thread. Se llama a los métodos update() de cada Observador, indicando que hay un cambio en el estado del objeto que estaban observando. El Observador accede entonces a los datos del Modelo a través del método público del Observable y actualiza las Vistas.

ACCESO A BASES DE DATOS EN JAVA JDBC JDBC (Java DataBase Connectivity) es un API que permite lanzar queries a una base de datos relacional. Su diseño está inspirado en dos conocidas APIs ODBC (Open DataBase Connectivity) X/OPEN SQL CLI (Call Level Interface)

Establecer una Conexión

Lo primero que tenemos que hacer es establecer una conexión con el controlador de base de datos que queremos utilizar. Esto implica dos pasos: (1) cargar el driver y (2) hacer la conexión. Cargar los Drivers Cargar el driver o drivers que queremos utilizar es muy sencillo y sólo implica una línea de código. Si, por ejemplo, queremos utilizar el puente JDBC-ODBC, se cargaría la siguiente línea de código. Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

La documentación del driver nos dará el nombre de la clase a utilizar. Por ejemplo, si el nombre de la clase es jdbc.DriverXYZ, cargaríamos el driver con esta línea de código. Class.forName("jdbc.DriverXYZ");

No necesitamos crear un ejemplar de un driver y registrarlo con el DriverManager porque la llamada a Class.forName lo hace automáticamente. Si hubiéramos creado nuestro propio ejemplar, creariamos un duplicado innecesario, pero no pasaría nada. Una vez cargado el driver, es posible hacer una conexión con un controlador de base de datos. Hacer la Conexión El segundo paso para establecer una conexión es tener el driver apropiado conectado al controlador de base de datos. La siguiente línea de código ilustra la idea general. Connection con = DriverManager.getConnection(url, "myLogin", "myPassword");

Este paso también es sencillo, lo más duro es saber qué suministrar para url. Si estamos utilizando el puente JDBC-ODBC, el JDBC URL empezará con jdbc:odbc:. el resto de la URL normalmente es la fuente de nuestros datos o el sistema de base de datos. Por eso, si estamos utilizando ODBC para acceder a una fuente de datos ODBC llamada "Fred," por ejemplo, nuestro URL podría ser jdbc:odbc:Fred. En lugar de "myLogin" pondríamos el nombre utilizado para entrar en el controlador de la base de datos; en lugar de "myPassword" pondríamos nuestra password para el controlador de la base de datos. Por eso si entramos en el controlador con el nombre "Fernando" y la password of "J8," estas dos líneas de código estableceran una conexión. String url = "jdbc:odbc:Fred"; Connection con = DriverManager.getConnection(url, "Fernando", "J8");

Si estamos utilizando un puente JDBC desarrollado por una tercera parte, la documentación nos dirá el subprotocolo a utilizar, es decir, qué poner despues de jdbc: en la URL. Por ejemplo, si el desarrollador ha registrado el nombre "acme" como el subprotocolo, la primera y segunda parte de la URL de JDBC serán jdbc:acme:. La documentación del driver también nos dará las guías para el resto de la URL del JDBC. Esta última parte de la URL suministra información para la identificación

de los datos fuente. Si uno de los drivers que hemos cargado reconoce la URL suministada por el método DriverManager.getConnection, dicho driver establecerá una conexión con el controlador de base de datos especificado en la URL del JDBC. La clase DriverManager, como su nombre indica, maneja todos los detalles del establecimiento de la conexión detrás de la escena. A menos que estemos escribiendo un driver, posiblemente nunca utilizaremos ningún método del interface Driver, y el único método de DriverManager que realmente necesitaremos conocer es DriverManager.getConnection. La conexión devuelta por el método DriverManager.getConnection es una conexión abierta que se puede utilizar para crear sentencias JDBC que pasen nuestras sentencias SQL al controlador de la base de datos. En el ejemplo anterior, con es una conexión abierta, y se utilizará en los ejemplos posteriores. Seleccionar una Tabla Primero, crearemos una de las tablas de nuestro ejemplo. Esta tabla, COFFEES, contiene la información esencial sobre los cafés vendidos en "The Coffee Break", incluyendo los nombres de los cafés, sus precios, el número de libras vendidas la semana actual, y el número de libras vendidas hasta la fecha. Aquí puedes ver la tabla COFFEES, que describiremos más adelante. COF_NAME SUP_ID PRICE SALES TOTAL Colombian 101 7.99 0 0 French_Roast 49 8.99 0 0 Espresso 150 9.99 0 0 Colombian_Decaf 101 8.99 0 0 French_Roast_Decaf 49 9.99 0 0 La columna que almacena el nombre del café es COF_NAME, y contiene valores con el tipo VARCHAR de SQL y una longitud máxima de 32 caracteres. Como utilizamos nombres diferentes para cada tipo de café vendido, el nombre será un único identificador para un café particular y por lo tanto puede servir como clave primaria. La segunda colunma, llamada SUP_ID, contiene un número que identifica al suministrador del café; este número será un tipo INTEGER de SQL. La tercera columna, llamada PRICE, almacena valores del tipo FLOAT de SQL porque necesita contener valores decimales. (Observa que el dinero normalmente se almacena en un tipo DECIMAL o NUMERIC de SQL, pero debido a las diferencias entre controladores de bases de datos y para evitar la incompatibilidad con viejas versiones de JDBC, utilizamos el tipo más estándard FLOAT.) La columna llamada SALES almacena valores del tipo INTEGER de SQL e indica el número de libras vendidas durante la semana actual. La columna final, TOTAL,contiene otro valor INTEGER de SQL que contiene el número total de libras vendidas hasta la fecha. SUPPLIERS, la segunda tabla de nuesta base de datos, tiene información sobre cada uno de los suministradores. SUP_ID SUP_NAME STREET CITY STATE ZIP 101 Acme, Inc. 99 Market Street Groundsville CA 95199 49 Superior Coffee 1 Party Place Mendocino CA 95460 150 The High Ground 100 Coffee Lane Meadows CA 93966 Las tablas COFFEES y SUPPLIERS contienen la columna SUP_ID, lo que significa que estas dos tablas pueden utilizarse en sentencias SELECT para obtener datos basados en la información de ambas tablas. La columna SUP_ID es la clave primaria de la tabla SUPPLIERS, y por lo tanto, es un identificador único para cada uno de los suministradores de café. En la tabla COFFEES, SUP_ID es llamada clave extranjera. (Se puede pensar en una clave extranjera en el sentido en que es importada desde otra tabla). Observa que cada número SUP_ID aparece sólo una vez en la tabla SUPPLIERS; esto es necesario para ser una clave primaria. Sin embargo, en la tabla COFFEES, donde es una clave extranjera, es perfectamente correcto que haya números duplicados de SUP_ID porque un

suministrador puede vender varios tipos de café. Más adelante en este capítulo podremos ver cómo utilizar claves primarias y extranjeras en una sentencia SELECT. La siguiente sentencia SQL crea la tabla COFFEES. Las entradas dentro de los paréntesis exteriores consisten en el nombre de una columna seguido por un espacio y el tipo SQL que se va a almacenar en esa columna. Una coma separa la entrada de una columna (que consiste en el nombre de la columna y el tipo SQL) de otra. El tipo VARCHAR se crea con una longitud máxima, por eso toma un parámetro que indica la longitud máxima. El parámetro debe estar entre paréntesis siguiendo al tipo. La sentencia SQL mostrada aquí, por ejemplo, específica que los nombres de la columna COFNAME pueden tener hasta 32 caracteres de longitud. CREATE TABLE COFFEES (COF_NAME VARCHAR(32), SUP_ID INTEGER, PRICE FLOAT, SALES INTEGER, TOTAL INTEGER)

Este código no termina con un terminador de sentecia de un controlador de base de datos, que puede variar de un controlador a otro. Por ejemplo, Oracle utiliza un punto y coma (;) para finalizar una sentencia, y Sybase utiliza la plabra go. El driver que estamos utilizado proporcionará automáticamente el terminador de setencia apropiado, y no necesitaremos introducirlo en nuestro código JDBC. Otra cosa que debíamos apuntar sobre las sentencias SQL es su forma. En la sentencia CREATE TABLE, las palabras clave se han imprimido en letras máyusculas, y cada ítem en una línea separada. SQL no requiere nada de esto, estas convenciones son sólo para una fácil lectura. El estándard SQL dice que la palabras claves no son sensibles a las mayúsculas, así, por ejemplo, la anterior sentencia SELECT pude escribirse de varias formas. Y como ejemplo, estas dos versiones son equivalentes en lo que concierne a SQL. SELECT First_Name, Last_Name FROM Employees WHERE Last_Name LIKE "Washington" select First_Name, Last_Name from Employees where Last_Name like "Washington"

Sin embargo, el material entre comillas si es sensible a las maýusculas: en el nombre "Washington", "W" debe estar en maýuscula y el resto de las letras en minúscula. Los requerimientos pueden variar de un controlador de base de datos a otro cuando se trada de nombres de identificadores. Por ejemplo, algunos controladores, requieren que los nombres de columna y de tabla seán exactamente los mismos que se crearon en las sentencias CREATE y TABLE, mientras que otros controladores no lo necesitan. Para asegurarnos, utilizaremos mayúsculas para identificadores como COFFEES y SUPPLIERS porque así es como los definimos. Hasta ahora hemos escrito la sentencia SQL que crea la tabla COFFEES. Ahora le pondremos comillas (crearemos un string) y asignaremos el string a la variable createTableCoffees para poder utilizarla en nuestro código JDBC más adelante. Como hemos visto, al controlador de base de datos no le importa si las líneas están divididas, pero en el lenguaje Java, un objeto String que se extienda más allá de una línea no será compilado. Consecuentemente, cuando estamos entregando cadenas, necesitamos encerrar cada línea entre comillas y utilizar el signo más (+) para concatenarlas. String createTableCoffees = "CREATE TABLE COFFEES " + "(COF_NAME VARCHAR(32), SUP_ID INTEGER, PRICE FLOAT, " + "SALES INTEGER, TOTAL INTEGER)";

Los tipos de datos que hemos utilizado en nuestras sentencias CREATE y TABLE son tipos genéricos SQL (también llamados tipos JDBC) que están definidos en la clase java.sql.Types. Los controladores de bases de datos generalmente utilizan estos tipos estándards, por eso cuando llegue el

momento de probar alguna aplicación, sólo podremos utilizar la aplicación CreateCoffees.java, que utiliza las sentencias CREATE y TABLE. Si tu controlador utiliza sus propios nombres de tipos, te suministrarermos más adelante una aplicación que hace eso. Sin embargo, antes de ejecutar alguna aplicación, veremos lo más básico sobre el JDBC. Crear sentencias JDBC Un objeto Statement es el que envía nuestras sentencias SQL al controlador de la base de datos. Simplemente creamos un objeto Statement y lo ejecutamos, suministando el método SQL apropiado con la sentencia SQL que queremos enviar. Para una sentencia SELECT, el método a ejecutar es executeQuery. Para sentencias que crean o modifican tablas, el método a utilizar es executeUpdate. Se toma un ejemplar de una conexión activa para crear un objeto Statement. En el siguiente ejemplo, utilizamos nuestro objeto Connection: con para crear el objeto Statement: stmt. Statement stmt = con.createStatement();

En este momento stmt existe, pero no tiene ninguna sentencia SQL que pasarle al controlador de la base de datos. Necesitamos suministrarle el metodo que utilizaremos para ejecutar stmt. Por ejemplo, en el siguiente fragmento de código, suministramos executeUpdate con la sentencia SQL del ejemplo anterior. stmt.executeUpdate("CREATE TABLE COFFEES " + "(COF_NAME VARCHAR(32), SUP_ID INTEGER, PRICE FLOAT, " + "SALES INTEGER, TOTAL INTEGER)");

Coma ya habíamos creado un String con la sentencia SQL y lo habíamos llamado createTableCoffees, podríamos haber escrito el código de esta forma alternativa. stmt.executeUpdate(createTableCoffees);

Ejecutar Sentencias Utilizamos el método executeUpdate porque la sentencia SQL contenida en createTableCoffees es una sentencia DDL (data definition language). Las sentencias que crean, modifican o eliminan tablas son todas ejemplos de sentencias DDL y se ejecutan con el método executeUpdate. Cómo se podría esperar de su nombre, el método executeUpdate también se utiliza para ejecutar sentencias SQL que actualizan un tabla. En la práctica executeUpdate se utiliza más frecuentemente para actualizar tablas que para crearlas porque una tabla se crea sólo una vez, pero se puede actualizar muchas veces. El método más utilizado para ejecutar sentencias SQL es executeQuery. Este método se utiliza para ejecutar sentencias SELECT, que comprenden la amplia mayoría de las sentencias SQL. Pronto veremos como utilizar este método. Introducir Datos en una Tabla Hemos visto como crear la tabla COFFEES especificando los nombres de columnas y los tipos de datos almacenados en esas columnas, pero esto sólo configura la estructura de la tabla. La tabla no contiene datos todavía. Introduciremos datos en nuestra tabla una fila cada vez, suministrando la información a almacenar en cada columna de la fila. Observa que los valores insertados en las columnas se listan en el mismo orden en que se declararon las columnas cuando se creó la tabla, que es el orden por defecto. El siguiente código isnerta una fila de datos con Colombian en la columna COF_NAME, 101 en SUP_ID, 7.99 en PRICE, 0 en SALES, y 0 en TOTAL. (Como acabamos de inaugurar "The Coffee Break", la cantidad vendida durante la semana y la cantidad total son cero para todos los cafés). Al igual que hicimos con el código que creaba la tabla COFFEES, crearemos un objeto Statement y lo ejecutaremos utilizando el método executeUpdate.

Como la sentencia SQL es damasiado larga como para entrar en una sóla línea, la hemos dividido en dos strings concatenándolas mediante un signo más (+) para que puedan compilarse. Presta especial atención a la necesidad de un espacio entre COFFEES y VALUES. Este espacio debe estar dentro de las comillas y debe estar después de COFFEES y antes de VALUES; sin un espacio, la sentencia SQL sería leída erróneamente como "INSERT INTO COFFEESVALUES . . ." y el controlador de la base de datos buscaría la tablaCOFFEESVALUES. Observa también que utilizamos comilla simples alrededor del nombre del café porque está anidado dentro de las comillas dobles. Para la mayoría de controladores de bases de datos, la regla general es alternar comillas dobles y simples para indicar anidación. Statement stmt = con.createStatement(); stmt.executeUpdate( "INSERT INTO COFFEES " + "VALUES ('Colombian', 101, 7.99, 0, 0)");

El siguiente código inserta una segunda línea dentro de la tabla COFFEES. Observa que hemos reutilizado el objeto Statement: stmt en vez de tener que crear uno nuevo para cada ejecución. stmt.executeUpdate("INSERT INTO COFFEES " + "VALUES ('French_Roast', 49, 8.99, 0, 0)");

Los valores de las siguientes filas se pueden insertar de esta forma. stmt.executeUpdate("INSERT INTO COFFEES " + "VALUES ('Espresso', 150, 9.99, 0, 0)"); stmt.executeUpdate("INSERT INTO COFFEES " + "VALUES ('Colombian_Decaf', 101, 8.99, 0, 0)"); stmt.executeUpdate("INSERT INTO COFFEES " + "VALUES ('French_Roast_Decaf', 49, 9.99, 0, 0)");

Obtener Datos desde una Tabla Ahora que la tablaCOFFEES tiene valores, podemos escribir una sentencia SELECT para acceder a dichos valores. El asterisco (*) en la siguiente sentencia SQL indica que la columna debería ser seleccionada. Como no hay claúsula WHERE que limite las columas a seleccionar, la siguiente sentencia SQL seleciona la tabla completa. SELECT * FROM COFFEES

El resultado, que es la tabla completa, se parecería a esto. COF_NAME SUP_ID PRICE SALES TOTAL Colombian 101 7.99 0 0 French_Roast 49 8.99 0 0 Espresso 150 9.99 0 0 Colombian_Decaf 101 8.99 0 0 French_Roast_Decaf 49 9.99 0 0 El resultado anterior es lo que veríamos en nuestro terminal si introdujeramos la petición SQL directamente en el sistema de la base de datos. Cuando accedemos a una base de datos a través de una aplicación Java, como veremos pronto, necesitamos recuperar los resultados para poder utilizarlos. Veremos como hacer esto en la siguiente página. Aquí tenemos otro ejemplo de una sentencia SELECT, ésta obtiene una lista de cáfes y sus respectivos precios por libra. SELECT COF_NAME, PRICE FROM COFFEES

El resultado de esta consulta se parecería a esto.

----------------PRICE -Colombian 7.99 French_Roast 8.99 Espresso 9.99 Colombian_Decaf 8.99 French_Roast_Decaf 9.99 La sentencia SELECT genera los nombres y precios de todos los cáfes de la tabla. La siguiente sentencia SQL límita los cafés seleccionados a aquellos que cuesten menos de $9.00 por libra. COF_NAME

SELECT COF_NAME, PRICE FROM COFFEES WHERE PRICE < 9.00

El resultado se parecería es esto. -----------------PRICE -Colombian 7.99 French_Roast 8.99 Colombian Decaf 8.99 Recuperar Valores desde una Hoja de Resultados Ahora veremos como enviar la sentencia SELECT de la página anterior desde un programa escrito en Java y como obtener los resultados que hemos mostrado. COF_NAME

JDBC devuelve los resultados en un objeto ResultSet, por eso necesitamos declarar un ejemplar de la clase ResultSet para contener los resultados. El siguiente código presenta el objeto ResultSet: rs y le asigna el resultado de una consulta anterior. ResultSet rs = stmt.executeQuery("SELECT COF_NAME, PRICE FROM COFFEES");

Utilizar el Método next La variable rs, que es un ejemplar de ResultSet, contiene las filas de cafés y sus precios mostrados en el juego de resultados de la página anterior. Para acceder a los nombres y los precios, iremos a la fila y recuperaremos los valores de acuerdo con sus tipos. El método next mueve algo llamado cursor a la siguiente fila y hace que esa fila (llamada fila actual) sea con la que podamos operar. Como el cursor inicialmente se posiciona justo encima de la primera fila de un objeto ResultSet, primero debemos llamar al método next para mover el cursor a la primera fila y convertirla en la fila actual. Sucesivas invocaciones del método next moverán el cursor de línea en línea de arriba a abajo. Observa que con el JDBC 2.0, cubierto en la siguiente sección, se puede mover el cursor hacia atrás, hacia posiciones específicas y a posiciones relativas a la fila actual además de mover el cursor hacia adelante. Utilizar los métodos getXXX Los métodos getXXX del tipo apropiado se utilizan para recuperar el valor de cada columna. Por ejemplo, la primera columna de cada fila de rs es COF_NAME, que almacena un valor del tipo VARCHAR de SQL. El método para recuperar un valor VARCHAR es getString. La segunda columna de cada fila almacena un valor del tipo FLOAT de SQL, y el método para recuperar valores de ese tipo es getFloat. El siguiente código accede a los valores almacenados en la fila actual de rs e imprime una línea con el nombre seguido por tres espacios y el precio. Cada vez que se llama al método next, la siguiente fila se convierte en la actual, y el bucle continúa hasta que no haya más filas en rs. String query = "SELECT COF_NAME, PRICE FROM COFFEES"; ResultSet rs = stmt.executeQuery(query);

}

while (rs.next()) { String s = rs.getString("COF_NAME"); Float n = rs.getFloat("PRICE"); System.out.println(s + " " + n);

La salida se parecerá a esto. Colombian 7.99 French_Roast 8.99 Espresso 9.99 Colombian_Decaf 8.99 French_Roast_Decaf 9.99

Veamos cómo funcionan los métodos getXXX examinando las dos sentencias getXXX de este código. Primero examinaremos getString. String s = rs.getString("COF_NAME");

El método getString es invocado sobre el objeto ResultSet: rs, por eso getString recuperará (obtendrá) el valor almacenado en la columna COF_NAME de la fila actual de rs. El valor recuperado por getString se ha convertido desde un VARCHAR de SQL a un String de Java y se ha asignado al objeto String s. Observa que utilizamos la variable s en la expresión println mostrada arriba, de esta forma: println(s + " " + n) La situación es similar con el método getFloat excepto en que recupera el valor almacenado en la columna PRICE, que es un FLOAT de SQL, y lo convierte a un float de Java antes de asignarlo a la variable n. JDBC ofrece dos formas para identificar la columna de la que un método getXXX obtiene un valor. Una forma es dar el nombre de la columna, como se ha hecho arriba. La segunda forma es dar el índice de la columna (el número de columna), con un 1 significando la primera columna, un 2 para la segunda, etc. Si utilizáramos el número de columna en vez del nombre de columna el código anterior se podría parecer a esto. String s = rs.getString(1); float n = rs.getFloat(2);

La primera línea de código obtiene el valor de la primera columna de la fila actual de rs (columna COF_NAME), convirtiéndolo a un objeto String de Java y asignándolo a s. La segunda línea de código obtiene el valor de la segunda columna de la fila actual de rs, lo convierte a un float de Java y lo asigna a n. Recuerda que el número de columna se refiere al número de columna en la hoja de resultados no en la tabla original. En suma, JDBC permite utilizar tanto el nombre cómo el número de la columna como argumento a un método getXXX. Utilizar el número de columna es un poco más eficiente, y hay algunos casos donde es necesario utilizarlo. JDBC permite muchas lateralidades para utilizar los métodos getXXX para obtener diferentes tipos de datos SQL. Por ejemplo, el método getInt puede ser utilizado para recuperar cualquier tipo numérico de caracteres. Los datos recuperados serán convertidos a un int; esto es, si el tipo SQL es VARCHAR, JDBC intentará convertirlo en un entero. Se recomienda utilizar el método getInt sólo para recuperar INTEGER de SQL, sin embargo, no puede utilizarse con los tipos BINARY, VARBINARY, LONGVARBINARY, DATE, TIME, o TIMESTAMP de SQL. Métodos para Recuperar Tipos SQL muestra qué métodos pueden utilizarse legalmente para recuperar tipos SQL, y más importante, qué métodos están recomendados para recuperar los distintos tipos SQL. Observa que esta tabla utiliza el término "JDBC type" en lugar de "SQL type." Ambos términos se refieren a los tipos genéricos de SQL definidos en java.sql.Types, y ambos son intercambiables.

Utilizar el método getString Aunque el metodo getString está recomendado para recuperar tipos CHAR y VARCHAR de SQL, es posible recuperar cualquier tipo básico SQL con él. (Sin embargo, no se pueden recuperar los nuevos tipos de datoas del SQL3. Explicaremos el SQL3 más adelante). Obtener un valor con getString puede ser muy útil, pero tiene sus limitaciones. Por ejemplo, si se está utilizando para recuperar un tipo numérico, getString lo convertirá en un String de Java, y el valor tendrá que ser convertido de nuevo a número antes de poder operar con él.

Actualizar Tablas Supongamos que después de una primera semana exitosa, el propietario de "The Coffee Break" quiere actualizar la columna SALES de la tabla COFFEES introduciendo el número de libras vendidas de cada tipo de café. La sentencia SQL para actualizar una columna se podría parecer a esto. String updateString = "UPDATE COFFEES " + "SET SALES = 75 " + "WHERE COF_NAME LIKE 'Colombian'";

Uitlizando el objeto stmt, este código JDBC ejecuta la sentencia SQL contenida en updateString. stmt.executeUpdate(updateString);

La tablaCOFFEES ahora se parecerá a esto. COF_NAME SUP_ID PRICE SALES TOTAL Colombian 101 7.99 75 0 French_Roast 49 8.99 0 0 Espresso 150 9.99 0 0 Colombian_Decaf 101 8.99 0 0 French_Roast_Decaf 49 9.99 0 0 Observa que todavía no hemos actualizado la columna TOTAL, y por eso tiene valor 0. Ahora seleccionaremos la fila que hemos actualizado, recuperando los valores de las columnas COF_NAME y SALES, e imprimiendo esos valores. String query = "SELECT COF_NAME, SALES FROM COFFEES " + "WHERE COF_NAME LIKE 'Colombian'"; ResultSet rs = stmt.executeQuery(query); while (rs.next()) { String s = rs.getString("COF_NAME"); int n = rs.getInt("SALES"); System.out.println(n + " pounds of " + s + " sold this week.") }

Esto imprimira lo siguiente. 75 pounds of Colombian sold this week.

Cómo la claúsula WHERE límita la selección a una sóla línea, sólo hay una línea en la ResultSet: rs y una línea en la salida. Por lo tanto, sería posible escribir el código sin un bucle while. rs.next(); String s = rs.getString(1); int n = rs.getInt(2); System.out.println(n + " pounds of " + s + " sold this week.")

Aunque hay una sóla línea en la hoja de resultados, necesitamos utilizar el método next para acceder a ella. Un objeto ResultSet se crea con un cursor apuntando por encima de la primera fila. La primera llamada al método next posiciona el cursor en la primera fila (y en este caso, la única) de rs. En este código, sólo se llama una vez a next, si sucediera que existiera una línea, nunca se accedería a ella. Ahora actualizaremos la columna TOTAL añadiendo la cantidad vendida durante la semana a la cantidad total existente, y luego imprimiremos el número de libras vendidas hasta la fecha. String updateString = "UPDATE COFFEES " + "SET TOTAL = TOTAL + 75 " + "WHERE COF_NAME LIKE 'Colombian'"; stmt.executeUpdate(updateString); String query = "SELECT COF_NAME, TOTAL FROM COFFEES " + "WHERE COF_NAME LIKE 'Colombian'"; ResultSet rs = stmt.executeQuery(query); while (rs.next()) { String s = rs.getString(1); int n = rs.getInt(2); System.out.println(n + " pounds of " + s + " sold to date.") }

Observa que en este ejemplo, utilizamos el índice de columna en vez del nombre de columna, suministrando el índice 1 a getString (la primera columna de la hoja de resultados es COF_NAME), y el índice 2 a getInt (la segunda columna de la hoja de resultados es TOTAL). Es importante distinguir entre un índice de columna en la tabla de la base de datos como opuesto al índice en la tabla de la hoja de resultados. Por ejemplo, TOTAL es la quinta columna en la tabla COFFEES pero es la segunda columna en la hoja de resultados generada por la petición del ejemplo anterior.

Utilizar Sentencias Preparadas Algunas veces es más conveniente o eficiente utilizar objetos PreparedStatement para enviar sentencias SQL a la base de datos. Este tipo especial de sentencias se deriva de una clase más general, Statement, que ya conocemos. Cuándo utilizar un Objeto PreparedStatement Si queremos ejecutar muchas veces un objeto Statement, reduciremos el tiempo de ejecución si utilizamos un objeto PreparedStatement, en su lugar. La caracterísitca principal de un objeto PreparedStatement es que, al contrario que un objeto Statement, se le entrega una sentencia SQL cuando se crea. La ventaja de esto es que en la mayoría de los casos, esta sentencia SQL se enviará al controlador de la base de datos inmediatamente, donde será compilado. Como resultado, el objeto PreparedStatement no sólo contiene una sentencia SQL, sino una sentencia SQL que ha sido precompilada. Esto significa que cuando se ejecuta la PreparedStatement, el controlador de base de datos puede ejecutarla sin tener que compilarla primero. Aunque los objetos PreparedStatement se pueden utilizar con sentencias SQL sin parámetros, probablemente nosotros utilizaremos más frecuentemente sentencias con parámetros. La ventajA de utilizar sentencias SQL que utilizan parámetros es que podemos utilizar la misma sentencia y suministrar distintos valores cada vez que la ejecutemos. Veremos un ejemplo de esto en las página siguientes. Crear un Objeto PreparedStatement Al igual que los objetos Statement, creamos un objeto PreparedStatement con un objeto Connection. Utilizando nuestra conexión con abierta en ejemplos anteriores, podríamos escribir lo siguiente para crear un objeto PreparedStatement que tome dos parámetros de entrada.

PreparedStatement updateSales = con.prepareStatement( "UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ?");

La variable updateSales contiene la sentencia SQL, "UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ?", que también ha sido, en la mayoría de los casos, enviada al controlador de la base de datos, y ha sido precompilado. Suministrar Valores para los Parámetros de un PreparedStatement Necesitamos suministrar los valores que se utilizarán en los luegares donde están las marcas de interrogación, si hay alguno, antes de ejecutar un objeto PreparedStatement. Podemos hacer esto llamado a uno de los métodos setXXX definidos en la clase PreparedStatement. Si el valor que queremos sustituir por una marca de interrogación es un int de Java, podemos llamar al método setInt. Si el valor que queremos sustituir es un String de Java, podemos llamar al método setString, etc. En general, hay un método setXXX para cada tipo Java. Utilizando el objeto updateSales del ejemplo anterior, la siguiente línea de código selecciona la primera marca de interrogación para un int de Java, con un valor de 75. updateSales.setInt(1, 75);

Cómo podríamos asumir a partir de este ejemplo, el primer argumento de un método setXXX indica la marca de interrogación que queremos seleccionar, y el segundo argumento el valor que queremos ponerle. El siguiente ejemplo selecciona la segunda marca de interrogación con el string "Colombian". updateSales.setString(2, "Colombian");

Después de que estos valores hayan sido asignados para sus dos parámetros, la sentencia SQL de updateSales será equivalente a la sentencia SQL que hay en string updateString que utilizando en el ejemplo anterior. Por lo tanto, los dos fragmentos de código siguientes consiguen la misma cosa. Código 1. String updateString = "UPDATE COFFEES SET SALES = 75 " + "WHERE COF_NAME LIKE 'Colombian'"; stmt.executeUpdate(updateString);

Código 2. PreparedStatement updateSales = con.prepareStatement( "UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ? "); updateSales.setInt(1, 75); updateSales.setString(2, "Colombian"); updateSales.executeUpdate().

Utilizamos el método executeUpdate para ejecutar ambas sentencias stmt updateSales. Observa, sin embargo, que no se suministran argumentos a executeUpdate cuando se utiliza para ejecutar updateSales. Esto es cierto porque updateSales ya contiene la sentencia SQL a ejecutar. Mirando esto ejemplos podríamos preguntarnos por qué utilizar un objeto PreparedStatement con parámetros en vez de una simple sentencia, ya que la sentencia simple implica menos pasos. Si actualizáramos la columna SALES sólo una o dos veces, no sería necesario utilizar una sentencia SQL con parámetros. Si por otro lado, tuvieramos que actualizarla frecuentemente, podría ser más fácil utilizar un objeto PreparedStatement, especialmente en situaciones cuando la utilizamos con un bucle while para seleccionar un parámetro a una sucesión de valores. Veremos este ejemplo más adelante en esta sección. Una vez que a un parámetro se ha asignado un valor, el valor permanece hasta que lo resetee otro valor o se llame al método clearParameters. Utilizando el objeto PreparedStatement: updateSales,

el siguiente fragmento de código reutiliza una sentencia prepared después de resetar el valor de uno de sus parámetros, dejando el otro igual. updateSales.setInt(1, 100); updateSales.setString(2, "French_Roast"); updateSales.executeUpdate(); // changes SALES column of French Roast row to 100 updateSales.setString(2, "Espresso"); updateSales.executeUpdate(); // changes SALES column of Espresso row to 100 (the first // parameter stayed 100, and the second parameter was reset // to "Espresso")

Utilizar una Bucle para asignar Valores Normalmente se codifica más sencillo utilizando un bucle for o while para asignar valores de los parámetros de entrada. El siguiente fragmento de código demuestra la utilización de un bucle for para asignar los parámetros en un objeto PreparedStatement: updateSales. El array salesForWeek contiene las cantidades vendidas semanalmente. Estas cantidades corresponden con los nombres de los cafés listados en el array coffees, por eso la primera cantidad de salesForWeek (175) se aplica al primer nombre de café de coffees ("Colombian"), la segunda cantidad de salesForWeek (150) se aplica al segundo nombre de café en coffees ("French_Roast"), etc. Este fragmento de código demuestra la actualización de la columna SALES para todos los cafés de la tabla COFFEES PreparedStatement updateSales; String updateString = "update COFFEES " + "set SALES = ? where COF_NAME like ?"; updateSales = con.prepareStatement(updateString);int [] salesForWeek = {175, 150, 60, 155, 90}; String [] coffees = {"Colombian", "French_Roast", "Espresso", "Colombian_Decaf", "French_Roast_Decaf"}; int len = coffees.length; for(int i = 0; i < len; i++) { updateSales.setInt(1, salesForWeek[i]); updateSales.setString(2, coffees[i]); updateSales.executeUpdate(); }

Cuando el propietario quiera actualizar las ventas de la semana siguiente, puede utilizar el mismo código como una plantilla. Todo lo que tiene que haces es introducir las nuevas cantidades en el orden apropiado en el array salesForWeek. Los nombres de cafés del array coffees permanecen constantes, por eso no necesitan cambiarse. (En una aplicación real, los valores probablemente serían introducidos por el usuario en vez de desde un array inicializado). Valores de retorno del método executeUpdate Siempre que executeQuery devuelve un objeto ResultSet que contiene los resultados de una petición al controlador de la base datos, el valor devuelto por executeUpdate es un int que indica cuántas líneas de la tabla fueron actualizadas. Por ejemplo, el siguiente código muestra el valor de retorno de executeUpdate asignado a la variable n. updateSales.setInt(1, 50); updateSales.setString(2, "Espresso"); int n = updateSales.executeUpdate(); // n = 1 because one row had a change in it

La tabla COFFEES se ha actualziado poniendo el valor 50 en la columna SALES de la fila correspondiente a Espresso. La actualización afecta sólo a una línea de la tabla, por eso n es igual a 1.

Cuando el método executeUpdate es utilizado para ejecutar una sentecia DDL, como la creación de una tabla, devuelve el int: 0. Consecuentemente, en el siguiente fragmento de código, que ejecuta la sentencia DDL utilizada pra crear la tabla COFFEES, n tendrá el valor 0. int n = executeUpdate(createTableCoffees); // n = 0

Observa que cuando el valor devuelto por executeUpdate sea 0, puede significar dos cosas: (1) la sentencia ejecutada no ha actualizado ninguna fila, o (2) la sentencia ejecutada fue una sentencia DDL.

tilizar Uniones Algunas veces necesitamos utilizar una o más tablas para obtener los datos que queremos. Por ejemplo, supongamos que el propietario del "The Coffee Break" quiere una lista de los cafés que le compra a Acme, Inc. Esto implica información de la tabla COFFEES y también de la que vamos a crear SUPPLIERS. Este es el caso en que se necesitan los "joins" (unión). Una unión es una operación de base de datos que relaciona dos o más tablas por medio de los valores que comparten. En nuestro ejemplo, las tablas COFFEES y SUPPLIERS tienen la columna SUP_ID, que puede ser utilizada para unirlas. Antes de ir más allá, necesitamos crear la tabla SUPPLIERS y rellenarla con valores. El sigueinte código crea la tabla SUPPLIERS. String createSUPPLIERS = "create table SUPPLIERS " + "(SUP_ID INTEGER, SUP_NAME VARCHAR(40), " + "STREET VARCHAR(40), CITY VARCHAR(20), " + "STATE CHAR(2), ZIP CHAR(5))"; stmt.executeUpdate(createSUPPLIERS);

El siguiente código inserta filas para tres suministradores dentro de SUPPLIERS. stmt.executeUpdate("insert into SUPPLIERS values (101, " + "'Acme, Inc.', '99 Market Street', 'Groundsville', " + "'CA', '95199'"); stmt.executeUpdate("Insert into SUPPLIERS values (49," + "'Superior Coffee', '1 Party Place', 'Mendocino', 'CA', " + "'95460'"); stmt.executeUpdate("Insert into SUPPLIERS values (150, " + "'The High Ground', '100 Coffee Lane', 'Meadows', 'CA', " + "'93966'");

El siguiente código selecciona la tabla y nos permite verla. ResultSet rs = stmt.executeQuery("select * from SUPPLIERS");

El resultado sería algo similar a esto. SUP_ID SUP_NAME STREET CITY STATE ZIP ---------- ---------------------- --------------------- ---------------- --------- --------101 Acme, Inc. 99 Market Street Groundsville CA 95199 49 Superior Coffee 1 Party Place Mendocino CA 95460 150 The High Ground 100 Coffee Lane Meadows CA 93966 Ahora que tenemos las tablas COFFEES y SUPPLIERS, podremos proceder con el escenario en que el propietario quería una lista de los cafés comprados a un suministrador particular. Los nombres de los suminstradores están en la tabla SUPPLIERS, y los nombres de los cafés en la tabla COFFEES. Como ambas tablas tienen la columna SUP_ID, podemos utilizar esta columna en una unión. Lo siguiente que necesitamos es la forma de distinguir la columna SUP_ID a la que nos referimos. Esto se hace precediendo el nombre de la columna con el nombre de la tabla, "COFFEES.SUP_ID" para

indicar que queremos referirnos a la columna SUP_ID de la tabla COFFEES. En el siguiente código, donde stmt es un objeto Statement, seleccionamos los cafés comprados a Acme, Inc.. String query = " SELECT COFFEES.COF_NAME " + "FROM COFFEES, SUPPLIERS " + "WHERE SUPPLIERS.SUP_NAME LIKE 'Acme, Inc.'" + "and SUPPLIERS.SUP_ID = COFFEES.SUP_ID"; ResultSet rs = stmt.executeQuery(query); System.out.println("Coffees bought from Acme, Inc.: "); while (rs.next()) { String coffeeName = getString("COF_NAME"); System.out.println(" " + coffeeName); }

Esto producirá la siguiente salida. Coffees bought from Acme, Inc.. Colombian Colombian_Decaf

Utilizar Transaciones Hay veces que no queremos que una sentencia tenga efecto a menos que otra también suceda. Por ejemplo, cuando el propietario del "The Coffee Break" actualiza la cantidad de café vendida semanalmente, también querrá actualizar la cantidad total vendida hasta la fecha. Sin embargo, el no querrá actualizar una sin actualizar la otra; de otro modo, los datos serían inconsistentes. La forma para asegurarnos que ocurren las dos acciones o que no ocurre ninguna es utilizar una transación. Una transación es un conjunto de una o más sentencias que se ejecutan como una unidad, por eso o se ejecutan todas o no se ejecuta ninguna.

Desactivar el modo Auto-entrega Cuando se crea una conexión, está en modo auto-entrega. Esto significa que cada sentencia SQL individual es tratada como una transación y será automáticamente entregada justo después de ser ejecutada. (Para ser más preciso, por defecto, una sentencia SQL será entregada cuando está completa, no cuando se ejecuta. Una sentencia está completa cuando todas sus hojas de resultados y cuentas de actualización han sido recuperadas. Sin embargo, en la mayoría de los casos, una sentencia está completa, y por lo tanto, entregada, justo después de ser ejecutada). La forma de permitir que dos o más sentencia sean agrupadas en una transación es desactivar el modo auto-entrega. Esto se demuestra en el siguiente código, donde con es una conexión activa. con.setAutoCommit(false);

Entregar una Transación Una vez que se ha desactivado la auto-entrega, no se entregará ninguna sentencia SQL hasta que llamemos explícitamente al método commit. Todas las sentencias ejecutadas después de la anterior llamada al método commit serán incluidas en la transación actual y serán entregadas juntas como una unidad. El siguiente código, en el que con es una conexión activa, ilustra una transación. con.setAutoCommit(false); PreparedStatement updateSales = con.prepareStatement( "UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ?"); updateSales.setInt(1, 50); updateSales.setString(2, "Colombian");

updateSales.executeUpdate(); PreparedStatement updateTotal = con.prepareStatement( "UPDATE COFFEES SET TOTAL = TOTAL + ? WHERE COF_NAME LIKE ?"); updateTotal.setInt(1, 50); updateTotal.setString(2, "Colombian"); updateTotal.executeUpdate(); con.commit(); con.setAutoCommit(true);

En este ejemplo, el modo auto-entrega se desactiva para la conexión con, lo que significa que las dos sentencias prepared updateSales y updateTotal serán entregadas juntas cuando se llame al método commit. Siempre que se llame al método commit (bien automáticamente, cuando está activado el modo auto-commit o explícitamente cuando está desactivado), todos los cambios resultantes de las sentencias de la transación serán permanentes. En este caso, significa que las columnas SALES y TOTAL para el café Colombian han sido cambiadas a 50 (si TOTAL ha sido 0 anteriormente) y mantendrá este valor hasta que se cambie con otra sentencia de actualización. La línea final del ejemplo anterior activa el modo auto-commit, lo que significa que cada sentencia será de nuevo entregada automáticamente cuando esté completa. Volvemos por lo tanto al estado por defecto, en el que no tenemos que llamar al método commit. Es bueno desactivar el modo autocommit sólo mientras queramos estar en modo transación. De esta forma, evitamos bloquear la base de datos durante varias sentencias, lo que incrementa los conflictos con otros usuarios.

Utilizar Transaciones para Preservar al Integridad de los Datos Además de agrupar las sentencias para ejecutarlas como una unidad, las transaciones pueden ayudarnos a preservar la integridad de los datos de una tabla. Por ejemplo, supongamos que un empleado se ha propuesto introducir los nuevos precios de los cafés en la tabla COFFEES pero lo retrasa unos días. Mientras tanto, los precios han subido, y hoy el propietario está introduciendo los nuevos precios. Finalmente el empleado empieza a intrudir los precios ahora desfasados al mismo tiempo que el propietario intenta actualizar la tabla. Después de insertar los precios desfasados, el empleado se da cuenta de que ya no son válidos y llama el método rollback de la Connection para deshacer sus efectos. (El método rollback aborta la transación y restaura los valores que había antes de intentar la actualziación. Al mismo tiempo, el propietario está ejecutando una sentencia SELECT e imprime los nuevos precios. En esta situación, es posible que el propietario imprima los precios que más tarde serían devueltos a sus valores anteriores, haciendo que los precio impresos sean incorrectos. Esta clase de situaciones puede evitarse utilizando Transaciones. Si un controlador de base de datos soporta transaciones, y casi todos lo hacen, proporcionará algún nivel de protección contra conflictos que pueden surgir cuando dos usuarios acceden a los datos a la misma vez. Para evitar conflictos durante una transación, un controlador de base de datos utiliza bloqueos, mecanismos para bloquear el acceso de otros a los datos que están siendo accedidos por una transación. (Observa que en el modo auto-commit, donde cada sentencia es una transación, el bloqueo sólo se mantiene durante una sentencia). Una vez activado, el bloqueo permanece hasta que la transación sea entregada o anulada. Por ejemplo, un controlador de base de datos podría bloquear una fila de una tabla hasta que la actualización se haya entregado. El efecto de este bloqueo es evitar que usuario obtenga una lectura sucia, esto es, que lea un valor antes de que sea permanente. (Acceder a un valor actualizado que no haya sido entregado se considera una lectura sucia porque es posible que el valor sea devuelto a su valor anterior. Si leemos un valor que luego es devuelto a su valor antiguo, habremos leído un valor nulo). La forma en que se configuran los bloqueos está determinado por lo que se llama nivel de aislamiento de transación, que pude variar desde no soportar transaciones en absoluto a soportar todas las transaciones que fuerzan una reglas de acceso muy estrictas. Un ejemplo de nivel de aislamiento de transación es TRANSACTION_READ_COMMITTED, que

no permite que se acceda a un valor hasta que haya sido entregado. En otras palabras, si nivel de aislamiento de transación se selecciona a TRANSACTION_READ_COMMITTED, el controlador de la base de datos no permitirá que ocurran lecturas sucias. El interface Connection incluye cinco valores que representan los niveles de aislamiento de transación que se pueden utilizar en JDBC. Normalmente, no se necesita cambiar el nivel de aislamiento de transación; podemos utilizar el valor por defecto de nuestro controlador. JDBC permite averiguar el nivel de aislamiento de transación de nuestro controlador de la base de datos (utilizando el método getTransactionIsolation de Connection) y permite configurarlo a otro nivel (utilizando el método setTransactionIsolation de Connection). Sin embargo, ten en cuenta, que aunque JDBC permite seleccionar un nivel de aislamiento, hacer esto no tendrá ningún efecto a no ser que el driver del controlador de la base de datos lo soporte.

Cuándo llamar al método rollback Como se mencionó anteriormente, llamar al método rollback aborta la transación y devuelve cualquier valor que fuera modificado a sus valores anteriores. Si estamos intentando ejecutar una o más sentencias en una transación y obtenemos una SQLException, deberíamos llamar al método rollback para abortar la transación y empezarla de nuevo. Esta es la única forma para asegurarnos de cuál ha sido entregada y cuál no ha sido entregada. Capturar una SQLException nos dice que hay algo erróneo, pero no nos dice si fue o no fue entregada. Como no podemos contar con el hecho de que nada fue entregado, llamar al método rollback es la única forma de asegurarnos.

Procedimientos Almacenados Un procedimiento almacenado es un grupo de sentencias SQL que forman una unidad lógica y que realizan una tarea particular. Los procedimientos almacenados se utilizan para encapsular un conjunto de operaciones o peticiones para ejecutar en un servidor de base de datos. Por ejemplo, las operaciones sobre una base de datos de empleados (salarios, despidos, promociones, bloqueos) podrían ser codificados como procedimientos almacenados ejecutados por el código de la aplicación. Los procedimientos almacenados pueden compilarse y ejecutarse con diferentes parámetros y resultados, y podrían tener cualquier combinación de parámtros de entrada/salida. > Los procedimientos almacenados están soportados por la mayoría de los controladores de bases de datos, pero existe una gran cantidad de variaciones en su síntaxis y capacidades. Por esta razón, sólo mostraremos un ejemplo sencillo de lo que podría ser un procedimiento almacenado y cómo llamarlos desde JDBC, pero este ejemplo no está diseñado para ejecutarse.

Llamar a un Procedimiento Almacenado desde JDBC JDBC permite llamar a un procedimiento almacenado en la base de datos desde una aplicación escrita en Java. El primer paso es crear un objeto CallableStatement. Al igual que con los objetos Statement y PreparedStatement, esto se hace con una conexión abierta, Connection. Un objeto CallableStatement contiene una llamada a un procedimiento almacenado; no contiene el propio procedimiento. La primera línea del código siguiente crea una llamada al procedimiento almacenado SHOW_SUPPLIERS utilizando la conexión con. La parte que está encerrada entre corchetes es la sintaxis de escape para los precedimientos almacenados. Cuando un controlador encuentra "{call SHOW_SUPPLIERS}", traducirá esta sintaxis de escape al SQL nativo utilizado en la base de datos para llamar al procedimiento almacenado llamado SHOW_SUPPLIERS. CallableStatement cs = con.prepareCall("{call SHOW_SUPPLIERS}"); ResultSet rs = cs.executeQuery();

La hoja de resultados de rs será similar a esto. SUP_NAME

COF_NAME

---------------Acme, Inc. Acme, Inc. Superior Coffee Superior Coffee The High Ground

----------------------Colombian Colombian_Decaf French_Roast French_Roast_Decaf Espresso

Observa que el método utilizado para ejecutar cs es executeQuery porque cs llama a un procedimiento almacenado que contiene una petición y esto produce una hoja de resultados. Si el procedimiento hubiera contenido una sentencia de actualziación o una sentencia DDL, se hubiera utilizado el método executeUpdate. Sin embargo, en algunos casos, cuando el procedimiento almacenado contiene más de una sentencia SQL producirá más de una hoja de resultados, o cuando contiene más de una cuenta de actualizaciónm o alguna combinación de hojas de resultados y actualizaciones. en estos casos, donde existen múltiples resultados, se debería utilizar el método execute para ejecutar CallableStatement. La clase CallableStatement es una subclase de PreparedStatement, por eso un objeto CallableStatement puede tomar parámetros de entrada como lo haría un objeto PreparedStatement. Además, un objeto CallableStatement puede tomar parámetros de salida, o parámetros que son tanto de entrada como de salida. Los parámetros INOUT y el método execute se utilizan raramente.

rear Aplicaciones JDBC Completas Hasta ahora sólo hemos visto fragmentos de código. Más adelante veremos programas de ejemplo que son aplicaciones completas que podremos ejecutar. El primer código de ejemplo crea la tabla COFFEES; el segundo inserta valores en la tabla e imprime los resultados de una petición. La terecera aplicación crea la tabla SUPPLIERS, y el cuarto la rellena con valores. Después de haber ejecutado este código, podemos intentar una petición que una las tablas COFFEES y SUPPLIERS, como en el quinto código de ejemplo. El sexto ejemplo de código es una aplicación que demuestra una transación y también muestra como configurar las posiciones de los parámetros en un objeto PreparedStatement utilizando un bucle for. Como son aplicaciones completas, incluyen algunos elementos del lenguaje Java que no hemos visto en los fragmentos anteriores. Aquí explicaremos estos elementos brevemente.

Poner Código en una Definición de Clase En el lenguaje Java, cualquier código que querramos ejecutar debe estar dentro de una definición de clase. Tecleamos la definición de clase en un fichero y a éste le damos el nombre de la clase con la extensión .java. Por eso si tenemos una clase llamada MySQLStatement, su definición debería estar en un fichero llamado MySQLStatement.java

Importar Clases para Hacerlas Visibles Lo primero es importar los paquetes o clases que se van a utilizar en la nueva clase. Todas las clases de nuestros ejemplos utilizan el paquete java.sql (el API JDBC), que se hace visible cuando la siguiente línea de código precede a la definición de clase. import java.sql.*;

El asterisco (*) indica que todas las clases del paquete java.sql serán importadas. Importar una clase la hace visible y significa que no tendremos que escribir su nombre totalmente cualificado cuando utilicemos un método o un campo de esa clase. Si no incluimos "import java.sql.*;" en nuestro código, tendríamos que escribir "java.sql." más el nombre de la clase delante de todos los campos o métodos JDBC que utilicemos cada vez que los utilicemos. Observa que también podemos importar

clases individuales selectivamente en vez de importar un paquete completo. Java no requiere que importemos clases o paquetes, pero al hacerlo el código se hace mucho más conveniente. Cualquier línea que importe clases aparece en la parte superior de los ejemplos de código, que es donde deben estar para hacer visibles las clases importadas a la clase que está siendo definida. La definición real de la clase sigue a cualquier línea que importe clases.

Utilizar el Método main() Si una clase se va a ejecutar, debe contener un método static public main. Este método viene justo después de la línea que declara la clase y llama a los otros métodos de la clase. La palabra clave static indica que este método opera a nivel de clase en vez sobre ejemplares individuales de la clase. La palabra clave public significa que los miembros de cualquier clase pueden acceder a este método. Como no estamos definiendo clases sólo para ser ejecutadas por otras clases sino que queremos ejecutarlas, las aplicaciones de ejemplo de este capítulo incluyen un método main.

Utilizar bloques try y catch Algo que también incluyen todas las aplicaciones de ejemplo son los bloques try y catch. Este es un mecanismo del lenguaje Java para manejar excepciones. Java requiere que cuando un método lanza un excepción exista un mecanismo que la maneje. Generalmente un bloque catch capturará la excepción y especificará lo que sucederá (que podría ser no hacer nada). En el código de ejemplo, utilizamos dos bloques try y dos bloques catch. El primer bloque try contiene el método Class.forName, del paquete java.lang. Este método lanza una ClassNotFoundException, por eso el bloque catch que le sigue maneja esa excepción. El segundo bloque try contiene métodos JDBC, todos ellos lanzan SQLException, por eso el bloque catch del final de la aplicación puede manejar el resto de las excepciones que podrían lanzarse ya que todas serían objetos SQLException.

Recuperar Excepciones JDBC permite ver los avisos y excepciones generados por nuestro controlador de base de datos y por el compilador Java. Para ver las excepciones, podemos tener un bloque catch que las imprima. Por ejemplo, los dos bloques catch del siguiente código de ejemplo imprimen un mensaje explicando la excepción. try {

// Aquí va el código que podría generar la excepción. // Si se genera una excepción, el bloque catch imprimirá // información sobre ella. } catch(SQLException ex) { System.err.println("SQLException: " + ex.getMessage()); } try {

Class.forName("myDriverClassName"); } catch(java.lang.ClassNotFoundException e) { System.err.print("ClassNotFoundException: "); System.err.println(e.getMessage()); }

Si ejecutarámos CreateCOFFEES.java dos veces, obtendríamos un mensaje de error similar a éste. SQLException: There is already an object named 'COFFEES' in the database. Severity 16, State 1, Line 1

Este ejemplo ilustra la impresión del componente mensaje de un objeto SQLException, lo que es suficiente para la mayoría de las situaciones. Sin embargo, realmente existen tres componentes, y para ser completos, podemos imprimirlos todos. El siguiente fragmento de código muestra un bloque catch que se ha completado de dos formas. Primero, imprime las tres partes de un objeto SQLException: el mensaje (un string que describe el error), el SQLState (un string que identifica el error de acuerdo a los convenciones X/Open de SQLState), y un código de error del vendedor (un número que es el código de error del vendedor del driver). El objeto SQLException, ex es capturado y se accede a sus tres componentes con los métodos getMessage, getSQLState, y getErrorCode. La segunda forma del siguiente bloque catch completo obtiene todas las exepciones que podrían haber sido lanzada. Si hay una segunda excepción, sería encadenada a ex, por eso se llama a ex.getNextException para ver si hay más excepciones. Si las hay, el bucle while continúa e imprime el mensaje de la siguiente excecpción, el SQLState, y el código de error del vendedor. Esto continúa hasta que no haya más excepciones. try {

// Aquí va el código que podría generar la excepción. // Si se genera una excepción, el bloque catch imprimirá // información sobre ella. } catch(SQLException ex) { System.out.println("\n--- SQLException caught ---\n"); while (ex != null) { System.out.println("Message: " + ex.getMessage ()); System.out.println("SQLState: " + ex.getSQLState ()); System.out.println("ErrorCode: " + ex.getErrorCode ()); ex = ex.getNextException(); System.out.println(""); } }

Si hubieramos sustituido el bloque catch anterior en el Código de ejemplo 1 (CreateCoffees) y lo hubieramos ejecutado después de que la tabla COFFEES ya se hubiera creado, obtendríamos la siguiente información. --- SQLException caught --Message: There is already an object named 'COFFEES' in the database. Severity 16, State 1, Line 1 SQLState: 42501 ErrorCode: 2714

SQLState es un código definido en X/Open y ANSI-92 que identifica la excepción. Aquí podemos ver dos ejemplos de códigos SQLState. 08001 -- No suitable driver HY011 -- Operation invalid at this time

El código de error del vendedor es específico de cada driver, por lo que debemos revisar la documentación del driver buscando una lista con el significado de estos códigos de error.

Recuperar Avisos Los objetos SQLWarning son una subclase de SQLException que trata los avisos de accesos a bases de datos. Los Avisos no detienen la ejecución de una aplicación, como las excepciones; simplemente alertan al usuario de que algo no ha salido como se esperaba. Por ejemplo, un aviso podría hacernos saber que un privilegio que queriamos revocar no ha fue revocado. O un aviso podría decirnos que ha ocurrido algún error durante una petición de desconexión.

Un aviso puede reportarse sobre un objeto Connection, un objeto Statement (incluyendo objetios PreparedStatement y CallableStatement), o un objeto ResultSet. Cada una de esas clases tiene un método getWarnings, al que debemos llamar para ver el primer aviso reportado en la llamada al objeto. Si getWarnings devuelve un aviso, podemos llamar al método getNextWarning de SQLWarning para obtener avisos adicionales. Al ejecutar una sentencia se borran automáticamente los avisos de la sentencia anterior, por eso no se apilan. Sin embargo, esto significa que si queremos recuperar los avisos reportados por una sentencia, debemos hacerlo antes de ejecutar otra sentencia. El siguiente fragmento de código ilustra como obtener información completa sobre los avisos reportados por el objeto Statement, stmt y también por el objeto ResultSet, rs. Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery("select COF_NAME from COFFEES"); while (rs.next()) { String coffeeName = rs.getString("COF_NAME"); System.out.println("Coffees available at the Coffee Break: "); System.out.println(" " + coffeeName); SQLWarning warning = stmt.getWarnings(); if (warning != null) { System.out.println("\n---Warning---\n"); while (warning != null) { System.out.println("Message: " + warning.getMessage()); System.out.println("SQLState: " + warning.getSQLState()); System.out.print("Vendor error code: "); System.out.println(warning.getErrorCode()); System.out.println(""); warning = warning.getNextWarning(); } } SQLWarning warn = rs.getWarnings(); if (warn != null) { System.out.println("\n---Warning---\n"); while (warn != null) { System.out.println("Message: " + warn.getMessage()); System.out.println("SQLState: " + warn.getSQLState()); System.out.print("Vendor error code: "); System.out.println(warn.getErrorCode()); System.out.println(""); warn = warn.getNextWarning(); } } }

Los avisos no son muy comunes, De aquellos que son reportados, el aviso más común es un DataTruncation, una subclase de SQLWarning. Todos los objetos DataTruncation tienen un SQLState 01004, indicando que ha habido un problema al leer o escribir datos. Los métodos de DataTruncation permiten encontrar en que columna o parámetro se truncaron los datos, si la ruptura se produjo en una operación de lectura o de escritura, cuántos bytes deberían haber sido transmitidos, y cuántos bytes se transmitieron realmente.

Ejecutar la aplicación de Ejemplo Ahora estamos listos para probar algún código de ejemplo. El directorio book.html, contiene una aplicación completa, ejecutable, que ilustra los conceptos presentados en este capítulo y el siguiente. Puedes descargar este codigo de ejemplo del site de JDBC situado en http://www.javasoft.com/products/jdbc/book.html Antes de poder ejecutar una de esas aplicaciones, necesitamos editar el fichero sustituyendo la información apropiada para las siguientes variables. url

La URL JDBC, las partes uno y dos son suministradas por el driver, y la tercera parte especifica la fuente de datos. myLogin Tu nombre de usuario o login. myPassword Tu password para el controlador de base de datos. myDriver.ClassName El nombre de clase suministrado con tu driver La primera aplicación de ejemplo es la clase CreateCoffees, que está en el fichero llamado CreateCoffees.java. Abajo tienes las instrucciones para ejecutar CreateCoffees.java en las dos plataformas principales. La primera línea compila el código del fichero CreateCoffees.java. Si la compilación tiene éxito, se producirá un fichero llamado CreateCoffees.class, que contendrá los bytecodes traducidos desde el fichero CreateCoffees.java. Estos bytecodes serán interpretados por la máquina virtual Java, que es la que hace posible que el código Java se pueda ejecutar en cualquier máquina que la tenga instalada. La segunda línea de código ejecuta el código. Observa que se utiliza el nombre de la clase, CreateCoffees, no el nombre del fichero CreateCoffees.class. UNIX javac CreateCoffees.java java CreateCoffees

Windows 95/NT javac CreateCoffees.java java CreateCoffees

Crear un Applet desde una Aplicación Supongamos que el propietario de "The Coffee Break" quiere mostrar los precios actuales de los cafés en un applet en su página Web. Puede asegurarse de que está mostrando los precios actuales haciendo que applet obtenga los precios directamente desde su base de datos. Para hacer esto necesita crear dos ficheros de código, uno con el código del applet y otro con el código HTML. El código del applet contiene el código JDBC que podría aparecer en una aplicación normal más el código adicional para ejecutar el applet y mostrar el resultado de la petición a la base d edatos. En nuestro ejemplo el código del applet está en el fichero OutputApplet.java. Para mostrar el applet en una página HTML, el fichero OutputApplet.html le dice al navegador qué mostrar y dónde mostrarlo. El resto de esta página explicará algunos elementos que se encuentran en el código del applet y que no están presentes en el código de las aplicaciones. Algunos de estos ejemplos involucran aspectos avanzados del lenguaje Java. Daremos alguna explicación básica y racional, ya que una explicación completa va más allá de este turorial. El propósito de este applet es dar una idea general, para poder utilizarlo como plantilla, sustituyendo nuestras consultas por las del applet.

Escribir el Código del Applet Para empezar, los applets importan clases que no son utilizadas por las aplicaciones. Nuestro applet importa dos clases que son especiales para los applets: la clase Applet, que forma parte del paquete java.applet, y la clase Graphics, que forma parte del paquete java.awt. Este applet también importa la clase de propósito general java.util.Vector que se utiliza para acceder a un contenedor tipo array cuyo tamaño puede ser modificado. Este código utiliza objetos Vector para almacenar los resultados

de las peticiones para poder mostrarlas después. Todos los applets descienden de la clase Applet; es decir, son subclases de Applet. Por lo tanto, toda definición de applet debe contener las palabras extends Applet; como se vé aquí. public class MyAppletName extends Applet { . . . }

En nuestro ejemplo, esta línea también incluye las palabras implements Runnable, por lo que se parece a esto. public class OutputApplet extends Applet implements Runnable { . . . }

Runnable es un interface que hace posible ejecutar más de un thread a la vez. Un thread es un flujo secuencial de control, y un programa puede tener muchos threads haciendo cosas diferentes concurrentemente. La clase OutputApplet implementa el interface Runnable definiendo el método run; el único método de Runnable. En nuestro ejemplo el método run contiene el código JDBC para abrir la conexión, ejecutar una petición, y obtener los resultados desde la hoja de resultados. Como las conexiones a las bases de datos pueden ser lentas, y algunas veces pueden tardar varios segundos, es una buena idea estructurar un applet para que pueda manejar el trabajo con bases de datos en un thread separado. Al igual que las aplicaciones deben tener un método main, un applet debe implementar al menos uno de estos métodos init, start, o paint. Nuestro ejemplo define un método start y un método paint. Cada vez que se llama a start, crea un nuevo thread (worker) para re-evaluar la petición a la base de datos. Cada vez que se llama a paint, se muestra o bien el resultado de la petición o un string que describe el estado actual del applet. Como se mencionó anteriormente, el método run definido en OutputApplet contiene el código JDBC, Cuando el thread worker llama al método start, se llama automáticamente al método run, y éste ejecuta el código JDBC en el thread worker. El código de run es muy similar al código que hemos visto en otros ejemplos con tres excepciones. Primero, utiliza la clase Vector para almacenar los resultados de la petición. Segundo, no imprime los resultados, sino que los añade al Vector, results para mostrarlos más tarde. Tercero, tampoco muestra ninguna excepción, en su lugar almacena los mensajes de error para mostrarlos más tarde. Los applets tienen varias formas de dibujar, o mostrar, su contenido. Este applet, es uno muy simple que sólo utiliza texto, utiliza el método drawString (una parte de la clase Graphics) para mostrar texto. El método drawString tiene tres argumentos: (1) el string a mostrar, (2) la coordenada x, indicando la posición horizontal de inicio del string, y (3) la coordenada y, indicando la posición vertical de inicio del string (que está en la parte inferior del texto). El método paint es el que realmente dibuja las cosas en la pantalla, y en OutputApplet.java, está definido para contener llamadas al método drawString. La cosa principal que muestra drawString es el contenido del Vector, results (los resultados almacenados). Cuando no hay resultados que mostrar, drawString muestra el estado actual contenido en el String, message. Este string será "Initializing" para empezar. Será "Connecting to database" cuando se llame al método start, y el método setError pondrá en él un mensaje de error cuando capture una excepción. Así, si la conexión a la base de datos tarda mucho tiempo, la persona que está viendo el applet verá el mensaje "Connecting to database" porque ese será el contenido de message en ese momento. (El método paint es llamado por el AWT cuando quiere que el applet muestre su estado actual en la pantalla). Al menos dos métodos definidos en la clase OutputApplet, setError y setResults son privados, lo que signfica que sólo pueden ser utilizados por OutputApplet. Estos métodos llaman el método repaint, que borra la pantalla y llama a paint. Por eso si setResults llama a repaint, se mostrarán los resultados de la petición, y si setError llama a repaint, se mostrará un mensaje de error.

Otro punto es hacer que todos los métodos definidos en OutputApplet excepto run son synchronized. La palabra clave synchronized indica que mientras que un método esté accediendo a un objeto, otros métodos synchronized están bloqueados para acceder a ese objeto. El método run no se declara synchronized para que el applet pueda dibujarse en la pantalla mientras se produce la conexión a la base de datos. Si los métodos de acceso a la base de datos fueran synchronized, evitarían que el applet se redibujara mientras se están ejecutando, lo que podría resultar en retrasos sin acompañamiento de mensaje de estado. Para sumarizar, en un applet, es una buena práctica de programación es hacer algunas cosas que no necesitaríamos hacer en una aplicación. 1. Poner nuestro código JDBC en un thread separado. 2. Mostrar mensajes de estado durante los retardos, como cuando la conexión a la base de datos tarda mucho tiempo. 3. Mostrar los mensajes de error en la pantalla en lugar de imprimirlos en System.out o System.err.

Ejecutar un Applet Antes de ejecutar nuestro applet, necesitamos compilar el fichero OutputApplet.java. Esto crea el fichero OutputApplet.class, que es referenciado por el fichero OutputApplet.html. La forma más fácil de ejecutar un applet es utilizar el appletviewer, que se incluye en el JDK. Sólo debemos seguir las instrucciones para nuestra plataforma. UNIX javac OutputApplet.java appletviewer OutputApplet.html

Windows 95/NT javac OutputApplet.java appletviewer OutputApplet.html

Los applets descargados a través de la red están sujetos a distintas restricciones de seguridad. Aunque esto puede parecer molesto, es absolutamente necesario para la seguridad de la red, y la seguridad es una de las mayores ventajas de utilizar Java. Un applet no puede hacer conexiones en la red excepto con el host del que se descargó a menos que el navegador se lo permita. Si uno puede tratar con applets instalados localmente como applets "trusted" (firmados) también dependen de restricciones de seguridad impuestas por el navegador. Un applet normalmente no puede leer o escribir ficheros en el host en el que se está ejecuando, y no puede cargar librerías ni definir métodos nativos. Los applets pueden hacer conexiones con el host del que vinieron, por eso pueden trabajar muy bien en intranets. El driver puente JDBC-ODBC es de alguna forma un caso especial. Puede utilizarse satisfactoriamente para acceder a intranet, pero requiere que ODBC, el puente, la librería nativa, y el JDBC esten instalados en cada cliente. Con esta configuración, los accesos a intranet funcionan con aplicaciones Java y con applets firmados. Sin embargo, como los puentes requieren configuraciones especiales del cliente, no es práctico para ejecutar applets en Internet con el puente JDBC-ODBC. Observa que esta limitaciones son para el puente JDBC-ODBC, no para JDBC. Con un driver JDBC puro Java, no se necesita ninguna configuración especial para ejecutar applets en Internet.

El API de JDBC 2..0 El paquete java.sql que está incluido en la versión JDK 1.2 (conocido como el API JDBC 2.0) incluye muchas nuevas características no incluidas en el paquete java.sql que forma parte de la versión JDK 1.1 (referenciado como el API JDBC 1.0). Con el API JDBC 2.0, podremos hacer las siguientes cosas: ● ● ● ●

Ir hacia adelante o hacia atrás en una hoja de resultados o movernos a un fila específica. Hacer actualizaciones de las tablas de la base datos utilizando métodos Java en lugar de utilizar comandos SQL. Enviar múltiples secuencias SQL a la base de datos como una unidad, o batch. Uitlizar los nuevos tipos de datos SQL3 como valores de columnas.

Mover el Cursor por una Hoja de Resultados Una de las nuevas características del API JDBC 2.0 es la habilidad de mover el cursor en una hoja de resultados tanto hacia atrás como hacia adelante. También hay métodos que nos permiten mover el cursor a una fila particular y comprobar la posición del cursor. La hoja de resultados Scrollable hace posible crear una herramienta GUI (Interface Gráfico de Usuario) para navegar a través de ella, lo que probablemente será uno de los principales usos de esta característica. Otro uso será movernos a una fila para actualizarla. Antes de poder aprovechar estas ventajas, necesitamos crear un objeto ResultSet Scrollable. Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); ResultSet srs = stmt.executeQuery("SELECT COF_NAME, PRICE FROM COFFEES");

Este código es similar al utilizado anteriormente, excepto en que añade dos argumentos al método createStatement. El primer argumento es una de las tres constantes añadidas al API ResultSet para indicar el tipo de un objeto ResultSet: TYPE_FORWARD_ONLY, TYPE_SCROLL_INSENSITIVE, y TYPE_SCROLL_SENSITIVE. El segundo argumento es una de las dos constantes de ResultSet para especificar si la hoja de resultados es de sólo lectura o actualizable:CONCUR_READ_ONLY y CONCUR_UPDATABLE. Lo que debemos recordar aquí es que si especificamos un tipo, también debemos especificar si es de sólo lectura o actualizable. También, debemos especificar primero el tipo, y como ambos parámetros son int, el compilador no comprobará si los hemos intercambiado. Especificando la constante TYPE_FORWARD_ONLY se crea una hoja de resultados no desplazable, es decir, una hoja en la que el cursor sólo se mueve hacia adelante. Si no se especifican constantes para el tipo y actualización de un objeto ResultSet, obtendremos automáticamente una TYPE_FORWARD_ONLY y CONCUR_READ_ONLY (exactamente igual que en el API del JDBC 1.0). Obtendremos un objeto ResultSet desplazable si utilizamos una de estas constantes:TYPE_SCROLL_INSENSITIVE o TYPE_SCROLL_SENSITIVE. La diferencia entre estas dos es si la hoja de resultados refleja los cambios que se han hecho mientras estaba abierta y si se puede llamar a ciertos métodos para detectar estos cambios. Generalmente hablando, una hoja de resultados TYPE_SCROLL_INSENSITIVE no refleja los cambios hechos mientras estaba abierta y en una hoja TYPE_SCROLL_SENSITIVE si se reflejan. Los tres tipos de hojas de resultados harán visibles los resultados si se cierran y se vuelve a abrir. En este momento, no necesitamos preocuparnos de los puntos delicados de las capacidades de un objeto ResultSet, entraremos en más detalle más adelante. Aunque deberíamos tener en mente el hecho de que no importa el tipo de hoja de resultados que especifiquemos, siempre estaremos limitados por nuestro controlador de base de datos y el driver utilizados.

Una vez que tengamos un objeto ResultSet desplazable, srs en el ejemplo anterior, podemos utilizarlo para mover el cursor sobre la hoja de resultados. Recuerda que cuando creabamos un objeto ResultSet anteriormente, tenía el cursor posicionado antes de la primera fila. Incluso aunque una hoja de resultados se seleccione desplazable, el cursor también se posiciona inicialmente delante de la primera fila. En el API JDBC 1.0, la única forma de mover el cursor era llamar al método next. Este método todavía es apropiado si queremos acceder a las filas una a una, yendo de la primera fila a la última, pero ahora tenemos muchas más formas para mover el cursor. La contrapartida del método next, que mueve el cursor una fila hacia delante (hacia el final de la hoja de resultados), es el nuevo método previous, que mueve el cursor una fila hacia atrás (hacia el inicio de la hoja de resultados). Ambos métodos devuelven false cuando el cursor se sale de la hoja de resultados (posición antes de la primera o después de la última fila), lo que hace posible utilizarlos en un bucle while. Ye hemos utilizado un método next en un bucle while, pero para refrescar la memoria, aquí tenemos un ejemplo que mueve el cursor a la primera fila y luego a la siguiente cada vez que pasa por el bucle while. El bucle termina cuando alcanza la última fila, haciendo que el método next devuelva false. El siguiente fragmento de código imprime los valores de cada fila de srs, con cinco espacios en blanco entre el nombre y el precio. Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); ResultSet srs = stmt.executeQuery("SELECT COF_NAME, PRICE FROM COFFEES"); while (srs.next()) { String name = srs.getString("COF_NAME"); float price = srs.getFloat("PRICE"); System.out.println(name + " " + price); }

La salida se podría parecer a esto. Colombian 7.99 French_Roast 8.99 Espresso 9.99 Colombian_Decaf 8.99 French_Roast_Decaf 9.99

Al igual que en el fragmento anterior, podemos procesar todas las filas de srs hacia atrás, pero para hacer esto, el cursor debe estar detrás de la última fila. Se puede mover el cursor explícitamente a esa posicón con el método afterLast. Luego el método previous mueve el cursor desde la posicón detrás de la última fila a la última fila, y luego a la fila anterior en cada iteracción del bucle while. El bucle termina cuando el cursor alcanza la posición anterior a la primera fila, cuando el método previous devuelve false. Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); ResultSet srs = stmt.executeQuery("SELECT COF_NAME, PRICE FROM COFFEES"); srs.afterLast(); while (srs.previous()) { String name = srs.getString("COF_NAME"); float price = srs.getFloat("PRICE"); System.out.println(name + " " + price); }

La salida se podría parecer a esto. French_Roast_Decaf 9.99 Colombian_Decaf 8.99 Espresso 9.99 French_Roast 8.99 Colombian 7.99

Como se puede ver, las dos salidas tienen los mismos valores, pero las filas están en orden inverso.

Se puede mover el cursor a una fila particular en un objeto ResultSet. Los métodos first, last, beforeFirst, y afterLast mueven el cursor a la fila indicada en sus nombres. El método absolute moverá el cursor al número de fila indicado en su argumento. Si el número es positivo, el cursor se mueve al número dado desde el principio, por eso llamar a absolute(1) pone el cursor en la primera fila. Si el número es negativo, mueve el cursor al número dado desde el final, por eso llamar a absolute(-1) pone el cursor en la última fila. La siguiente línea de código mueve el cursor a la cuarta fila de srs. srs.absolute(4);

Si srs tuviera 500 filas, la siguiente línea de código movería el cursor a la fila 497. srs.absolute(-4);

Tres métodos mueven el cursor a una posición relativa a su posición actual. Como hemos podido ver, el método next mueve el cursor a la fila siguiente, y el método previous lo mueve a la fila anterior. Con el método relative, se puede especificar cuántas filas se moverá desde la fila actual y también la dirección en la que se moverá. Un número positivo mueve el cursor hacia adelante el número de filas dado; un número negativo mueve el cursor hacia atrás el número de filas dado. Por ejemplo, en el siguiente fragmente de código, el cursor se mueve a la cuarta fila, luego a la primera y por último a la tercera. srs.absolute(4); // cursor está en la cuarta fila . . . srs.relative(-3); // cursor está en la primera fila . . . srs.relative(2); // cursor está en la tercera fila

El método getRow permite comprobar el número de fila donde está el cursor. Por ejemplo, se puede utilizar getRow para verificar la posición actual del cursor en el ejemplo anterior. srs.absolute(4); int rowNum = srs.getRow(); // rowNum debería ser 4 srs.relative(-3); int rowNum = srs.getRow(); // rowNum debería ser 1 srs.relative(2); int rowNum = srs.getRow(); // rowNum debería ser 3

Existen cuatro métodos adicionales que permiten verificar si el cursor se encuentra en una posición particular. La posición se indica en sus nombres:isFirst, isLast, isBeforeFirst, isAfterLast. Todos estos métodos devuelven un boolean y por lo tanto pueden ser utilizados en una sentencia condicional. Por ejemplo, el siguiente fragmento de código comprueba si el cursor está después de la última fila antes de llamar al método previous en un bucle while. Si el método isAfterLast devuelve false, el cursor no estará después de la última fila, por eso se llama al método afterLast. Esto garantiza que el cursor estára después de la última fila antes de utilizar el método previous en el bucle while para cubrir todas las filas de srs. if (srs.isAfterLast() == false) { srs.afterLast(); } while (srs.previous()) { String name = srs.getString("COF_NAME"); float price = srs.getFloat("PRICE"); System.out.println(name + " " + price); }

Hacer Actualizaciones en una Hoja de Resultados Otra nueva característica del API JDBC 2.0 es la habilidad de actualizar filas en una hoja de

resultados utilizando métodos Java en vez de tener que enviar comandos SQL. Pero antes de poder aprovechar esta capacidad, necesitamos crear un objeto ResultSet actualizable. Para hacer esto, suministramos la constante CONCUR_UPDATABLE de ResulSet al método createStatement, como se ha visto en ejemplos anteriores. El objeto Statement creado producirá un objeto ResultSet actualizable cada vez que se ejecute una petición. El siguiente fragmento de código ilustra la creacción de un objeto ResultSet actualizable, uprs. Observa que el código también lo hace desplazable. Un objeto ResultSet actualizable no tiene porque ser desplazable, pero cuando se hacen cambios en una hoja de resultados, generalmente queremos poder movernos por ella. Con una hoja de resultados desplazable, podemos movernos a las filas que queremos cambiar, y si el tipo es TYPE_SCROLL_SENSITIVE, podemos obtener el nuevo valor de una fila después de haberlo cambiado. Connection con = DriverManager.getConnection("jdbc:mySubprotocol:mySubName"); Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); ResultSet uprs = stmt.executeQuery("SELECT COF_NAME, PRICE FROM COFFEES");

El objeto ResultSet, uprs resultante se podría parecer a esto. COF_NAME -----------------Colombian French_Roast Espresso Colombian_Decaf French_Roast_Decaf

PRICE ----7.99 8.99 9.99 8.99 9.99

Podemos utilizar los nuevos métodos del JDBC 2.0 en el interface ResultSet para insertar una nueva fila en uprs, borrar una fila de uprs, o modificar un valor de una columna de uprs.

Actualizar una Hoja de Resultados Programáticamente Una actualización es la modificación del valor de una columna de la fila actual. Supongamos que queremos aumentar el precio del café "French Roast Decaf" a 10.99. utilizando el API JDBC 1.0, la actualización podría ser algo como esto. stmt.executeUpdate("UPDATE COFFEES SET PRICE = 10.99" + "WHERE COF_NAME = FRENCH_ROAST_DECAF");

El siguiente fragmento de código muesta otra forma de realizar la actualización, esta vez utilizando el API JDBC 2.0. uprs.last(); uprs.updateFloat("PRICE", 10.99);

Las operaciones de actualización en el API JDBC 2.0 afectan a los valores de columna de la fila en la que se encuentra el cursor, por eso en le primera línea se llama al método last para mover el cursor a la última fila (la fila donde la columna COF_NAME tiene el valor FRENCH_ROAST_DECAF). Una vez situado el cursor, todos los métodos de actualización que llamemos operarán sobre esa fila hasta que movamos el cursor a otra fila. La segunda línea de código cambia el valor de la columna PRICE a 10.99 llamando al método updateFloat. Se utiliza este método porque el valor de la columna que queremos actualizar es un float Java. Los métodos updateXXX de ResultSet toman dos parámetros: la columna a actualizar y el nuevo valor a colocar en ella. Al igual que en los métodos getXXX de ResultSet., el parámetro que designa la columna podría ser el nombre de la columna o el número de la columna. Existe un método updateXXX diferente para cada tipo (updateString, updateBigDecimal, updateInt, etc.) En este punto, el precio en uprs para "French Roast Decaf" será 10.99, pero el precio en la tabla

COFFEES de la base de datos será todavía 9.99. Para que la actualización tenga efecto en la base de datos y no sólo en la hoja de resultados, debemos llamar al método updateRow de ResultSet. Aquí está el código para actualizar tanto uprs como COFFEES. uprs.last(); uprs.updateFloat("PRICE", 10.99); uprs.updateRow();

Si hubiéramos movido el cursor a una fila diferente antes de llamar al método updateRow, la actualización se habría perdido. Si, por el contrario, nos damos cuenta de que el precio debería haber sido 10.79 en vez de 10.99 podríamos haber cancelado la actualización llamando al método cancelRowUpdates. Tenemos que llamar al método cancelRowUpdates antes de llamar al método updateRow; una vez que se llama a updateRow, llamar a cancelRowUpdates no hará nada. Observa que cancelRowUpdates cancela todas las actualizaciones en una fila, por eso, si había muchas llamadas a método updateXXX en la misma fila, no podemos cancelar sólo una de ellas. El siguiente fragmento de código primero cancela el precio 10.99 y luego lo actualiza a 10.79. uprs.last(); uprs.updateFloat("PRICE", 10.99); uprs.cancelRowUpdates(); uprs.updateFloat("PRICE", 10.79); uprs.updateRow();

En este ejemplo, sólo se había actualizado una columna, pero podemos llamar a un método updateXXX apropiado para cada una de las columnas de la fila. El concepto a recordar es que las actualizaciones y las operaciones relacionadas se aplican sobre la fila en la que se encuentra el cursor. Incluso si hay muchas llamadas a métodos updateXXX, sólo se hace una llamada al método updateRow para actualizar la base de datos con todos los cambios realizados en la fila actual. Si también queremos actualizar el precio de COLOMBIAN_DECAF, tenemos que mover el cursor a la fila que contiene ese café. Cómo la fila de COLOMBIAN_DECAF precede inmediatamente a la fila de FRENCH_ROAST_DECAF, podemos llamar al método previous para posicionar el cursor en la fila de COLOMBIAN_DECAF. El siguiente fragmento de código cambia el precio de esa fila a 9.79 tanto en la hoja de resultados como en la base de datos. uprs.previous(); uprs.updateFloat("PRICE", 9.79); uprs.updateRow();

Todos los movimientos de cursor se refieren a filas del objeto ResultSet, no a filas de la tabla de la base de datos. Si una petición selecciona cinco filas de la tabla de la base de datos, habrá cinco filas en la hoja de resultados, con la primera fila siendo la fila 1, la sengunda siendo la fila 2, etc. La fila 1 puede ser identificada como la primera, y, en una hoja de resultados con cinco filas, la fila 5 será la última. El orden de las filas en la hoja de resultados no tiene nada que ver con el orden de las filas en la tablas de la base de datos. De hecho, el orden de las filas en la tabla de la base de datos es indeterminado. El controlador de la base de datos sigue la pista de las filas seleccionadas, y hace las actualizaciones en la fila apropiada, pero podrían estar localizadas en cualquier lugar de la tabla. Cuando se inserta una fila, por ejemplo, no hay forma de saber donde será insertada dentro de la tabla.

Insertar y Borrar filas Programáticamente En la sección anterior hemos visto cómo modificar el valor de una columna utilizando métodos del API JDBC 2.0 en vez de utilizar comandos SQL. Con el API JDBC 2.0 también podemos insertar una fila en una tabla o borrar una fila existente programáticamente. Supongamos que nuestro propietario del café ha obtenido una nueva variedad de café de uno de sus suministradores. El "High Ground", y quiere añadirlo a su base de datos. Utilizando el API JDBC 1,0

podría escribir el código que pasa una sentencia insert de SQL al controlador de la báse de datos. El siguiente fragmento de código, en el que stmt es un objeto Statement, muestra esta aproximación. stmt.executeUpdate("INSERT INTO COFFEES " + "VALUES ('Kona', 150, 10.99, 0, 0)");

Se puede hacer esto mismo sin comandos SQL utilizando los métodos de ResultSet del API JDBC 2.0. Básicamente, después de tener un objeto ResultSet con los resultados de la tabla COFFEES, podemos constuir una nueva fila insertándola tanto en la hoja de resultados como en la tabla COFFEES en un sólo paso. Se construye una nueva fila en una llamada "fila de inserción", una fila especial asociada con cada objeto ResultSet. Esta fila realmente no forma parte de la hoja de resultados; podemos pensar en ella como un buffer separado en el que componer una nueva fila. El primer paso será mover el cursor a la fila de inserción, lo que podemos hacer llamando al método moveToInsertRow. El siguiente paso será seleccionar un valor para cada columna de la fila. Hacemos esto llamando a los métodos updateXXX apropiados para cada valor. Observa que estos son los mismos métodos updateXXX utilizados en la página anterior para cambiar el valor de una columna. Finalmente, podemos llamar al método insertRow para insertar la fila que hemos rellenado en la hoja de resultados. Este único método inserta la fila simultáneamente tanto en el objeto ResultSet como en la tabla de la base de datos de la que la hoja de datos fue seleccionada. El siguiente fragmento de código crea un objeto ResultSet actualizable y desplazable, uprs, que contiene todas las filas y columnas de la tabla COFFEES. Connection con = DriverManager.getConnection("jdbc:mySubprotocol:mySubName"); Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); ResultSet uprs = stmt.executeQuery("SELECT * FROM COFFEES");

El siguiente fragmento de código utiliza el objeto ResultSet, uprs para insertar la fila para "kona", mostrada en el ejemplo SQL. Mueve el cursor a la fila de inserción, selecciona los cinco valores de columna e inserta la fila dentro de uprs y COFFEES. uprs.moveToInsertRow(); uprs.updateString("COF_NAME", "Kona"); uprs.updateInt("SUP_ID", 150); uprs.updateFloat("PRICE", 10.99); uprs.updateInt("SALES", 0); uprs.updateInt("TOTAL", 0); uprs.insertRow();

Como podemos utilizar el nombre o el número de la columna para indicar la columna seleccionada, nuestro código para seleccionar los valores de columna se podría parecer a esto. uprs.updateString(1, "Kona"); uprs.updateInt(2, 150); uprs.updateFloat(3, 10.99); uprs.updateInt(4, 0); uprs.updateInt(5, 0);

Podríamos habernos preguntado por qué los métodos updateXXX parecen tener un comportamiento distinto a como lo hacían en los ejemplos de actualización. En aquellos ejemplos, el valor seleccionado con un método updateXXX reemplazaba inmediatamente el valor de la columna en la hoja de resultados. Esto era porque el cursor estaba sobre una fila de la hoja de resultados. Cuando el cursor está sobre la fila de inserción, el valor seleccionado con un método updateXXX también es automáticamente seleccionado, pero lo es en la fila de inserción y no en la propia hoja de resultados. Tanto en actualizaciones como en inserciones, llamar a los métodos updateXXX no afectan a la tabla de la base de datos. Se debe llamar al método updateRow para hacer que las actualizaciones ocurran en la base de datos. Para el inserciones, el método insertRow inserta la nueva fila en la hoja de resultados y en la base de datos al mismo tiempo.

Podríamos preguntarnos que sucedería si insertáramos una fila pero sin suministrar los valores para cada columna. Si no suministramos valores para una columna que estaba definida para aceptar valores NULL de SQL, el valor asignado a esa columna es NULL. Si la columna no acepta valores null, obtendremos una SQLException. Esto también es cierto si falta una columna de la tabla en nuestro objeto ResultSet. En el ejemplo anterior, la petición era SELECT * FROM COFFEES, lo que producía una hoja de resultados con todas las columnas y todas las filas. Cuando queremos insertar una o más filas, nuestra petición no tiene porque seleccionar todas las filas, pero sí todas las columnas. Especialmente si nuestra tabla tiene cientos o miles de filas, querremos utilizar una claúsula WHERE para límitar el número de filas devueltas por la sentencia SELECT. Después de haber llamado al método insertRow, podemos construir otra fila, o podemos mover el cursor de nuevo a la hoja de resultados. Por ejemplo, podemos, llamar a cualquier método que ponga el cursor en una fila específica, como first, last, beforeFirst, afterLast, y absolute. También podemos utilizar los métodos previous, relative, y moveToCurrentRow. Observa que sólo podemos llamar a moveToCurrentRow cuando el cursor está en la fila de inserción. Cuando llamamos al método moveToInsertRow, la hoja de resultados graba la fila en la que se encontraba el cursor, que por definición es la fila actual. Como consecuencia, el método moveToCurrentRow puede mover el cursor desde la fila de inserción a la fila en la que se encontraba anteriormente. Esto también explica porque podemos utilizar los métodos previous y relative, que requieren movimientos relativos a la fila actual.

Insertar una Fila El siguiente ejemplo de código es un programa completo que debería funcionar si tenemos un driver JDBC 2.0 que implemente una hoja de resultados desplazable. Hay algunas cosas que podríamos observar sobre el código. 1. El objeto ResultSet, uprs es actualizable, desplazable y sensible a los cambios hechos por ella y por otros. Aunque es TYPE_SCROLL_SENSITIVE, es posible que los métodos getXXX llamados después de las inserciones no recuperen los valores de la nuevas filas. Hay métodos en el interface DatabaseMetaData que nos dirán qué es visible y qué será detectado en los diferentes tipos de hojas de resultados para nuestro driver y nuestro controlador de base de datos. 2. Después de haber introducido los valores de una fila con los métodos updateXXX, el código inserta la fila en la hoja de resultados y en la base de datos con el método insertRow. Luego, estándo todavía en la "fila de inserción", selecciona valores para otra nueva fila. import java.sql.*; public class InsertRows { public static void main(String args[]) { String url = "jdbc:mySubprotocol:myDataSource"; Connection con; Statement stmt; try { Class.forName("myDriver.ClassName"); } catch(java.lang.ClassNotFoundException e) { System.err.print("ClassNotFoundException: "); System.err.println(e.getMessage()); } try { con = DriverManager.getConnection(url, "myLogin", "myPassword"); stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); ResultSet uprs = stmt.executeQuery("SELECT * FROM COFFEES"); uprs.moveToInsertRow(); uprs.updateString("COF_NAME", "Kona"); uprs.updateInt("SUP_ID", 150);

uprs.updateFloat("PRICE", 10.99f); uprs.updateInt("SALES", 0); uprs.updateInt("TOTAL", 0); uprs.insertRow(); uprs.updateString("COF_NAME", "Kona_Decaf"); uprs.updateInt("SUP_ID", 150); uprs.updateFloat("PRICE", 11.99f); uprs.updateInt("SALES", 0); uprs.updateInt("TOTAL", 0); uprs.insertRow(); uprs.beforeFirst(); System.out.println("Table COFFEES after insertion:"); while (uprs.next()) { String name = uprs.getString("COF_NAME"); int id = uprs.getInt("SUP_ID"); float price = uprs.getFloat("PRICE"); int sales = uprs.getInt("SALES"); int total = uprs.getInt("TOTAL"); System.out.print(name + " " + id + " " + price); System.out.println(" " + sales + " " + total); } uprs.close(); stmt.close(); con.close();

}

} catch(SQLException ex) { System.err.println("SQLException: " + ex.getMessage()); }

Borrar una Fila Hasta ahora, hemos visto cómo actualizar un valor y cómo insertar una nueva fila. Borrar una fila es la tercera forma de modificar un objeto ResultSet, y es la más simple. Todo lo que tenemos que hacer es mover el cursor a la fila que queremos borrar y luego llamar al método deleteRow. Por ejemplo, si queremos borrar la cuarta fila de la hoja de resultados uprs, nuestro código se parecería a esto. uprs.absolute(4); uprs.deleteRow();

La cuarta fila ha sido eliminada de uprs y de la base de datos. El único problema con las eliminaciones es lo que ResultSet realmente hace cuando se borra una fila. Con algunos driver JDBC, una línea borrada es eliminada y ya no es visible en una hoja de resultados. Algunos drives JDBC utilizan una fila en blanco en su lugar pone (un "hole") donde la fila borrada fuera utilizada. Si existe una fila en blanco en lugar de la fila borrada, se puede utilizar el método absolute con la posición original de la fila para mover el cursor, porque el número de filas en la hoja de resultados no ha cambiado. En cualquier caso, deberíamos recordar que los drivers JDBC manejan las eliminaciones de forma diferente. Por ejemplo, si escribimos una aplicación para ejecutarse con diferentes bases de datos, no deberíamos escribir código que dependiera de si hay una fila vacía en la hoja de resultados.

Ver los cambios en una Hoja de Resultados Si modificamos los datos en un objeto ResultSet, los cambios se harán visibles si lo cerramos y lo abrimos de nuevo. En otras palabras, si re-ejecutamos la misma petición, producirá una nueva hoja de resultados, basada en los datos actuales de la tabla. Esta hoja de resultados reflejara naturalmente los

cambios que hayamos hecho anteriormente. La cuestión es si podemos ver los cambios que hayamos realizado mientras el objeto ResultSet esté todavía abierto. (Generalmente, estaremos más interesados en los cambios hechos por otros). La respuesta depende del controlador de la base de datos, del driver, y del tipo del objeto ResultSet utilizado. Con un objeto ResultSet que sea TYPE_SCROLL_SENSITIVE, siempre podremos ver las actualizaciones que alguien haga en los valores de las columnas. Normalmente veremos inserciones y eliminaciones, pero la única forma de estar seguros es utilizar los métodos DatabaseMetaData que devuelven esta información. Podemos regular la extensión de que los cambios sean visibles aumentando o bajando el nivel de aislamiento de la transación con la base de datos. Por ejemplo, la siguiente línea de código, donde con es un objeto Connection activo, selecciona el nivel de aislamiento de la conexión a TRANSACTION_READ_COMMITTED. con.setTransactionIsolation(TRANSACTION_READ_COMMITTED);

Con este nivel de aislamiento, nuestro objeto ResultSet no mostrará ningún cambio antes de ser enviado, pero puede mostrar los cambios que podrían tener problemas de consistencia. Para permitir menores niveles de inconsistencia, podríamos subir el nive de aislamiento a TRANSACTION_REPEATABLE_READ. El problema es que a niveles más altos de aislamiento, el rendimiento se empobrece. Y siempre estamos limitados por lo que proporcionan las bases de datos y los drivers. En un objeto ResultSet que sea TYPE_SCROLL_INSENSITIVE, generalmente no podremos ver los cambios hechos mientras esté abierta. Algunos programadores utilizan sólo este tipo de objeto ResultSet porque quieren una vista consistente de los datos y no quieren ver los cambios hechos por otros. Se puede utilizar el método refreshRow para obtener los últimos valores de una fila en la base de datos. Este método puede utilizar muchos recursos, especialmente si el controlador de la base de datos devuelve múltiples filas cada vez que se llama a refreshRow. De todas formas, puede utilizarse cuando es crítico tener los últimos datos. Incluso aunque una hoja de resultados sea sensible y los cambios sean visibles, una aplicación no podría ver siempre los últimos cambios si el driver recupera varias filas a la vez y las almacena. Por eso, utilizar el método refreshRow es el único método para asegurarnos que estamos viendo los últimos datos. El siguiente código ilustra cómo una aplicación podría utilizar el método refreshRow cuando es absolutamente crítico ver los últimos valores. Observa que la hoja se resultados debería ser sensible; si queremos utilizar el método refreshRow con un objeto ResultSet que sea TYPE_SCROLL_INSENSITIVE, no hará nada. (La urgencia de obtener los últimos datos es bastante improvable en la tabla COFFEES, pero la fortuna de un inversor, depende de conocer los últimos precios en la amplia fluctuación del mercado del café. O, por ejemplo, querriamos asegurarnos de que el nuestro asiento reservado en el avión de regreso todavía está disponible). Statement stmt = con.createStatement( ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); ResultSet uprs = stmt.executeQuery(SELECT COF_NAME, PRICE FROM COFFEES); uprs.absolute(4); Float price1 = uprs.getFloat("PRICE"); // do something. . . uprs.absolute(4); uprs.refreshRow(); Float price2 = uprs.getFloat("PRICE"); if (price2 > price1) { // do something. . . }

Hacer Actualizaciones Por Lotes Una actualización por lotes es un conjunto de varias sentencias de actualización que son enviadas a la base de datos para ser procesadas como un lote. Enviar múltiples sentencias de actualizaicón juntas a la base de datos puede, en algunas situaciones, ser mucho más eficiente que enviar cada sentencia separadamente. Esta posibilidad de enviar actualizaciones como una unidad, referida como facilidad de actualización por lotes, es una de las características proporcionadas por el API JDBC 2.0.

Utilizar Objetos Statement para Actualizaciones por Lotes En el API JDBC 1.0, los objetos Statement enviaban actualizaciones a la base de datos individualmente con el método executeUpdate. Se pueden enviar varias sentencias executeUpdate en la misma transación, pero aunque son enviadas como una unidad, son procesadas individualmente. Los interfaces derivados de Statement, PreparedStatement y CallableStatement, tienen las mismas capacidades, utilizando sus propias versiones de executeUpdate. Con el API JDBC 2.0, los objetos Statement, PreparedStatement y CallableStatement tienen la habilidad de mantener una lista de comandos que pueden ser enviados juntos como un lote. Ellos son creados con una lista asociada, que inicialmente está vacía. Se pueden añadir comandos SQL a esta lista con el método addBatch, y podemos vaciar la lista con el método clearBatch. Todos los comandos de la lista se envían a la base de datos con el método executeBatch. Ahora veamos como funcionan estos métodos. Supongamos que nuestro propietario del café quiere traer nuevos cafés. Ha determinado que su mejor fuente es uno de sus actuales suministradores, Superior Coffee, y quiere añadir cuatro nuevos cafés a la tabla COFFEES. Cómo sólo va a insertar cuatro nuevas filas, la actualización por lotes podría no aumentar el rendimiento significativamente, pero es una buena oportunidad para demostrar la actualización por lotes. Recordamos que la tabla COFFEES tiene cinco columnas: COF_NAME del tipo VARCHAR(32), SUP_ID del tipo INTEGER, PRICE del tipo FLOAT, SALES del tipo INTEGER, y TOTAL del tipo INTEGER. Cada fila insertada tendrá valores para las cinco columnas en orden. El código para insertar una nueva fila como un lote se podría parecer a esto. con.setAutoCommit(false); Statement stmt = con.createStatement(); stmt.addBatch("INSERT INTO COFFEES" + "VALUES('Amaretto', 49, 9.99, stmt.addBatch("INSERT INTO COFFEES" + "VALUES('Hazelnut', 49, 9.99, stmt.addBatch("INSERT INTO COFFEES" + "VALUES('Amaretto_decaf', 49, stmt.addBatch("INSERT INTO COFFEES" + "VALUES('Hazelnut_decaf', 49, int [] updateCounts = stmt.executeBatch();

0, 0)"); 0, 0)"); 10.99, 0, 0)"); 10.99, 0, 0)");

Ahora exáminemos el código línea por línea. con.setAutoCommit(false);

Esta línea desactiva el modo auto-commit para el objeto Connection, con para que la transación no sea enviada o anulada automáticamente cuando se llame al método executeBatch. (Si no recuerdas qué era una transación, deberías revisar la página Transaciones). Para permitir un manejo de errores correcto, también deberíamos actualizar el modo auto-commit antes de empezar una actualización por lotes. Statement stmt = con.createStatement();

Esta línea de código crea el objeto Statement, stmt. Al igual que todos los nuevos objetos Statement recien creados, stmt tiene una lista de comandos asociados y ésta lista está vacía.

stmt.addBatch("INSERT INTO COFFEES" + "VALUES('Amaretto', 49, 9.99, stmt.addBatch("INSERT INTO COFFEES" + "VALUES('Hazelnut', 49, 9.99, stmt.addBatch("INSERT INTO COFFEES" + "VALUES('Amaretto_decaf', 49, stmt.addBatch("INSERT INTO COFFEES" + "VALUES('Hazelnut_decaf', 49,

0, 0)"); 0, 0)"); 10.99, 0, 0)"); 10.99, 0, 0)");

Cada una de estas líneas de código añade un comando a la lista de comandos asociados con stmt. Estos comandos son todos sentencias INSERT INTO, cada una añade una nueva fila que consiste en cinco valores de columna. Los valores para las columnas COF_NAME y PRICE se explican a sí mismos. el segundo valor de cada fila es 49 porque es el número de identificación del suministrador, Superior Coffee. Los últimos dos valores, las entradas para las columnas SALES y TOTAL, todas empiezan siendo cero porque todavía no se ha vendido nada. (SALES es el número de libras del café de esa columna vendidas la semana actual; y TOTAL es el número total de libras vendidas de este café). int [] updateCounts = stmt.executeBatch();

En esta línea, stmt envía a la base de datos los cuatro comandos SQL que fueron añadidos a su lista de comandos para que sean ejecutados como un lote. Observa que stmt utiliza el método executeBatch para el lote de inserciones, no el método executeUpdate, que envía sólo un comando y devuelve una sóla cuenta de actualización. El controlador de la base de datos ejecuta los comandos en el orden en que fueron añadidos a la lista de comandos, por eso primero añadirá la fila de valores apra Amaretto, luego añade la fila de Hazelnut, luego Amaretto decaf, y finalmente Hazelnut decaf. Si los cuatro comandos se ejecutan satisfactoriamente, el controlador de la base de datos devolverá una cuenta de actualización para cada comando en el orden en que fue ejecutado. Las cuentas de actualización, indicarán cuántas líneas se vieron afectadas por cada comando, y se almacenan en el array de enteros updateCounts. En este punto, updateCounts debería contener cuatro elementos del tipo int. En este caso, cada uno de ellos será 1 porque una inserción afecta a un fila. La lista de comandos asociados con stmt ahora estará vacía porque los cuatro comandos añadidos anteriormente fueron enviados a la base de datos cuando stmt llamó al método executeBatch. En cualquier momento podemos vaciar la lista de comandos con el método clearBatch.

Excepciones en las Actualizaciones por Lotes Existen dos excepciones que pueden ser lanzadas durante una actualización por lotes: SQLException y BatchUpdateException. Todos los métodos del API JDBC lanzarán un objeto SQLException si existe algún problema de acceso a la base de datos. Además, el método executeBatch lanzará una SQLException si hemos utilizado el método addBatch para añadir un comando y devuelve una hoja de resultados al lote de comandos que está siendo ejecutado. Típicamente una petición (una sentencia SELECT) devolverá una hoja de resultados, pero algunos métodos, como algunos de DatabaseMetaData pueden devolver una hoja de datos. Sólo utilizar el método addBatch para añadir un comando que produce una hoja de resultados no hace que se lance una excepción. No hay problema mientras el comando está siendo situado en la lista de comandos de un objeto Statenment. Pero habrá problemas cuando el método executeBatch envíe el lote al controlador de la base de datos para ejecutarlo. Cuando se ejecuta cada comando, debe devolver una cuenta de actualización que pueda ser añadida al array de cuentas de actualización devuelto por el método executeBatch. Intentar poner una hoja de resultados en un array de cuentas de actualización causará un error y hará que executeBatch lance una SQLException. En otras palabras, sólo los comandos que devuelven cuentas de actualización (comandos como INSERT INTO,

UPDATE, DELETE, CREATE TABLE, DROP TABLE, ALTER TABLE, etc) pueden ser ejecutados como un lote con el método executeBatch. Si no se lanzó una SQLException, sabremos que no hubo problemas de acceso y que todos los comandos produjeron cuentas de actualización. Si uno de los comandos no puede ser ejecutado por alguna razón, el método executeBatch lanzará una BatchUpdateException. Además de la información que tienen todas las excepciones, este excepción contiene un array de cuentas de actualización para los comandos que se ejecutaron satisfactoriamente antes de que se lanzara la excepción. Cómo las cuentas de actualización están en el mismo orden que los comandos que las produjeron, podremos decir cuántos comandos fueron ejecutados y cuáles fueron. BatchUpdateException desciende de SQLException. Esto significa que utiliza todos los métodos disponibles en un objeto SQLException. El siguiente fragmento de código imprime la información de SQLException y las cuentas de actualización contenidas en un objeto BatchUpdateException. Como getUpdateCounts devuelve un array de int, utiliza un bucle for para imprimir todas las cuentas de actualización. try {

// make some updates } catch(BatchUpdateException b) { System.err.println("SQLException: " + b.getMessage()); System.err.println("SQLState: " + b.getSQLState()); System.err.println("Message: " + b.getMessage()); System.err.println("Vendor: " + b.getErrorCode()); System.err.print("Update counts: "); int [] updateCounts = b.getUpdateCounts(); for (int i = 0; i < updateCounts.length; i++) { System.err.print(updateCounts[i] + " "); } }

Usar Tipos de Datos de SQL3 Los tipos de datos comunmente referidos como tipos SQL3 son los nuevos tipos de datos que están siendo adoptados en la nueva versión del estándard ANSI/ISO de SQL. El JDBC 2.0 proporciona interfaces que representan un mapeado de estos tipos de datos SQL3 dentro del lenguaje Java. Con estos nuevos interfaces, podremos trabajar con tipos SQL3 igual que con otros tipos. Los nuevos tipos SQL3 le dan a una base de datos relacional más flexibilidad en lo que pueden utiizar como tipos para una columna de una tabla. Por ejemplo, una columna podría ahora almacenar el nuevo tipo de dato BLOB (Binary Large Object), que puede almacenar grandes cantidades de datos como una fila de bytes. Una columna también puede ser del tipo CLOB (Character Large Object), que es capaz de almacenar grandes cantidades de datos en formato caracter. El nuevo tipo ARRAY hace posible el uso de un array como un valor de columna. Incluso las nuevas estructuras de tiposdefinidos-por-el-usuario (UDTs) de SQL pueden almacenarse como valores de columna. La siguiente lista tiene los interfaces del JDBC 2.0 que mapean los tipos SQL3. Los explicaremos en más detalle más adelante. ● ● ● ● ●

Un ejemplar Blob mapea un BLOB de SQL. Un ejemplar Clob mapea un CLOB de SQL. Un ejempar Array mapea un ARRAY de SQL. Un ejemplar Struct mapea un tipo Estructurado de SQL. Un ejemplar Ref mapea un REF de SQL.

Uitlizar tipos de datos SQL3 Se recuperan, almacenan, y actualizan tipos de datos SQL3 de la misma forma que los otros tipos. Se

utilizan los métodos ResultSet.getXXX o CallableStatement.getXXX para recuperarlos, los métodos PreparedStatement.setXXXpara almacenarlos y updateXXX para actualizarlos. Probablemente el 90% de las operacuines realizadas con tipos SQL3 implican el uso de los métodos getXXX, setXXX, y updateXXX. La siguiente tabla muestra qué métodos utilizar. Tipo SQL3 Método getXXX Método setXXX Método updateXXX BLOB getBlob setBlob updateBlob CLOB getClob setClob updateClob ARRAY getArray setArray updateArray Tipo Structured getObject setObject updateObject REF(Tipo getRef setRef updateRef Structured) Por ejemplo, el siguiente fragmento de código recupera un valor ARRAY de SQL. Para este ejemplo, la columna SCORES de la tabla STUDENTS contiene valores del tipo ARRAY. La variable stmt es un objeto Statement. ResultSet rs = stmt.executeQuery("SELECT SCORES FROM STUDENTS 2238"); rs.next(); Array scores = rs.getArray("SCORES");

WHERE ID =

La variable scores es un puntero lógico al objeto ARRAY de SQL almacenado en la tabla STUDENTS en la fila del estudiante 2238. Si queremos almacenar un valor en la base de datos, utilizamos el método setXXX apropiado. Por ejemplo, el siguiente fragmento de código, en el que rs es un objeto ResultSet, almacena un objeto Clob. Clob notes = rs.getClob("NOTES"); PreparedStatement pstmt = con.prepareStatement("UPDATE MARKETS SET COMMENTS = ? WHERE SALES < 1000000", ResultSet.TYPE_SCROLL_INSENSITIVE , ResultSet.CONCUR_UPDATABLE); pstmt.setClob(1, notes);

Este código configura notes como el primer parámetro de la sentencia de actualización que está siendo enviada a la base de datos. El valor CLOB designado por notes será almacenado en la tabla MARKETS en la columna COMMENTS en cada columna en que el valor de la columna SALES sea menor de un millón.

Objetos Blob, Clob, y Array Una característica importante sobre los objetos Blob, Clob, y Array es que se pueden manipular sin tener que traer todos los datos desde el servidor de la base de datos a nuestra máquina cliente. Un ejemplar de cualquiera de esos tipos es realmente un puntero lógico al objeto en la base de datos que representa el ejemplar. Como los objetos SQL BLOB, CLOB, o ARRAY pueden ser muy grandes, esta característica puede aumentar drásticamente el rendimiento. Se pueden utilizar los comandos SQL y los API JDBC 1.0 y 2.0 con objetos Blob, Clob, y Array como si estuvieramos operando realmente con el objeto de la base de datos. Sin embargo, si queremos trabajar con cualquiera de ellos como un objeto Java, necesitamos traer todos los datos al cliente, con lo que la referencia se materializa en el objeto. Por ejemplo, si queremos utilizar un ARRAY de SQL en una aplicación como si fuera un array Java, necesitamos materializar el objeto ARRAY en el cliente para convertirlo en un array de Java. Entonces podremos utilizar los métodos de arrays de Java

para operar con los elementos del array. Todos los interfaces Blob, Clob, y Array tienen métodos para materializar los objetos que representan.

Tipos Struct y Distinct Los tipos estructurados y distinct de SQL son dos tipos de datos que el usuario puede definir. Normalmente nos referimos a ellos como UDTs (user-defined types), y se crean con un sentencia CREATE TYPE de SQL. Un tipo estructurado de SQL se parece a los tipos estructurados de Java en que tienen miembros, llamados atributos, que puede ser cualquier tipo de datos. De echo, un atributo podría ser a su vez un tipo estructurado. Aquí tienes un ejemjplo de una simple definición de un nuevo tipo de dato SQL. CREATE TYPE PLANE_POINT ( X FLOAT, Y FLOAT )

Al contrario que los objetos Blob, Clob, y Array, un objeto Struct contiene valores para cada uno de los atributos del tipo estructurado de SQL y no es sólo un puntero lógico al objeto en la base de datos. Por ejemplo, supongamos que un objeto PLANE_POINT está almacenado en la columna POINTS de la tabla PRICES. ResultSet rs = stmt.executeQuery("SELECT POINTS FROM PRICES 3000.00"); while (rs.next()) { Struct point = (Struct)rs.getObject("POINTS"); // do something with point }

WHERE PRICE >

Si el objeto PLANE_POINT recuperado tiene un valor X de 3 y un valor Y de -5, el objeto Struct, point contendrá los valores 3 y -5. Podríamos haber observador que Struct es el único tipo que no tiene métodos getXXX y setXXX con su nombre como XXX. Debemos utilizar getObject y setObject con ejemplares Struct. Esto significa que cuando recuperemos un valor utilizando el método getObject, obtendremos un Object Java que podremos forzar a Struct, como se ha hecho en el ejemplo de código anterior. El segundo tipo SQL que un usuario puede definir con una sentencia CREATE TYPE de SQL es un tipo Distinct. Este tipo se parece al typedef de C o C++ en que es un tipo basado en un tipo existente. Aquí tenemos un ejemplo de creacción de un tipo distinct. CREATE TYPE MONEY AS NUMERIC(10, 2)

Esta definición crea un nuevo tipo llamado MONEY, que es un número del tipo NUMERIC que siempre está en base 10 con dos dígitos después de la coma decimal. MONEY es ahora un tipo de datos en el esquema en el que fue definido, y podemos almacenar ejemplares de MONEY en una tabla que tenga una columna del tipo MONEY. Un tipo distinct SQL se mapea en Java al tipo en el que se mapearía le tipo original. Por ejemplo, NUMERIC mapea a java.math.BigDecimal, porque el tipo MONEY mapea a java.math.BigDecimal. Para recuperar un objeto MONEY, utilizamos ResultSet.getBigDecimal o CallableStatement.getBigDecimal; para almacenarlo utilizamos PreparedStatement.setBigDecimal.

Características Avanzadas de SQL3 Algunos aspectos del trabajo con los tipos SQL3 pueden parecer bastante complejos. Mencionamos

alguna de las características más avanzadas para que las conozcas, pero una explicación profunda no es apropiada para un tutorial básico. El interface Struct es el mapeo estándard para un tipo estructurado de SQL. Si queremos trabajar fácilmente con un tipo estructurado de Java, podemos mapearlo a una clae Java. El tipo structurado se convierte en una clase, y sus atributos en campos. No tenemos porque utilizar un mapeado personalizado, pero normalmente es más conveniente. Algunas veces podríamos querer trabajar con un puntero lógico a un tipo estructurado de SQL en vez de con todos los valores contenidos en el propio tipo, Esto podría suceder, por ejemplo, si el tipo estructurado tiene muchos atributos o si los atributos son muy grandes. Para referenciar un tipo estructurado, podemos declarar un tipo REF de SQL que represente un tipo estructurado particular. Un objeto REF de SQL se mapea en un objeto Ref de Java, y podemos operar con ella como si lo hicieramos con el objeto del tipo estructurado al que representa.

Características de Extensión Estándar El paquete javax.sql es una extensión estándard al lenguaje Java. La expecificación final no está terminada pero podemos ver algunas de las funcionalidades básicas que proporcionará. Estas son las características de la extensión estándard del JDBC 2.0. Rowsets Este objeto encapsula un conjunto de filas de una hoja de resultados y podría mantener abierta la conexión con la base de datos o desconectarse de la fuente. Un rowset es un componente JavaBean; puede ser creado en el momento del diseño y puede utilizarse con otros JavaBeans en una herramienta visual. JNDI para Nombrar Bases de Datos El interface JNDI (Nombrado y Direccionado) de Java hace posible conectar a una base de datos utilizando un nombre lógico en lugar de codificar un nombre de base de datos y de driver. Connection Pooling Un Connection Pool es un caché para conexiones frecuentes que pueden ser utilizadas y reutilizadas, esto recorta la sobrecarga de crear y destruir conexiones a bases de datos. Soporte de Transación Distribuida Este soporte permite al driver JDBC soportar el protocolo estándard de dos-fases utilizados en el API Java Transaction (JTA). Esta caracterísitca facilita el uso de las funcionalidades JDBC en componentes JavaBeans de Enterprise.

BIBLIOGRAFÍA Título

Autor

Editorial

Java 2

Sun Microsystems

Sun Microsystems

Java Programmer's Reference

Grant Palmer

Wrox

Café Programming Front Runner

Friedel, Kereivsky, Potts, Rodley

Paraninfo

Como se hace con JAVA

Stephen D. Lockwood Madu Siddalingaiah

Infor Books

Programación de Juegos en Java

Joel Fan Eric Ries Calin Tenitchi

ANAYA

JDBC

Sun Microsystmes

Sun Microsystmes

Core JAVA 2 Advanced Features

Cay S. Horstmann

Prentice Hall

Gary Cornell

Lihat lebih banyak...

Comentarios

Copyright © 2017 DATOSPDF Inc.