Sistemas Informáticos I (Ingeniería Informática. UNED) 1. Introducción a los objetos 1.1 Todo objeto tiene una interfaz. La creación de tipos abstractos de datos (clases) es un concepto fundamental en la POO. Es posible la creación de variables de un tipo (objeto o instancia) y manipularlas (mensajes). Los miembros de cada clase comparten algunos rasgos comunes. Siempre que aparezca la palabra clave “tipo” (type) puede sustituirse por la “clase” (class) y viceversa. Las peticiones que se puedan hacer a un objeto se encuentran definidas en su interfaz, que viene determinada por el tipo de objeto. La interfaz establece qué peticiones pueden hacerse a un objeto en particular. 1.2 La implementación oculta. La creación de clases debe ser de forma que únicamente se exponga lo necesario, ocultando el resto. Este enfoque permite la modificación de la parte oculta sin preocuparse de las implicaciones de la modificación. Otro motivo es evitar las manipulaciones indebidas de los métodos de las clases. Java usa tres palabras clave para establecer los límites en una clase, estos modificadores de acceso determinan quién puede usar las definiciones a las que preceden: public: Las definiciones están disponibles para todo el mundo. private: Sólo el creador del tipo puede acceder a esas definiciones. Si se intenta acceder se obtiene un error en tiempo de compilación. protected: Actúa como privada con la diferencia de que una clase heredada tiene acceso a miembros protegidos pero no a los privados. Cuando no se especifica el modo tiene un acceso por defecto, se suele denominar acceso amistoso (friendly) ya que las clases pueden acceder a los miembros amigos de otras clases que estén en el mismo paquete (package), fuera del paquete, estos miembros amigos se convierten en private. 1.3 Reutilizar la implementación La manera más simple de reutilizar una clase es usar un objeto de esta clase, también es posible ubicar un objeto de esa clase dentro de otra clase, creación de un objeto miembro. La nueva clase puede construirse a partir de un número indefinido de otros objetos, de igual o distinto tipo, en cualquier combinación necesaria para lograr la funcionalidad deseada dentro de la nueva clase. Este concepto es la composición (agregación), que se representa mediante la relación “esparte-de”. La composición conlleva una gran carga de flexibilidad. Los objetos miembros de la nueva clase suelen ser privados. Esto permite cambiar los miembros en tiempo de ejecución, para así cambiar de manera dinámica el comportamiento de un programa. La herencia no es tan flexible, ya que el compilador debe emplazar restricciones de tiempo de compilación en las clases creadas por herencia. Es recomendable intentar el uso de la composición (simple y sencilla) que aplicar herencia. 1.4 Herencia: Reutilizar la interfaz. La herencia expresa la semejanza entre tipos haciendo uso del concepto de tipos base y tipos derivados. Un tipo base contiene todas las características y comportamientos que comparten los tipos que de él se derivan. El uso de la herencia permite construir una jerarquía de tipos que expresa el problema en términos de los propios tipos. De esta forma el modelo principal lo constituye la jerarquía de tipos, de manera que se puede ir directamente de la descripción del sistema en el mundo real a la descripción del sistema en código. Al heredar a partir de un tipo existente, se crea un nuevo tipo. Este nuevo tipo contiene no sólo los miembros del tipo existente (aunque los datos private estén ocultos e inaccesibles) sino que duplica la interfaz de la clase base. Hay dos formas de diferenciar el comportamiento de la clase derivada de la clase base original. El primero es añadir nuevas funciones. La segunda (y más importante) es variar el comportamiento de una función ya existente en la clase base. A esto se le llama redefinición (anulación o superposición) de la función. Para ello simplemente se crea una nueva definición de la función dentro de la clase derivada. 1.4.1 La relación es-un frente a la relación es-como-un El principio de sustitución dice que la filosofía a seguir entre clases bases y derivadas es sólo superponer las funciones de la clase base (un círculo es un polígono), es lo que se conoce como relación es-un, en esta relación es posible sustituir un objeto de la clase derivada por otro de la clase base. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 1 Sistemas Informáticos I (Ingeniería Informática. UNED) Existen casos en los que es necesario añadir nuevos elementos a la interfaz del tipo derivado, extendiendo así la interfaz y cuando un nuevo tipo, en este caso la sustitución por el tipo base no es perfecto y esta relación se define escomo-un. 1.5 Objetos intercambiables con polimorfismo Se puede desear tratar un objeto como su tipo base, con lo que el código no dependerá de tipos específicos. En los compiladores no-OO una llamada a una función crea una ligadura temprana, es decir, genera una llamada a una función con nombre específico y el montador resuelve esta llamada a la dirección absoluta del código a ejecutar. En POO, el programa no puede determinar la dirección del código hasta tiempo de ejecución, utilizándose otro esquema, el de ligadura tardía, al enviar un mensaje a un objeto, no se determina el código invocado hasta el tiempo de ejecución. El compilador se asegura de que la función exista y hace la comprobación de tipos de los argumentos y del valor de retorno, pero se desconoce el código exacto a ejecutar. El proceso de tratar un tipo derivado como si fuera el tipo base se le llama conversión de tipos (moldeado) hacia arriba (casting), es decir, moverse hacia arriba por el diagrama de herencias. 1.5.1 Clases base abstractas e interfaces Puede ser deseable que la clase base sólo presente una interfaz para sus clases derivadas, es decir, que no se creen objetos de ella, sino sólo de las derivadas. Esto se consigue convirtiendo esa clase en abstracta con la palabra clave abstrac. Si se intenta construir un objeto de una clase abstracta el compilador lo evita. También se puede utilizar la palabra clave abstrac para describir un método que aún no ha podido ser implementando, un método abstracto sólo puede existir dentro de una clase abstracta. Cuando se hereda la clase, debe implementarse el método o de lo contrario la clase heredada se convierte en abstracta. La creación de métodos abstractos permiten poner un método en una interfaz sin verse forzado a proporcionar un fragmento de código para ese método. La palabra clave interface toma el concepto de clase abstracta, evitando totalmente las definiciones de funciones, es una herramienta muy útil al proporcionar la separación perfecta entre interfaz e implementación. 1.6 Localización de objetos y longevidad Un factor importante en la POO es la manera de crear y destruir objetos. Java utiliza el enfoque de crear objetos dinámicamente en el montículo (heap). En este enfoque, no es necesario conocer hasta el tiempo de ejecución el número de objetos, longevidad o tipo. Si se necesita un nuevo objeto simplemente se construye en el montículo. Como el almacenamiento se gestiona dinámicamente, en tiempo de ejecución, la cantidad de tiempo necesaria para asignar espacio de almacenamiento en el montículo es mayor que el necesario en la pila. Sin embargo la flexibilidad es esencial para resolver en general los problemas de programación. Cada vez que se desea crear un objeto se usa la palabra clave new para construir una instancia dinámica de ese objeto. El aspecto de la longevidad se resuelve, en el caso de la pila, por el compilador que determina cuánto dura cada objeto y puede destruirlo cuando no es necesario. En el enfoque del montículo el compilador no tiene conocimiento de su duración, lo proporciona un recolector de basura que descubre automáticamente cuándo se ha dejado de utilizar un objeto, y puede ser destruido. Esto disminuye la cantidad de código y ofrece un nivel superior de seguridad. 1.6.1 Colecciones e iteradores. En un problema concreto se pueden desconocer el número y duración de objetos para solucionarlo, y en consecuencia cómo almacenarlos. En la POO se soluciona creando otro tipo de objetos que tiene referencias a otros objetos. Además este nuevo objeto, llamado contenedor, se expandirá a sí mismo cuando sea necesario para albergar todo lo que se coloque en su interior. Todos los contenedores tienen alguna manera de introducir y extraer cosas, pero la extracción puede ser problemática porque una función de selección única suele ser restrictiva. La solución es un iterador, que es un objeto cuyo trabajo es seleccionar los elementos de dentro de un contenedor y presentárselos al usuario. Como clase también proporciona un cierto nivel de abstracción, que puede ser usada para separar los detalles del contenedor del código al que éste esté accediendo. El contenedor se abstrae a través del iterador hasta convertirse en una secuencia, que se puede recorrer con el iterador, sin preocuparse de la estructura subyacente (Arraylist, LinkedList, Stack...). Java2 cuenta con un iterador denominado Iterator (en Java 1.0 y 1.1 Enumeration). Página 2 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) 1.6.2 Jerarquía de raíz única. En Java todas las clases heredan, en última instancia, de una única clase base, Object. De esta forma todos los objetos tienen una interfaz común, por lo que en última instancia son del mismo tipo. Puede garantizarse que todos los objetos de una jerarquía de raíz única tienen cierta funcionalidad y junto a la creación de todos los objetos en el montículo simplifica el paso de argumentos. Además simplifica la implementación del recolector de basura ya que su soporte se puede instalar en la clase base y podrá enviar los mensajes apropiados a todos los objetos del sistema. Dado que está garantizado que en tiempo de ejecución la información de tipos está en todos los objetos, jamás será imposible encontrar un objeto cuyo tipo no pueda ser determinado, esto es importante en operaciones a nivel de sistema y por la flexibilidad que proporciona al programar. 1.6.3 Bibliotecas de colecciones Para utilizar un contenedor, basta con añadirle referencias a objetos y luego preguntar por ellas. Dado que el contenedor sólo guarda objetos de tipo Object, al añadirle una referencia se hace un moldeado hacia arriba a Object perdiendo la identidad. Al recuperarlo, se obtiene una referencia a Object y no una referencia al tipo introducido. Para recuperar la interfaz del objeto introducido en el contenedor se debe hacer un moldeado hacia abajo, a un tipo erróneo, se mostrará un error en tiempo de ejecución, exception. Para solucionar el moldeado hacia abajo y las comprobaciones en tiempo de ejecución, se crean los tipos parametrizados, que son clases que el compilador puede adaptar automáticamente para que trabajen con tipos determinados. Dado que C++ no tiene jerarquía de raíz única es la solución que adopta (template). Java no los tienen ya que logra lo mismo explotando la unicidad de raíz de su jerarquía, aunque lo hace de manera complicada. 1.7 Multihilo A veces, es necesario hacer uso de las interrupciones para el manejo de tareas críticas en el tiempo, pero hay una gran cantidad de problemas en los que simplemente se intenta dividir un problema en fragmentos de código que pueden ser ejecutados por separado, de manera que se logra un menor tiempo de respuesta para todo el programa en general. Estos fragmentos de código que pueden ser ejecutados por separado, se denominan hilos y el concepto general multihilo. Los hilos son una herramienta para facilitar la planificación de un monoprocesador. Si el sistema operativo soporta múltiples procesadores, es posible asignar cada hilo a un procesador distinto de manera que los hilos se ejecuten verdaderamente en paralelo. Uno de los aspectos más destacables es que el programa no tiene de ocuparse de ello. Para los recursos compartidos por varios hilos se pueden establecer bloqueos a los accesos. El hilo de Java está incluido en el propio lenguaje y se soporta a nivel de objeto. El bloqueo de memoria de cualquier objeto se logra mediante la palabra clave synchronized. Otros tipos de recursos se deben bloquear por el programador creando un objeto que represente el bloqueo que todos los hilos deben comprobar antes de acceder al recurso. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 3 Sistemas Informáticos I (Ingeniería Informática. UNED) 2. Todo es un objeto 2.1 Los objetos se manipulan mediante referencias En Java, todo se trata como un objeto, utilizando una única sintaxis consistente, que se utiliza en todas partes. Aunque se trata todo como un objeto, el identificador que se manipula es una referencia a un objeto. Es la referencia, la que actúa sobre el objeto. La referencia puede existir por sí misma sin necesidad de que exista un objeto. De esta forma, si se desea tener una palabra o frase, se crea una referencia String : | String s; Esta sentencia sólo crea la referencia, no el objeto. Si se decide enviar un mensaje en este momento, se obtendrá un error en tiempo de ejecución porque s no se encuentra vinculado a nada. Una práctica más segura es iniciar la referencia en el mismo momento de su creación: | String s = “abcd“ Cuando se crea una nueva referencia, debe conectarse con un objeto. Java utiliza la palabra reservada new, para crear un objeto nuevo. | String s = new String (“abcd“) 2.2 Donde reside el almacenamiento? Hay seis lugares diferentes donde almacenar información: 1. 2. 3. 4. 5. 6. Página 4 Registros. Elemento de almacenamiento más rápido. Número limitado de ellos. Los asigna el compilador en función de sus necesidades. No se tiene control directo sobre ellos. La pila. Reside en la memoria RAM, tiene soporte directo del procesador a través del puntero de pila, que se mueve hacia abajo para crear más memoria y hacia arriba para liberarla. Este método es una manera rápida y eficiente de asignar espacio. El compilador debe conocer, mientras está creando el programa, el tamaño exacto y la vida de todos los datos almacenados en la pila. El uso de la pila pone limitaciones de flexibilidad a los programas. El Montículo. Espacio de memoria de propósito general, en el que residen los objetos Java. En el montículo, a diferencia de la pila, el compilador no necesita conocer cuanto espacio de almacenamiento se necesita asignar. Proporciona gran flexibilidad. Almacenamiento estático: (con una ubicación/posición fija pero en RAM). Contiene datos que están disponibles durante todo el tiempo que se esté ejecutando un programa. Se utiliza static para especificar que un elemento particular de un objeto sea estático, pero los objetos nunca se sitúan en el espacio de almacenamiento estático. Almacenamiento constante. Los valores constantes se suelen ubicar directamente en el código del programa. En ocasiones las constantes suelen ser acordonadas por si mismas, de forma que puedan ser ubicadas ocasionalmente en ROM. Almacenamiento no-RAM. Los dos ejemplos principales son: objeto de flujo de datos (stream), que se convierte en flujo o corrientes de bits, generalmente para ser enviados a otra máquina y los objetos persistentes, que son ubicados en disco. Java proporciona soporte para persistencia ligera. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) 2.3 Los tipos primitivos Los tipos primitivos tienen un tratamiento especial, no es eficiente crear un objeto de este tipo con new ya que lo coloca en el montículo. Para estos tipos se crea una variable automática que no es una referencia. La variable guarda el valor y se coloca en la pila para que sea más eficiente. Java determina el tamaño de cada tipo primitivo, que no varía de una plataforma a otra: Tipo primitivo boolean Tamaño - Máximo - char 16 bits byte 8 bits Mínimo - Tipo envoltura Boolean 16 Unicode 0 Unicode 2 -1 Character -128 127 Byte 15 15 short 16 bits -2 +2 -1 Short int 32 bits - 231 +231 -1 Integer long 64 bits 63 -2 +2 -1 Long float 32 bits IEEE754 IEEE754 Float double 64 bits IEEE754 IEEE754 Double void - - - Void 63 Todos los tipos numéricos tienen signo. El tamaño de boolean no está explícitamente definido; sólo se especifica que debe ser capaz de tomar los valores True o False. Los tipos de datos primitivos tienen también clases “envoltura“, de forma que si se desea crear un objeto no primitivo en el montículo para representar ese tipo primitivo, se hace uso del envoltorio asociado. Por ejemplo: char c = 'x'; Character C = new Character(c); 2.4 Números de alta precisión Java incluye dos clases para llevar a cabo aritmética de alta precisión: BigInteger y BigDecimal. Aunque estos tipos vienen a encajar en la misma categoría de las clases “envoltorio“, ninguna de ellas tiene un tipo primitivo. Ambas clases tienen métodos que proporcionan operaciones análogas que se llevan a cabo con tipos primitivos (int y float), utilizando llamadas a métodos en lugar de operadores. Son más lentas que las primitivas. 2.5 Arrays en Java Java garantiza que los arrays estarán siempre inicializados y que no se podrá acceder más allá de su rango. La comprobación de rangos se resuelve con una pequeña sobrecarga de memoria en cada array, además de verificar el índice en tiempo de ejecución. Cuando se crea un array de objetos, se está creando realmente un array de referencias a los objetos, y cada una de estas se inicializa con un valor especial representado por la palabra clave null. Debe asignarse un objeto a cada referencia antes de usarla y si se intenta hacer uso de una referencia que aún vale null, se informará de que ha ocurrido un problema en tiempo de ejecución. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 5 Sistemas Informáticos I (Ingeniería Informática. UNED) 2.6 Ámbito Determina tanto la visibilidad como la vida de los nombres definidos dentro de éste ámbito. El ámbito se determina por la ubicación de las llaves {}, por ejemplo: { int x=12; /* sólo x disponible */ { int q=96; /* tanto x como q están disponibles */ } /* sólo x disponible */ /* q está fuera del ámbito o alcance */ } Una variable definida dentro de un ámbito solamente está disponible hasta que finalice su ámbito. La visibilidad en Java está implementada en modo de anidamiento descendente. Si una variable se define en un determinado nivel, y posteriormente en un nivel inferior, el compilador comunicará que la variable está duplicada. { int x; { int x; } Ilegal } 2.7 Ámbito de los objetos Los objetos no tienen la misma vida que los tipos primitivos. Cuando se crea un objeto con new, éste perdura hasta el final del ámbito, de manera que: { String s = new String(“un string“); /* fin del ámbito */ } La referencia s desaparece al final del ámbito, sin embargo, el objeto String al que apunta s sigue ocupando memoria. Java tiene un recolector de basura, que recorre todos los objetos que fueron creados con new y averigua cuales no serán referenciados más, liberando la memoria que estos ocupan. En Java no es necesario hacer ninguna reserva de memoria, simplemente se crean los objetos y cuando dejan de ser necesarios, desaparecen por sí mismos. Esto elimina el problema de programación denominado “agujero de memoria“, que se produce en otros lenguajes cuando no se destruyen explícitamente los objetos para liberar memoria. 2.8 Crear nuevos tipos de datos: clases Un nuevo tipo de objeto se define mediante la palabra clave class, que siempre va seguida del nombre del nuevo tipo, por ejemplo: class UnNombreDeTipo {/* Aquí va el cuerpo de la clase */} Esto introduce un nuevo tipo, siendo posible crear un objeto de este tipo haciendo uso de la palabra new, por ejemplo: UnNombreDeTipo u = new UnNombreDeTipo (); 2.9 Campos y métodos Cuando se define una nueva clase, es posible poner dos tipos de elementos: datos miembros (denominados campos) funciones miembros (denominados métodos). Un dato miembro es un objeto de cualquier tipo con el que te puedes comunicar a través de su referencia. También puede ser algún tipo primitivo. Si es una referencia a un objeto, hay que reinicializar esa referencia para conectarla a algún objeto real (utilizando new) en una función especial denominada constructor. Si se trata de un tipo primitivo es posible inicializarla directamente en el momento de definir la clase. Cada objeto mantiene el espacio de almacenamiento necesario para todos sus datos miembro; estos no son compartidos con otros objetos: class SoloDatos { Página 6 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) int i; float f; boolean b; } Es posible asignar valores a los datos miembro, pero primero es necesario hacer referencia al miembro del objeto. s.i = 47; s.f = 1.1f; f.b = False; También es posible que un objeto pueda contener otros datos que se quieran modificar. Para ello hay que seguir concatenando puntos por ejemplo: miAvion.TanqueIzquierdo.capacidad = 100; 2.10 Valores por defecto para los miembros primitivos Cuando un tipo de datos primitivo es un miembro de una clase, se garantiza que tenga un valor por defecto si no se inicializa: Tipo Primitivo Valor por defecto Tipo Primitivo Valor por defecto boolean false int 0 char u0000 (null) long 0L byte (byte) (0) float 0.0f short short (0) double 0.0d Esta garantía no se aplica a las variables “locales“ -- aquellas que no sean campos de clases. Si dentro de una función se tiene: int x; entonces x tomará algún valor arbitrario. El compilador Java advierte como error de esta circunstancia. 2.11 Métodos, parámetros y valores de retorno Los métodos en Java determinan los mensajes que puede recibir un objeto. Las partes fundamentales de un método son su nombre, sus parámetros, el tipo de retorno y el cuerpo. Su forma básica se asemeja a la siguiente: tipoRetorno nombreMetodo ( /* lista de parámetros */ ) { /* cuerpo del método */ } El tipo de retorno es el tipo de valor que surge del método tras ser invocado. La lista de parámetros indica los tipos y nombres de las informaciones que es necesario pasar en ese método. Cada método se identifica unívocamente mediante el nombre del método y la lista de parámetros. En Java, los métodos pueden crearse como parte de una clase. Es posible que un método pueda ser invocado sólo por un objeto, que llevará a cabo la llamada al método. Un método puede invocarse mediante la expresión completa de su ruta cualificada, o bien, asignando a una variable el valor de retorno de un objeto, en este caso, la declaración de la variable debe corresponderse con el valor que retorna el método. El acto de invocar a un método suele denominarse envío de un mensaje a un objeto. Por ejemplo en: int x = a.f(x); el mensaje es f() y el objeto es a. 2.12 Lista de parámetros La lista de parámetros de un método especifica la información que se le pasa. Esta información tiene forma de objeto y se debe especificar los tipos de objetos a pasar, y nombre a utilizar en cada uno. Debe tenerse en cuenta que realmente no se están manipulando directamente objetos, sino que se están pasando referencias. Un método puede devolver el tipo que se desee, pero si no se desea devolver nada, se debe indicar que el método devuelve void Cuando el tipo de retorno es void, se utiliza la palabra clave return sólo para salir del método, siendo innecesaria cuando se llega al final del mismo. Es posible salir de un método en cualquier punto, pero si se devuelve un valor de retorno distinto de void, el compilador obligará a devolver el tipo apropiado (mediante mensaje de error). J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 7 Sistemas Informáticos I (Ingeniería Informática. UNED) 2.13 Construcción de un programa en Java 2.13.1 Visibilidad de nombres Para producir un nombre no ambiguo para una biblioteca, el identificador utilizado se asemeja a los nombres de dominio de Internet, pero a la inversa, dado que es posible garantizar que estos sean únicos. Una vez que se da la vuelta al nombre de dominio, los nombres supuestamente representan directorios. Este mecanismo hace posible que todos sus ficheros residan automáticamente en sus propios espacios de nombres, y cada clase de un fichero debe tener un identificador único. 2.13.2 Utilización de otros componentes Cuando se desee utilizar una clase predefinida en un programa, el compilador debe saber donde encontrarla. Para ello, debe utilizarse la sentencia import. Esta palabra clave le dice al compilador que traiga un paquete, que es una biblioteca de clases. La mayoría de las veces se utilizan componentes de bibliotecas de Java estándar que vienen con el propio compilador, simplemente se hace: import java.util.ArrayList; para indicar al compilador que se desea utilizar la clase ArrayList de Java. Sin embargo, util contiene bastantes clases y se podría desear utilizar varias de ellas sin tener que declararlas todas. Esto se logra sencillamente utilizando el * que hace las veces de comodín, así: import java.util.*; importaría la colección de clases que forman parte de la librería útil. 2.17.3 La palabra clave static Al crear una clase se está describiendo qué apariencia tienen sus objetos y cómo se comportan. No se tiene nada hasta crear un objeto de esa clase con new, momento en el que se crea el espacio de almacenamiento y los métodos pasan a estar disponibles. Hay dos situaciones en las que este enfoque no es suficiente. Una es cuando se desea tener sólo un fragmento de espacio de almacenamiento para una parte concreta de datos, independientemente de cuántos objetos se crean, o incluso aunque no se cree ninguno. La otra es cuando se necesita un método que no esté asociado a ningún objeto particular de una clase. Puesto que los métodos estáticos no precisan de la creación de ningún objeto, no pueden acceder directamente a miembros de métodos no estáticos sin referirse al objeto con nombre del que son propiedad. Para declarar un dato o un miembro a nivel de clase como estático, basta con colocar la palabra clave static antes de la definición, así: class PruebaEstatica { static int i = 47; } Ahora, incluso si se construyen dos objetos de tipo PruebaEstatica, sólo habrá un espacio de almacenamiento para PruebaEstatica.i. Ambos objetos compartirán la misma i: PruebaEstatica st1 = new PruebaEstatica(); PruebaEstatica st2 = new PruebaEstatica(); En este momento, tanto st1.i como st2.i tienen el valor 47, puesto que se refieren al mismo espacio de memoria. Hay dos maneras de referirse a una variable estática. Es posible manejarlas a través de un objeto como en st2.i. También es posible referirse a ellas directamente a través de su nombre de clase, algo que no se puede hacer con miembros no estáticos. Ésta manera especial énfasis en la naturaleza estática de esa variable. PruebaEstatica.i ++; El operador ++ incrementa la variable. En ese momento tanto st1.i como st2.i valdrán 48. Algo similar se aplica a los métodos estáticos. Es posible referirse a ellos, bien a través de un objeto o bien con la sintaxis adicional NombreClase.metodo(). Un método estático se define de manera semejante: class FunEstatico { static void incr() {PruebaEstatica.i++} } Página 8 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) Mientras que static al ser aplicado a un miembro de datos, cambia definitivamente la manera de crear los datos (uno por cada clase en lugar de uno por cada objeto no estático), al aplicarse a un método no es tan drástico. Un uso importante de static para los métodos es permitir invocar a un método sin tener que crear un objeto. Esto, es esencial en la definición del método main(), que es el punto de entrada para la ejecución de la aplicación. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 9 Sistemas Informáticos I (Ingeniería Informática. UNED) 3 Controlar el flujo del programa. 3.1 Asignación La asignación de tipos primitivos es sencilla y directa, ya que el dato primitivo alberga el valor actual y no una referencia a un objeto. En la asignación de objetos, sin embargo, cuando se hace una asignación de un objeto a otro, se copia la referencia de un sitio a otro, es decir, si se hace C=D siendo ambos objetos, tanto C como D apuntarán al objeto que originalmente apuntaba D. Este fenómeno se llama uso de alias. El uso de alias también se produce en el paso de un objeto a un método, de forma que se pasa una referencia. 3.2 Operadores Relacionales(<,>,<=...), generan un resultado de tipo boolean, su uso para tipos primitivos es tal cual, pero para comparar los contenidos de dos objetos (referencias) se debe usar equals. Lógicos (AND &&, OR ||, NOT !), producen un valor lógico, pero sólo es posible aplicarlos a los valores boolean. La evaluación de operadores lógicos sufre cortocircuito, es decir, se evalúan solo hasta que es posible determinar sin ambigüedad la certeza o falsedad de toda expresión lógica. Operadores de bit. Permiten manipular bits individuales, los operadores son AND (&), OR(|), XOR(^), NOT(~); estos operadores son combinables con el de asignación (&=) a excepción del operador NOT. Los tipos boolean se pueden manipular con estos operadores, pero no se les puede aplicar el operador NOT. Operadores de desplazamiento. Estos operadores manipulan bits y sólo se pueden utilizar con tipos primitivos enteros. Al desplazar un char será convertido a int y el resultado será también un int. Sólo se desplazan los n-1 bits más significativos para evitar salirse de rango. Operador ternario if-else, tiene la forma exp-booleana ? Valor_0 : valor_1 Si la expresión booleana es cierta se evaluará valor_0, en caso contrario valor_1. Conversión. Normalmente Java convierte los tipos cuando cree apropiado, y la conversión es segura exceptuando las conversiones reductoras, en las que se corre el riesgo de perder información. Página 10 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) 4 Inicialización y limpieza 4.1 Inicialización garantizada con el constructor. En Java, el diseñador de cada clase puede garantizar que se inicialice cada objeto proporcionando un método especial llamado constructor. Si una clase tiene constructor, Java lo llama automáticamente al crear un objeto, quedando la inicialización garantizada. Para evitar conflictos el nombre del constructor es el mismo que el de la clase. Los constructores no tienen valor de retorno. El constructor por defecto es aquél que no tiene parámetros, y se utiliza para crear un objeto básico. Si se crea una clase sin constructores, el compilador siempre creará un constructor por defecto. Sin embargo si se define algún constructor (con o sin parámetros) el compilador no creará uno automáticamente. 4.2 Sobrecarga de métodos La sobrecarga de métodos es esencial para permitir que se use el mismo nombre de métodos con distintos tipos de parámetros y es esencial para los constructores. 4.2.1 Distinción de métodos sobrecargados. Orden de los parámetros Los tipos primitivos sino coinciden y se tiene un tipo de datos menor al parámetro del método, ese tipo se promociona (a excepción de char que se promociona a int). Si el parámetro es mayor se debe convertir explícitamente sino el compilador mostrará un mensaje de error. NO se pueden usar los tipos de valores de retorno para distinguir los métodos sobrecargados. 4.2.2 La palabra clave this. La palabra clave this, que sólo se puede usar dentro de un método, produce la referencia al objeto por el que se ha invocado al método. Si se invoca a un método de una clase dentro de un método de esa misma clase no es necesario usar this. Cuando se escriben varios constructores para una clase, hay veces en las que uno quisiera invocar a un constructor desde otro para evitar la duplicación de código. Esto se puede lograr utilizando this. Se puede invocar a un constructor utilizando this, pero no a dos. Además, la llamada al constructor debe ser la primera cosa que se haga o se obtendrá un mensaje de error del compilador. El compilador NO permite tampoco invocar a un constructor desde dentro de otro método que no sea un constructor. En los métodos estáticos no existe la posibilidad de usar this ya que no se puede invocar a métodos no estáticos desde dentro de métodos estáticos (al revés sí), y se puede invocar a un método estático de la propia clase sin objetos. 4.3 Limpieza. En Java existe el método finalize, que se puede definir en cada clase, y si el recolector de basura está preparado para liberar el espacio de almacenamiento utilizado por el objeto, primero invocará a finalize, y sólo recuperará la memoria del objeto durante la pasada del recolector de basura. Sin embargo finalize no es en sí un destructor, es decir, si se quiere hacer alguna cosa cuando finalice la vida del objeto se debe programar fuera de finalize, ya que puede que éste no llegue nunca a ejecutarse. La verdadera utilidad de finalize es la verificación de la condición de muerte de un objeto. 4.4 Inicialización de miembros Las variables definidas localmente en un método deben estar inicializadas, en caso contrario se obtendrá un error en tiempo de compilación. En el caso de atributos de tipo primitivo esto no es así y Java tiene una inicialización por defecto. Al definir una referencia a un objeto dentro de una clase sin inicializarla a un nuevo objeto, la referencia recibe el valor null. Si se define una clase y no se inicializa se obtiene una excepción. Es posible invocar a un método para proporcionar un valor de inicialización, pero si tiene parámetros no pueden ser de una clase que no esté inicializada, el compilador daría error. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 11 Sistemas Informáticos I (Ingeniería Informática. UNED) 4.5 Inicialización de constructores Dentro de una clase el orden de inicialización lo determina el orden en que se definen las variables dentro de la clase. Las definiciones pueden estar dispersas dentro de las definiciones de los métodos, pero las variables se inicializan antes de invocar a ningún método, incluido el constructor. Los objetos estáticos se inicializan antes que los no estáticos aunque sólo se hace una vez, es decir en la primera llamada. Al aplicar la herencia se inicializan las clases bases antes que los constructores de las derivadas, y se llama a la clase base una vez por cada clase derivada. Página 12 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) 5. Ocultar la implementación. 5.1 El paquete: la unidad de biblioteca. Un paquete es lo que se obtiene al utilizar la palabra clave import. Las importaciones proporcionan un mecanismo para gestionar los espacios de nombres. Existe la necesidad de controlar los espacios de nombres en Java, para tener la capacidad de crear un nombre completamente único sin importar las limitaciones de Internet. Al crear un fichero de código fuente en Java, se crea lo que comúnmente se denomina una unidad de compilación. Cada una de estas unidades tiene un nombre que acaba en .java, y dentro de la unidad de compilación sólo puede haber una única clase pública, sino el compilador dará error. El resto de clases de esa unidad de compilación quedan ocultas al exterior del paquete y son de apoyo para la clase pública principal. Al compilar un fichero .java, se obtiene un fichero de salida que tiene exactamente el mismo nombre con la extensión .class. Una biblioteca también es un conjunto de ficheros de clase. Cada fichero tiene una clase que es pública (aunque no es obligatorio), de forma que hay un componente por cada fichero. Si se desea que los componentes permanezcan unidos es necesario el uso de pakage al principio del archivo, en la primer línea que no sea un comentario, indicando que esa unidad de compilación es parte de un paquete. A la hora de buscar los archivos de clase el interprete de Java procede de la siguiente forma. En primer lugar encuentra la variable de entorno CLASSPATH, a partir de esta raíz, toma el nombre del paquete y reemplaza cada punto por una barra para generar un nombre relativo a la raíz CLASSPATH y busca los ficheros .class correspondientes. En caso de colisiones de nombres al utilizar import clase.*, el compilador se queja y se deberá especificar cual es la clase que se quiere usar. Es posible que exista alguna unidad de compilación sin ninguna clase pública, en este caso el archivo se puede llamar como se desee. 5.2 Modificadores de acceso en Java. Public: Las declaraciones del miembro estarán disponibles a todo el mundo Private: Nadie, puede tener acceso a ese miembro excepto a través de los métodos de esa clase. Protected: Como private pero dando acceso a los descendientes por herencia. Amistoso: Acceso por defecto, permite el acceso a las clases del mismo paquete. Si no hay un nombre de paquete explícito se usa el mismo directorio (paquete por defecto). Ejemplo de acceso a clase con constructor privado class sopa { private sopa () {} // permite la creación a través de un método estático. public static sopa hacersopa () { return new sopa(); } // Patrón devuelve una referencia private static sopa sp1=new sopa(); public static acceso () { return ps1; } public void f() {..} } public class Almuerzo { void prueba () { sopa priv_2 =sopa.hacerSopa();// el uso de sopa priv_2 = new sopa(); dará error sopa.acceso().f(); J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 13 Sistemas Informáticos I (Ingeniería Informática. UNED) 6. Reutilizando clases 6.1 Herencia En Java siempre se está haciendo herencia cuando se crea una clase, ya que al menos se herede explícitamente de otra clase, se hereda implícitamente de la clase raíz estándar de Java object. Para heredar se usa la palabra clase extends seguida del nombre de la clase base. Al hacer esto se tienen automáticamente todos los datos miembro y métodos de la clase base. Class derivada extends ClaseBase { ... Las clases derivadas y las clases base pueden tener un método main cada una, de hecho esto es interesante para poder realizar pruebas individuales de las clases, no importa que la clase no sea pública ya que el método main si lo es. Se debe de tener cuidado de hacer los métodos de la clase extendida públicos, sino la herencia sólo es posible en clases del mismo paquete (acceso amistoso). Es posible sobreescribir los métodos heredados, en este caso sí queremos referirnos al método original (el de la clase base) se utiliza la palabra clave super (super.metodo). También se pueden añadir nuevos métodos a la clase derivada. 6.1.1 Inicialización de la clase base. La herencia no es una simple copia de la interfaz de la clase base, cuando se crea un objeto de la clase derivada, éste contiene dentro de él un subobjeto de la clase base. Es esencial que este subobjeto de la clase base se inicialice correctamente, para ello Java inserta automáticamente llamadas al constructor de la clase base en el constructor de la clase derivada, de forma que se inicializan las clases bases antes que los constructores de la clase derivada puedan acceder a ellos. En el caso de que el constructor de la clase base tenga parámetros se debe escribir explícitamente la llamada al constructor de la clase base mediante super y la lista de parámetros apropiada. Además debe ser lo primero que se haga en el constructor de la derivada. Sino se hace el compilador envía un error. Dado que no puede aparecer nada antes de la llamada al constructor de la clase base no se podrán capturar excepciones en el constructor de la derivada, lo que a veces es un inconveniente. Si una clase en Java tiene un nombre de método sobrecargado varias veces, la redefinición de ese nombre de método en la clase derivada no esconde las versiones de la clase base, es decir, la sobrecarga funciona igual con independencia del nivel de herencia. 6.2 La palabra clave final. 6.2.1 Declaración de datos como final. Si el dato es de un tipo primitivo, se convierte en constante en tiempo de compilación y se le debe dar un valor en tiempo de la definición. Un campo que se defina como estático y final sólo tiene un espacio de almacenamiento que no se puede modificar. Sin embargo cuando declaramos como final cualquier objeto, en realidad se hace final la referencia a una constante, es decir, una vez que la referencia se inicializa a un objeto, ésta nunca se puede cambiar para que apunte a otro objeto, pero, sí se puede modificar el objeto en sí: public class DatosConstantes { ... final int i1=9; public static final int VAL =99; final valor Val2=new Valor(); ... DatosConstantes dc1 = new datosConstantes(); dc1.i1++; // ERROR i1 es constante dc1.Val2.i1++; // Referencia constante el objeto no dc1.Val2=new Valor(); // ERROR referencia constante ... ... Si declaramos un dato como final y no se inicializa hasta el momento de su uso es lo que se conoce como constante blanca, en cualquier caso se debe inicializar antes de su uso y ésto lo garantiza el compilador. Es obligatorio hacer las asignaciones a constantes, bien en el momento de definición del campo o bien en el constructor, de forma que se garantice que el campo constante se inicializa siempre antes de ser usado. Java permite hacer parámetros constantes declarándolos como final en la lista de parámetros. Esto significa que dentro del método no se puede cambiar aquello a lo que apunta la referencia al parámetro. Página 14 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) 6.2.2 Declaración de métodos como final Hay dos razones para justificar los métodos constates, evitar que las clases heredadas varíen su significado y por motivos de eficiencia. Cualquier método privado de una clase es implícitamente constante, no se puede acceder ni modificar (si se intenta se crea uno nuevo en realidad). 6.2.3 Clases constantes Al definir una clase como constante (final) se establece que no se puede heredar de esta clase bien sea por cuestiones de diseño, seguridad o eficiencia. Al definir una clase como constante todos sus métodos son implícitamente constantes ya que no se pueden modificar. 6.3 Carga de clases e inicialización. En general se puede asegurar que el código de las clases se carga en el momento de su primer uso, esto normalmente ocurre cuando se construye el primer objeto de esa clase, pero también se da una carga cuando se accede a un dato o método estático. En el momento del primer uso es también donde se da la inicialización estática. Todos los objetos estáticos y el bloque de código estático se inicializan en el orden textual en el momento de la carga. Los datos estáticos se inicializan solo una vez. Si se utiliza herencia la clase base se carga con independencia de si se crea un objeto de la clase derivada. Posteriormente se lleva a cabo la inicialización estática de la clase raíz y posteriormente la siguiente clase derivada. Tras cargar las clases necesarias se puede crear el objeto. Primero se pone a sus valores por defecto todos los datos primitivos y las referencias a objetos se ponen a null. Después se invoca al constructor de la clase base. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 15 Sistemas Informáticos I (Ingeniería Informática. UNED) 7. Polimorfismo La conexión de una llamada a un método se denomina ligadura. Cuando se lleva a cabo la ligadura antes de ejecutar el programa, es decir, en tiempo de compilación, se denomina temprana. La correspondencia en tiempo de ejecución se llama ligadura tardía o dinámica. Cuando un lenguaje implementa la ligadura tardía, debe haber algún mecanismo para determinar el tipo de objeto en tiempo de ejecución e invocar al método adecuado. Toda ligadura de métodos en Java es tardía excepto en los métodos constantes. Esto permite asegurar el polimorfismo ya que se puede escribir el código que trata la clase base y saber que todas las clases derivadas funcionarán correctamente al usar el código. 7.1 Clases y métodos abstractos. Se crea una clase abstracta cuando se desea manipular un conjunto de clases a través de un interfaz común. Todos los métodos de clases derivadas que encajan en la declaración de la clase base se invocarán utilizando el mecanismo de ligadura dinámica (si los parámetros son diferentes se tiene sobrecarga). Toda clase que tenga uno o más métodos abstractos se considera abstracta. No es posible crear objetos de una clase abstracta, y si se hereda de ella se deben de proporcionar definiciones de los métodos que en la clase base eran abstractos. Si no se hace así la clase derivada será también abstracta y el compilador obligará a definirla como tal. Se puede tener una clase cuyos métodos sean no abstractos y esté definido como abstracto, eso tiene sentido si no se desea que se creen instancias de ellos. Abstract class claseAbstracta .... public abstract void metodoAbstracto(); public void metodo_no_abstracto() {...}; .... ... class clasederivadadeAbstracta .... public void metodoAbstracto () {//Definición} .... .... 7.2 Constructores y polimorfismo En el constructor de la clase derivada siempre se invoca a un constructor de la clase base, encadenando la jerarquía de herencias de forma que se invoca a un constructor de cada clase base, después se llama a los inicializadores de miembros en el orden de declaración y finalmente al cuerpo del constructor de la clase derivada. Al utilizar la herencia y utilizar finalize, se debe superponer éste método en la clase derivada si se quiere hacer alguna limpieza especial ya que se puede tener control sobre el orden de finalización. El orden más adecuado es finalizar primero la clase derivada y después la clase base. Sin embargo, cuando dentro de un constructor se invoca a un método de ligadura dinámica del objeto que se construye, se utiliza la definición superpuesta de ese método, el efecto puede ser inesperado y dar lugar a errores difíciles de detectar. Esto es así porque se llama a un método que podría manipular miembros que no han sido aún inicializados. 7.2.1 Conversión hacia abajo Dado que en la conversión hacia arriba se pierde información específica de los tipos, tiene sentido hacer una conversión hacia abajo si se quiere recuperar la información de tipos, es decir, moverse hacia abajo por la jerarquía. La conversión hacia arriba siempre es segura ya que la clase base no puede tener una interfaz mayor que la derivada, sin embargo la conversión hacia abajo no está tan claro. Para garantizarlo, Java, realiza una comprobación de todas las conversiones en tiempo de ejecución. Página 16 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) 8 Interfaces y clases internas 8.1 Interfaces Mediante la palabra clave abstract, que permite crear uno o más métodos sin definición dentro de una clase, se proporciona parte de la interfaz sin proporcionar la implementación, que será creada por sus descendientes. La palabra clave interface produce una clase completamente abstracta, que no tiene ningún tipo de implementación. Una interfaz puede contener campos, pero éstos son implícitamente estáticos y constantes, proporcionando sólo la forma, pero no la implementación. Definición de interfaz Implementación tipo_acceso interfaz nombre { tipo_devuelto metodo1 (parámetro); ... tipo_var1 = valor; } Acceso public o no se usa. Las variables son implícitamente final y static Un método privado no es parte de la interfaz acceso class nombreclase implements nombre { .... } Acceso public o no se usa Los métodos que implemente la interfaz deben ser public 8.2 Herencia múltiple Dado que una interfaz no tiene implementación alguna (no tiene espacio de almacenamiento asociado) se pueden combinar varias interfaces. Sólo se puede heredar desde una clase no interfaz, el resto de elementos base deben ser interfaces. Al implementar múltiples interfaces pueden colisionar nombres de métodos. Al mezclar la sobrecarga, la implementación y la superposición, dado que las funciones sobrecargadas no pueden diferir sólo en el valor de retorno, podemos tener errores del compilador. Por ello se debe evitar la utilización de métodos con los mismos nombres en las interfaces. Una interfaz puede extender otra interfaz o bien puede combinar varias interfaces sin ningún tipo de problema. 8.2.1 Constantes de agrupamiento Cualquier campo que se ponga en un interfaz se convierte automáticamente en estático y constante, por ello la interface es una herramienta conveniente para la creación de grupos de valores constantes (tipo enumerado de C). 8.2.2 Inicialización de atributos Los atributos definidos en las interfaces son automáticamente estáticos y constantes. Estos no pueden se constantes blancas, pero pueden inicializarse con expresiones no constantes. Dado que los campos son estáticos, se inicializan cuando se carga la clase por primera vez, lo que ocurre cuando se accede a cualquiera de los atributos por primera vez. Los atributos aunque no son pare del interfaz se almacenan, sin embargo, en el área de almacenamiento estático de esa interfaz. Int num = (int)(Mathrandom()*10);// La espresión no es constante pero sería válida en un interfaz 8.2.3 Interfaces anidadas Las interfaces anidadas, y las que no lo son, pueden tener visibilidad pública o amistosa y pueden implementarse como clases públicas, amistosas y privadas. La novedad de las interfaces anidadas es que pueden definirse privadas. 8.3 Clases internas Es posible colocar una definición de clase dentro de otra definición de clase, lo que se denomina clase interna. Esto permite agrupar clases que están relacionadas, además de controlar la visibilidad de una con la otra. Las clases internas tienen su razón de ser al comenzar a hacer una conversión hacia una clase base (en particular a una interfaz). Esto aumenta la ocultación de la implementación. 8.3.1 Ámbitos y clases internas en métodos Las clases internas pueden crearse dentro de un método o incluso en un ámbito arbitrario. Las razones son: Se implementa una interfaz de algún tipo, de forma que se puede crear y devolver una referencia. Para crear una clase que no esté públicamente disponible. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 17 Sistemas Informáticos I (Ingeniería Informática. UNED) 8.3.2 Clases internas anónimas Tienen la forma public class clase1 { public claseInterna nombre() { return new claseInterna() { ... } ; } } Cómo se ve, combina la creación del valor de retorno con la definición de la clase que representa ese valor de retorno y es anónima (no tiene nombre). Este código crea un objeto de una clase anónima heredada de claseInterna, a la referencia que devuelve new se le hace una conversión hacia arriba automáticamente para convertirla en una referencia a claseInterna, es decir, se puede considerar una abreviación de: class miclaseInterna implements claseInterna { ... } return new miclaseInterna(); Si se define una clase anónima interna y se desea utilizar un objeto definido fuera de la clase interna anónima, el compilador exige que el objeto externo sea constante. Una clase interna tiene acceso automático a los miembros de la clase contenedora ya que mantiene una referencia al objeto particular de la clase contenedora que era responsable de crearla. 8.3.3 Clases internas estáticas Si no se necesita una conexión entre el objeto de la clase interna y el objeto de la clase externa, se puede hacer estática la clase interna. Esto es así ya que una clase interna estática quiere decir: 1. No se necesita un objeto de la clase externa para crear un objeto de la clase interna estática. 2. No se puede acceder a un objeto de una clase externa desde un objeto de la clase interna estática. 3. Las clases interna no estáticas no pueden tener campos, datos o clases internas estáticas. Las clases internas hacen referencia a la clase externa mediante: claseExterna.this. 8.3.4 ¿Porque las clases internas? Cada clase interna puede heredar independientemente de una implementación. Por consiguiente, la clase interna no está limitada por el hecho de que la clase externa puede ya estar heredando de una implementación. Es decir, las clases internas permiten heredar de forma efectiva de más de un no interfaz. Además se tienen las siguientes características adicionales: 1. La clase interna tiene múltiples instancias, cada una con sus propia información de estado que es independiente de la información del objeto de la clase externa. 2. En una clase externa se pueden tener varias clases internas, cada una de las cuales implementan la misma interfaz o hereda de la misma clase de distinta forma. 3. El momento de la creación del objeto de la clase interna no está atado a la creación del objeto de la clase externa. 4. No hay relaciones es – un dentro de la clase interna. 10 Manejo de errores con excepciones. 10.1 Excepciones básicas. Una condición excepcional es un problema que evita la continuación de un método o del alcance actual, debido a que no se dispone de la información necesaria para tratar el problema en el contexto actual. Todo lo que se puede hacer es salir del contexto actual y relegar el problema a un contexto superior. Esto es lo que ocurre cuando se lanza una excepción: Se crea el objeto excepción en el montículo con new. Se detiene el cauce normal de ejecución y se lanza la referencia al objeto excepción desde el contexto actual. Página 18 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) El mecanismo de gestión de excepciones busca el lugar apropiado donde continuar ejecutando el programa. Este lugar es el gestor de excepciones, cuyo trabajo es recuperar el problema de forma que el programa pueda o bien intentarlo de nuevo, o bien simplemente continuar. Como cualquier objeto en Java las excepciones tienen su constructor, en el caso de las excepciones estándar dos, uno por defecto y otro que toma un parámetro string de forma que pueda ubicar la información pertinente en la excepción. Tras crear el objeto excepción (new) se le da a throw la referencia resultante. Además se puede lanzar cualquier tipo de objeto Throwable que se desee. Habitualmente se lanzará una clase de excepción diferente para cada tipo de error. La información sobre cada error se representa tanto dentro del objeto excepción como implícitamente en el tipo de objeto excepción elegido. 10.2 Capturar una excepción. Si un método lanza una excepción, debe asumir que esa excepción será capturada y tratada. El método acaba en el momento del lanzamiento, para evitarlo, se puede establecer un bloque excepcional dentro del método para que capture la excepción: try { // Código que puede generar excepciones } Toda excepción lanzada debe acabar en el manejador de excepciones, hay uno para cada tipo de excepción que se desee capturar. Los manejadores de excepción siguen inmediatamente al bloque try y se identifican por la palabra clave catch: try { // Código que puede generar excepciones } catch (tipo1 id1) { // Manejo excepciones tipo 1 } catch (tipo2 id2) { // Manejo excepciones tipo 2 } catch (tipo3 id3) { // Manejo excepciones tipo 3 .... } En este código sólo se ejecuta una sentencia catch (no pasa como en switch que le hace falta un break). No se está limitado a las excepciones estándar de Java, se pueden crear las propias excepciones para indicar un error especial, para ello se debe heredar de un tipo de excepción existente. 10.2.1 La especificación de excepciones La especificación de excepciones utiliza la palabra clave Throws seguida de todos los tipos de excepción potenciales. No se puede engañar sobre una especificación de excepciones, si un método provoca excepciones y no las maneja, el compilador lo detecta e indica que o bien se tratan o se colocan en la especificación de excepciones todas las excepciones que el método puede lanzar. Este sistema permite a Java asegurar la corrección de la excepción en tiempo de compilación. 10.2.2 Captura de cualquier excepción. La clase base de todas las excepciones es la clase Exception (si se usa en una lista debe ser la última), para conseguir información se puede llamar a los métodos de su tipo base Throwable. String getMessage() : Mensaje String getLocalizedMessage(): Mensaje ajustado al escenario particular String toString: Descripción del objeto Throwable void printStackTrace (): Imprime el objeto y la traza de pila de llamadas void printStackTrace(PrintStream): Apunta a un flujo de datos void printStackTrace (PrintWriter) Throwable fillInStackTrace (): registra la información relativa al estado actual de la pila. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 19 Sistemas Informáticos I (Ingeniería Informática. UNED) 10.2.3 Relanzar una excepción Si se desea se puede volver a lanzar una excepción que se acaba de capturar. Esto hace que la excepción vaya al contexto inmediatamente superior de manejadores de excepciones. Cualquier cláusula catch subsiguiente del mismo bloque try seguirá siendo ignorada. 10.3 Excepciones estándar de Java. Hay un grupo de tipos de excepciones que Java siempre lanza automáticamente, están agrupadas bajo una única clase denominada RuntimeException. Si no se capturan estas excepciones en tiempo de ejecución, se invoca a printStackTrace() para esa excepción y el programa finaliza su ejecución. 10.3.1 Restricciones Al aplicar la herencia a clases que tengan excepciones, el constructor de la clase derivada debe declarar cualquier excepción del constructor de la clase base en su especificación de excepciones. El constructor de la clase derivada no puede capturar las excepciones de la clase base. Justo porque existe una especificación de excepciones en una versión de la clase base de un método, no tiene porque existir en la versión de la clase derivada del mismo, esto es distinto a las normas generales de la herencia. Página 20 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) 11 El sistema de E/S de Java 11.1 La clase file La clase file puede representar el nombre de un archivo particular, o los nombres de un conjunto de archivos de un directorio. Si se trata de un conjunto de archivos, se puede preguntar por el conjunto con el método list(), que devuelve un array de strings. También se puede usar un objeto File para crear un nuevo directorio o una trayectoria de directorios completa si esta no existe. También se pueden mirar las características de archivos, ver si es objeto File representa un archivo o un directorio, y borrar un archivo. 11.2 Entrada y Salida Las bibliotecas de E/S usan a menudo la abstracción del flujo, que representa cualquier fuente o consumidor de datos como un objeto capaz de producir o recibir fragmentos de código. El flujo oculta los detalles de lo que ocurre con los datos en el dispositivo de E/S real. Todo lo que deriva de las clases InputStream o Reader tiene el método básico Read() para leer y lo que deriva de OutputStream o Writer tiene el método Write(). Aunque por lo general no se usan ya que a través de otras clases (que sí los usan) se aplican objetos por capas que proporcionan una funcionalidad final mayor. 11.2.1 Tipos de InputStream El trabajo de InputStream es representar las clases que producen entradas desde distintas fuentes: Un array de bytes Un objeto string Un archivo. Una tubería. Una secuencia de otros flujos que se agrupan todos en uno Otras fuentes, como una conexión a Internet. 11.2.2 Tipos de OutputStream Esta categoría incluye las clases que deciden donde irá la salida. Un array de bytes, un fichero o una tubería. No puede ser un String. 11.2.3 Leer de un InputStream con un FilterInputStream. Las clases de FilterInputStream son: DataInputStream: Permite leer tipos de datos primitivos (readByte(), readFloat()...) además de objetos String. BufferedInputSream: Se usa para evitar una lectura cada vez que se solicitan nuevos datos, es decir, crea un espacio intermedio. LineNumberInputStream: Mantiene un seguimiento de los números de línea en el flujo de datos de entrada. Puede llamar a getLineNumber() y a SetLineNumber(int). PushbackInputStream:Tiene un espacio de almacenamiento intermedio de un byte para el último carácter a leer. 11.2.4 Escribir un OutputStream con FilterOutputStream. Las clases son: DataOutpuStream: Da formato a los datos primitivos y objetos string, convirtiéndolos en un flujo de forma que cualquier DataInputStream los pueda leer. PrintStream: Produce salida formateada, maneja la visualización. BufferedOutputStream: Se usa para evitar una escritura física cada vez que se envía un paquete de datos. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 21 Sistemas Informáticos I (Ingeniería Informática. UNED) 11.3 E/S estándar. El término E/S estándar hace referencia al flujo de información que utiliza todo programa, de forma que toda entrada y salida fluye por la E/S estándar, mientras que todos sus mensajes de error se envían a la salida de error estándar. Esto proporciona la posibilidad de encadenar programas de modo que la salida de uno se convierta en la entrada estándar del siguiente. Siguiendo este modelo Java tiene System.in, System.out y System.err. La clase System de Java permite redirigir los flujos de entrada, salida y error haciendo uso de las llamadas a los métodos estáticos setIn(InputStream), setOut(PrintStream) y SetErr(PrintStream). 11.4 Compresión. La biblioteca de E/S de Java contiene clases que dan soporte a la lectura y escritura de flujos en un formato comprimido. Estas clases están envueltas en torno a las clases E/S existentes para proporcionar funcionalidad de compresión. Sin embargo, no derivan de Reader y Writer, sino que son parte de las jerarquías InputStream y OutputStream. Esto se debe a que la biblioteca de compresión funciona con bytes en lugar de caracteres. Las clases de compresión son: CheckedInputStream: La función GetCheckSum() produce una suma de comprobación de cualquier InputStream. ChekedOutputStream: La función GetCheckSum() produce una suma de comprobación de cualquier OutputStream. DeflaterOutputStream:Clase base de las funciones de compresión. ZipOutputStream: DeflaterOutputStream que comprime datos a formato de archivos ZIP. GZipOutputStream: DeflaterOutputStream que comprime datos a formato de archivos GZIP. InflaterItputStream:Clase base de las funciones de descompresión. ZipInputStream: InflaterInputStream que descomprime datos a formato de archivos ZIP. GZipInputStream: InflaterInputStream que descomprime datos a formato de archivos GZIP. 11.4.1 Archivos Java (JAR). El formato ZIP también se usa en el formato JAR; que es una forma de coleccionar un grupo de archivos en un único archivo comprimido, con la diferencia de que los ficheros JAR son multiplataforma. Los archivos JAR son particularmente útiles al trabajar con Internet ya que permite cargar de forma conjunta todos los archivos que conforman un applet. Mediante estos archivos la carga es más rápida y además soporta firmas digitales. Un archivo JAR consta de un único archivo que contiene una colección de archivos ZIP junto con una declaración que los describe. La utilidad jar comprime automáticamente los archivos que se seleccionan. jar [opciones] [manifiesto] destino archivo(s) Entrada Las opciones son: c: Crear archivo nuevo o vacío. t: Lista la tabla de contenidos. x: Extrae todos los archivos xfile: Extrae el archivo nombrado f: Indica que se va a dar nombre al archivo m: Indica que el primer parámetro será el nombre de un archivo de declaración creado por el usuario v: Genera una salida que describe que va haciendo JAR O: Sólo almacena los archivos no los comprime M: No crear automáticamente un archivo de declaración Mediante jar no se puede añadir o actualizar archivos de uno ya existentes, tampoco se pueden mover. 11.5 Serialización de objetos La serialización de objetos de Java permite tomar cualquier objeto que implemente la interfaz Serializable y convertirlo en una secuencia de bits que puede ser restaurada para generar el objeto original. El mecanismo de serialización compensa automáticamente las diferencias entre sistemas operativos. La serialización de un objeto es bastante simple, siempre que el objeto implemente la interfaz serializable (que no tiene métodos). Para serializar un objeto, se crea algún tipo de objeto OutputStream y se envuelve en un ObjectOutputStream. En este momento se invoca writeObject() y el objeto se serializa enviándose al OutputStream. Página 22 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) Para el proceso inverso, se envuelve un InputStream en un ObjectInputStream y se invoca a readObject(), como devuelve un Object, normalmente se deberá hacer una conversión hacia abajo, por ello una vez recuperado un objeto serializado, hay que asegurarse que la JVM pueda encontrar su clase, bien en el path o cualquier otro lugar especificado. Se puede controlar la serialización implementando la interfaz Externalizable que extiende Serializable añadiendo dos métodos: writeExternal() y readExternal(). Cuando se está controlando la serialización, puede ocurrir que haya un subobjeto en particular que no se desee que se salve y recupere de forma automática. Por ejemplo, si contiene información sensible (contraseñas). Para controlar esto, se puede desactivar la serialización en una base campo-a-campo mediante la palabra clave trasient. Esto indica que no se debe guardar o recuperar ese dato en la serialización: acceso trasient tipo nombre 11.6 Identificar símbolos de una entrada Las dos clases de Java para dividir las secuencias de caracteres en una secuencia de símbolos son StreamTokenizer y StringTokenizer. 11.6.1 StreamTokenizer StreamTokenizer no deriva de InputStream ni de OutputStream, sólo funciona con objetos InputStream, por lo que pertenece a la parte de E/S de la biblioteca. Esta clase divide el flujo de entrada en Tokens que están delimitados por conjuntos de caracteres. 11.6.2 StringTokenizer Esta clase facilita el análisis semántico de un flujo de entrada, y devuelve todos los símbolos contenidos en una cadena de caracteres de uno en uno, estos símbolos son los caracteres consecutivos delimitados por tabuladores, espacios y saltos de línea. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 23 Sistemas Informáticos I (Ingeniería Informática. UNED) 13 Crear ventanas y applets 13.1 Applet básico 13.1.1 Restricciones de los applets Un applet no tiene acceso al disco local El tiempo de ejecución aumenta ya que se debe descargar. Con los applets de confianza firmados digitalmente se suavizan las restricciones de acceso. 13.1.2 Ventajas de los applets. No precisan instalación, siendo independiente de la plataforma No hay peligro de que un código erróneo cause daño a la máquina. 13.1.3 Marcos de trabajo de la aplicación Las bibliotecas se agrupan dependiendo de su funcionalidad, o bien para facilitar la construcción de otras clases. Cierta categoría de biblioteca es el marco de trabajo de aplicación, cuya meta es ayudar en la construcción de aplicaciones proporcionando una clase o un conjunto de clases que producen el comportamiento básico deseado para toda aplicación de un tipo particular. Para adaptar el comportamiento a las necesidades particulares se debe heredar de la clase aplicación y superponer los métodos que interesan. Los applets se construyen utilizando un marco de trabajo de aplicación. Se hereda de JApplet y se superponen los métodos apropiados. Los métodos que controlan la ejecución y creación de un applet son: init(): se invoca automáticamente para lograr la primera inicialización del applet, incluyendo la disposición de componentes. Siempre se superpone. start(): Se invoca cada vez que se visualiza un applet en el navegador para permitirle empezar sus operaciones normales. Se invoca tras init(). stop(): Es invocado cada vez que el applet se aparta de la vista de un navegador web. Se invoca antes que destroy(). destroy(): Se invoca cuando se descarga el applet para liberar recursos. 13.1.4 Ejecución de applets dentro de un navegador. Los applets se deben de ejecutar en una página web. La sintaxis de una etiqueta APPLET estándar es: <APPLET CODEBASE = urlBasecódigo CODE = archivoClassApplet ALT= texto alternativo si el navegador entiende la etiqueta APPLET pero no puede ejecutarla. NAME=nombreinstanciaApplet WIDTH= pixels HEIGHT= pixels ALING= tipoAlineamiento > <PARAM NAME= nombreAtributo_1 VALUE = valorAtributo_1> <PARAM NAME= nombreAtributo_2 VALUE = valorAtributo_2> .... <PARAM NAME= nombreAtributo_n VALUE = valorAtributo_n> </APPLET> Página 24 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) 14 Hilos múltiples Puede ser necesario convertir un programa en subtareas separadas que se ejecuten independientemente. Cada una de estas subtareas recibe el nombre de hilo. Un proceso es un programa en ejecución autocontenido con su propio espacio de direcciones. Un sistema operativo multitarea es capaz de ejecutar más de un proceso a la vez, proporcionándole ciclos de UCP periódicamente. Por consiguiente, un único proceso puede tener múltiples hilos ejecutándose concurrentemente 14.1 Creando hilos con Thread. La forma más simple de crear un hilo es heredar de la clase Thread, que tiene todo lo necesario para crear o ejecutar hilos. El método run debe ser sobreescrito para que el hilo haga lo deseado, es decir es el código simultáneo de los hijos. El método start de la clase Thread lleva a cabo alguna inicialización especial para el hijo y después llama a run(). El orden con el que la UCP atiende a un conjunto de hilos es indeterminado, a menos que se cambien las prioridades haciendo uso del método setPriority() de Thread. Además cuando se crea un Thread no se captura su referencia, es el propio hilo el que guarda su referencia de forma que el recolector de basura no pueda limpiarlo. 14.2 Crear hilos con Runnable. Para poder combinar la clase principal del programa con un hilo se le debe añadir al primero funcionalidades adicionales, esto se consigue mediante la interfaz Runnable. El uso programa/hilo no es tan obvio, cuando se empieza el programa se crean un objeto que es Runnable, pero no se arranca el hilo. Esto hay que hacerlo explícitamente: hilo_propio= new Thread (nombreclase.this); // nombreclase implementa Runnable Cuando algo tiene una interfaz Runnable, simplemente significa que tiene un método run() (pero sin propiedades como cuando se hereda de Thread). Por lo tanto, para producir un hilo a partir de un objeto Runnable, hay que crear un objeto Thread separado, pasándole el objeto Runnable al constructor Thread especial. Después se puede llamar al método start() de ese hilo, que lleva a cabo la inicialización y llama a run() hilopropio.start(); 14.3 Hilos demonio Un hilo demonio es aquél que supuestamente proporciona un servicio general en segundo plano mientras que se está ejecutando el programa, no siendo parte esencial del programa. Por consiguiente, cuando todos los hijos no demonio acaban, se finaliza el programa. Se puede saber si un hilo es demonio llamando a isDeamon(), y se puede activar o desactivar el funcionamiento como demonio de un hilo con setDeamon. Si un hilo es demonio, todos los hilos que creen serán demonios. 14.4 Compartir recursos Con la capacidad multihilo existe la posibilidad de que dos o más hilos traten de usar el mismo recurso limitado a la vez. Hay que prevenir las colisiones por un recurso. Para ello Java tiene integrado un soporte para prevenir las colisiones de acceso a un recurso, este es declarar el método que accede al mismo como synchronized. Sólo un hilo puede invocar a un método synchronized en cada instante para cada objeto. Acceso synchronized tiporetorno método (parámetros) { // Zona sincronizada .... } Las secciones críticas también se pueden utilizar para especificar el objeto cuyo bloqueo se usa para sincronizar el código adjunto: synchronized (objetoSincronizado) { .... // A este código sólo puede acceder un Thread } 14.5 JavaBeans Cuando se crea un Bean, hay que asumir que se ejecutará en un entorno multihilo. Esto significa: 1. Siempre que sea posible sus métodos public deberían ser synchronized. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 25 Sistemas Informáticos I (Ingeniería Informática. UNED) 2. Al disparar un evento multidifusión a un conjunto de oyentes, hay que asumir que se podría añadir o eliminar oyentes de la lista al recorrerla. 14.6 Bloqueo Un hilo puede estar en uno de los siguientes estados: 1. Nuevo: El hilo se ha creado pero no se ha arrancado. 2. Ejecutable: El hilo puede ponerse en ejecución cuando le sea asignada la UCP 3. Muerto: Al finalizar su método run(). Si se hace llamando a stop se produce una excepción. 4. Bloqueado: Hay algo que evita que se ejecute el hilo. 14.6.1 Bloqueándose Un hilo puede estar bloqueado por: Se ha puesto a dormir con sleep Se ha suspendido con suspend, no se reanuda hasta recibir resume() (en desuso para Java 2). Se suspende con wait(). No se reanuda hasta recibir notify() o notifyAll(). En este caso se liberan los objetos synchronized que podría estar usando el hilo. El hilo está esperando que se complete una E/S. El hilo está intentando acceder a un método synchronized usado por otro. Se puede llamar a yield() para ceder voluntariamente la UCP para que se ejecuten otros hilos. Página 26 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) 15 Computación distribuida 15.1 Programación en Red. 15.1.1 Identificar una máquina. Para identificar de forma unívoca una máquina se utiliza el nombre de dominio DNS o un cuarteto de tres cifras, en ambos casos la dirección IP se representa internamente como un número de 32 bits, y se puede lograr un objeto Java que lo representa, usando el método static InetAdress.getByName() se obtiene un objeto de tipo InetAddress: InetAddress Val = InetAddress.getByName (args[0]);// Args[0] es el nombre de la máquina // Si args[0]= null, se consigue LocalHost Una máquina se identifica con la dirección IP y por el puerto, por ello cuando se establece un cliente o un servidor hay que elegir un puerto en el que tanto el cliente como el servidor acuerden conectarse. Los servicios del sistema se reservan el uso de los puertos 1 a 1024. 15.1.2 Sockets El socket es la abstracción software que se usa para representar los terminales de una conexión entre dos máquinas. Para una conexión dada hay un socket en cada máquina. En Java se crea un socket para hacer una conexión a la otra máquina, después se crea un InputStream y un OutputStream a partir del socket para poder tratar la conexión como un objeto flujo E/S. Hay dos clases de sockets basados en flujos: un ServerSocket que usa un servidor para escuchar eventos entrantes y un Socket que usa un cliente para iniciar una conexión. Cuando un cliente hace una conexión, ServerSocket devuelve vía accept() un socket, a través del que se realiza la comunicación. En éste momento se usa getInputStream() y getOutputStream() para producir los objetos InputStream y OutputStream correspondientes a cada Socket. // servidor ServerSocket s= new ServerSocket (PUERTO); try { // Bloqueado esperando conexión Socket conect = s.accept(); .... } finally { // Cierra la conexión s.close(); } .... // cliente ..... Socket conectCliente= new Socket(addr, Puerto); ... try { ..... } finally { // Cierra la conexión conectCliente.close(); } .... 15.1.3 Objeto URL. Dado que todos los navegadores utilizan URL, existe en Java una clase del mismo nombre que proporciona una interfaz API simple y concisa para acceder a información a través de Internet. El constructor más usado es: URL(URL objeto_URL, String especificación_URL); que nos permite relacionarlo con la clase applet: URL url = new URL (getDocumentBase(), “pagina.html”); getAppletContext().showDocument(url); AppletContext es una interfaz que nos proporciona información desde el entorno de ejecución del Applet, dentro de un Applet, y una vez se ha obtenido su contexto, se puede visualizar otro documento llamando a ShowDocument(). Del objeto URL se puede obtener, por un lado el directorio donde cuelga el fichero clase de Applet con getCodeBase() y el directorio donde está el fichero HTML (y su nombre) desde el que arranca el applet. 15.2 Conectividad a Base de Datos de Java (JDBC). JDBC permite a Java construir aplicaciones de bases de datos clientes/servidor independientes de la plataforma. Para apoyar estas funciones es interesante utilizar exclusivamente identificadores de tipos SQL genéricos. Para trabajar con una base de datos en un principio nos debemos conectar, crear unas sentencia, ejecutar la petición y acceder al conjunto resultado. Para abrir una base de datos hay que crear un URL de base de datos que especifica: 1. Que se está utilizando JDBS con “jdbc”. 2. El subprotocolo, nombre del control de la conexión (por ejemplo “odbc”). J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 27 Sistemas Informáticos I (Ingeniería Informática. UNED) 3. Identificador de la base de datos, varía en función del controlador de base de datos, es un nombre lógico que es asociado por el administrador de base de datos a un directorio físico donde se hayan los datos. Así por ejemplo: String dbURL = “jdbc:odbc:pepe”; Class.forName (“sun.jdbc.odbc.JdbcOdbcDriver”);// Carga del controlador. Connection c = DriverManager.getConnection (dbURL, usuario, contrasenya); /* Este método estático es el que conecta con la base de datos. Devuelve un objeto tipo connection que se usará para consultar la base de datos */ Statements = c.createStatement();//Se crea un estamento que permite hacer las consultas. ResultSet r= s.executeQuery (CONSULTA_SQL); // Es un iterador while (r.next()) { r.getString(campo)...// Usa getInt(), getString(), getFloat()... dependiendo del tipo esperado s.close(); 15.3 Servlets Los servlets proporcionan una solución excelente para soporte en el lado servidor. La arquitectura del API servlet es la de un proveedor de servicios clásico con un método service() a través del cual se enviarán todas las peticiones del cliente por parte del software contenedor del servlet, y los métodos del ciclo de vida init() y destroy(), que se invocan sólo cuando se carga y descarga el servlet. public public public public public public interface Servlet void init (ServletGetConfig config) Throws ServletException; ServletConfig getServletConfig(); void Service (ServletRequest req, ServletResponse res) Throws ServletEsception, IOException String getServletInfo(); void destroy(); El propósito de getServletConfig() es devolver un objeto ServletConfig que contiene los parámetros de inicialización y arranque del servidor. El método setServletInfo() devuelve una cadena de caracteres que contiene información sobre el servlet, como el autor, versión etc. El atributo más conveniente de la API de servlets lo constituyen los objetos auxiliares que vienen con la clase HttpServlet (que es extensión de GenericServlet) para darle soporte. El servlet se inicializa llamando al método init(), al cargar el servidor tras arrancar por primera vez el contenedor de servlets. Cuando un cliente hace una petición a una URL que resulta representar un servlet, el contenedor de servlets intercepta la petición y hace una llamada al método service(), tras configurar los objetos HttpServletRequest y HttpServletResponse (que son extensiones de ServletRequest y ServletResponse). 15.3.5 Gestionar sesiones con servlets. HTTP es un protocolo sin sesión, de forma que no se puede distinguir entre una persona que accede repetidamente al sitio, y otra persona diferente. Una de las soluciones aportadas son las cookies, que son una pequeña pieza de información enviada por un servidor web a un navegador. El navegador almacena la cookie en el disco local y cuando se hace otra llamada al URL asociado a la cookie, ésta se envía junto con la llamada, proporcionando así que la información deseada vuelva al servidor. El API servlet proporciona la clase Cookie. Esta clase incorpora todos los detalles de cabecera HTTP y permite el establecimiento de varios atributos de cookie. Para usarlas se añaden al objeto respuesta. Cookie oreo = new Cookie (“una”, “2000”); // Una es el nombre y 2000 el valor. res.addCookie(oreo); String valor= getValue(“una”); 15.3.4 La clase Session Una sesión es una o más solicitudes de páginas por parte de un cliente a un sitio web durante un periodo definido de tiempo. Un objeto SessionServlet vive en la parte servidores del canal de comunicación; su meta es capturar datos útiles sobre ese cliente a medida que el cliente recorre e interactúa con el sitio web. La clase session del API servlet usa la palabra Cookie para hacer su trabajo, sin embargo, todo objeto Session necesita algún tipo de identificador único almacenado en el cliente y que se pasa al servidor. 15.4 Java Server Pages Las Java Server Pages (JSP) son una extensión del estándar Java definido sobre las extensiones de sevlets. El objetivo de JSP es la creación simplificada de páginas web dinámicas, al combinar HTML con Java en el mismo documento. Página 28 J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 Sistemas Informáticos I (Ingeniería Informática. UNED) El código Java está rodeado de etiquetas especiales que indican al contenedor de JSP que deberá usar el código para generar un servlet o parte de uno. La primera vez que el contenedor de JSP carga un JSP se genera, compila y carga en el contenedor de Servlets el código servlet necesario para cumplimentar las etiquetas JSP. Las porciones estáticas de la página HTML se producen enviando objetos String estáticos a write(). Las porciones dinámicas se incluyen directamente en el servlet. Hasta que el código JSP de la página no se modifique, se comporta como si fuera una página HTML estática con servlets asociados. Si se codifica, se recompila y recarga automáticamente la siguiente vez que se solicite esa página. Las etiquetas JSP son: < % código JSP %> Las directivas son mensajes al contenedor de JSP y se delimitan por @ <%@ directiva { atributo = “valor”} * %> No envían nada al flujo Out, permiten configurar atributos y dependencias. 15.5 RMI (Remote Mode Invocation). RMI hace un uso intensivo de las interfaces. Cuando se desea crear un objeto remoto, se enmascara la implementación subyacente pasando una interfaz. Por consiguiente, cuando el cliente obtiene una referencia a un objeto remoto, lo que verdaderamente logra es una referencia al un interfaz, que resulta estar conectado a algún fragmento de código local. Al crear una interfaz remota hay que seguir ciertas normas: Debe ser public. Debe extender la interfaz java.rmi.Remote. Cada uno de sus métodos debe declarar java.rmi.RemoteException en su cláusula Throws. Todo objeto remoto pasado como parámetro o valor de retorno debe declararse como la interfaz remota, no como la clase implementation. J.M. Godoy, F. Gómez y E. Rubio. 2002-2003 página 29