Download String x = "Java"
Document related concepts
no text concepts found
Transcript
Capítulo 6 - Strings, IO, Formateo y Parseo 1. Objetivo de Certificación 3.1 String, StringBuilder y StringBuffer Discutir las diferencias entre las clases String, StringBuilder y StringBuffer. Índice 1. Objetivo de Certificación 3.1 - String, StringBuilder y StringBuffer 1.1. La Clase String 1.2. Cósas Importantes sobre los String y la Memoria 1.3. Métodos Importantes en la Clase String 1.4. Las Clases StringBuffer y StringBuilder 1.5. Métodos Importantes en las Clases StringBuffer y StringBuilder 2. Objetivo de Certificación 3.2 - Manejo de Ficheros y I/O 2.1. Creando Ficheros Mediante la Clase File 2.2. Usando FileWriter y FileReader 2.3. Combinando Clases I/O 2.4. Trabajando con Ficheros y Directorios 3. Objetivo de Certificación 3.3 - Serialización 3.1. Trabajando con ObjectOutputStream y ObjectInputStream 3.2. Representación de Objetos 3.3. Usar WriteObject y ReadObject 3.4. ¿Cómo afecta la Herencia a la Serialización? 3.5. La Serialización no vale para los Estáticos 4. Objetivo de Certificación 3.4. - Fechas, Números y Divisas 4.1. Trabajando cn Fechas, Números y Divisas 5. Objetivo de Certificación 3.5 - Parseo, Tokens y Formateo 5.1. Tutorial de Búsqueda 5.2. Localizando Datos a través de Patrones 5.3. Tokenizando 5.4. Formateo con printf() y format() Todo lo que necesitamos saber sobre Strings en el examen SCJP 1.4 lo necesitaremos para el examen SCJP 5, además de algunas cosas más. Por ejemplo, SUN añadió la clase StringBuilder a la API para proporcionar la capacidad de StringBuffer pero de una forma más rápida y no-sincronizada. La clase StringBuilder tiene exáctamente los mismos métodos que StringBuffer. Ambas clases nos proporcionan objetos String que manejan alguna de las deficiencias de la clase String (como la inmutabilidad). 1.1. La Clase String Esta sección cubre la clase String y el concepto clave de que cuando se crea un objeto String, éste nunca puede ser cambiado. Los String son Objetos Inmutables Empezaremos con un poco de información básica sobre las cadenas que no necesitaremos para el test pero que ayudará. Manejas "cadenas" de caracteres es un aspecto fundamental de la mayoría de los lenguajes. En Java, cada carácter en una cadena es un carácter Unicode de 16-Bit. En Java, los String son objetos y podemos crear una instancia con la palabra reservada 'new': String s = new String(); La línea anterior crea un objeto String y lo asigna a la variable de referencia 's'. Ahora le daremos un valor: Este capítulo se centra en los temas relacionados con la API que se añadieron al examen para Java 5. El examen se centra en las APIs para I/O, formateo y parseo. No tenemos que conocer todos los detalles de estas tecnologías sino los aspectos básicos. Este capítulo cubre algo más de lo que necesitamos conocer para los objetivos del examen. s = "abcdef"; La clase String tiene muchos constructores, por lo que podríamos hacerlo de forma más directa: String s = new String("abcdef"); También podemos usar la siguiente forma: String s = "abcdef"; Existen algunas pequeñas diferencias entre estas opciones que se discutirán más tarde, pero tienen en común que todas crean un nuevo objeto String con el valor "abcdef" y lo asignan a la variable 's'. Ahora digamos que queremos una segunda referencia al objeto String referenciado por 's': String s2 = s; same String as s // refer s2 to the Los objetos String parecen tener el mismo comportamiento que otros objetos, la diferencia es que son Inmutables. Una vez que hemos asignado un valor a un String, el valor no puede cambiar. Lo bueno es que, aunque el objeto String es inmutable, su variable de referencia no lo es, por lo que continuemos con nuestro ejemplo anterior: s = s.concat(" more stuff"); // the concat() method 'appends' a literal to the end Veamos qué ha pasado... La VM toma el valor de 's' (que es "abcdef") y añade "more stuff" al final, dándole el valor "abcdef more stuff". Puesto que los Strings son inmutables, la VM no puede agregar este valor a la antigua cadena referenciada por 's', por lo que crea un nuevo objeto String con el nuevo valor y hace que 's' apunte a él. En este punto del ejemplo, tenemos dos objetos String: el primero que hemos creado, con el valor "abcdef" y el segundo con el valor "abcdef more stuff". Técnicamente tenemos tres objetos String, puesto que el argumento del método 'concat' (" more stuff") es en sí mismo un nuevo objeto String. Pero sólo tenemos referenciados los dos nombrados antes. Pero, ¿que pasa si no hubiéramos creado la segunda referencia para la cadena "abcdef" antes de llamar al método 'concat'?. En ese caso, la cadena original seguiría existiendo en memora pero se considera "perdida". No hay forma de recuperar una variable perdida. Observa sin embargo que la cadena original nomcambió (es inmutable), sólo lo hizo su referencia, por lo que podríamos haber asignado el valor a otra variable. La siguiente figura muestra qué ocurre en la pila cuando re-asignamos una variable de referencia. Observa que la línea de puntos indica una referencia eliminada. Veamos el ejemplo completo: String s = "abcdef"; // create a new String object, with value "abcdef", refer s to it String s2 = s; // create a 2nd reference variable referring to the same String // create a new String object, with value "abcdef more stuff", refer //s// to it. (Change s's reference from the old String // to the new String.) ( Remember s2 is still referring to the original "abcdef" String.) s = s.concat(" more stuff"); Veamos otro ejemplo: String x = "Java"; x.concat(" Rules!") ; System.out.println("x = " + x); output is "x = Java" // the La primera línea es simple: crea un nuevo objeto String, le da el valor "Java" y hace que 'x' apunte a él. Luego la VM crea un segundo objeto String con el valor "Java Rules!" pero nadie apunta a él. El segundo objeto String está perdido al momento, no podemos acceder a él. La variable 'x' sigue apuntando al valor original. La siguiente figura ilustra esto: Expandamos el ejemplo anterior, partiendo de lo siguiente: String x = "Java"; x.concat(" Rules!"); System.out.println("x = " + x); output is: x = Java // the Añadimos: x.toUpperCase() ; System.out.println("x = " + x); output is still: x = Java // the Hemos creado un nuevo objeto String con el valor "JAVA" pero al igual que antes se ha perdido. Ahora veamos lo siguiente: x.replace('a', 'X'); System.out.println("x = " + x); output is still: x = Java // the ¿Podemos determinar qué ha pasado?. La VM creó otro objeto con el valor "JXvX" pero de nuevo se perdió, porque 'x' seguía apuntando al String con el valor "Java". En todos los casos anteriores, hemos ido llamando a métodos de String para crear nuevos objetos alterando un String existente, pero no hemos asignado nunca el nuevo String a una variable de referencia. Podemos cambiar un poco el ejemplo anterior de la siguiente forma: String x = "Java"; x = x.concat(" Rules!"); // Now we're assigning the new String to x System.out.println("x = " + x); // the output will be: x = Java Rules! En este ejemplo, al crear el nuevo String lo hemos asignado a la variable 'x' pero el String original se ha perdido, nadie está apuntando a él. En ambos ejemplos se han creado dos objetos String y sólo una variable de referencia, por lo que uno siempre se pierde. La siguiente figura ilustra esta triste historia: Veamos el siguiente ejemplo: String x = "Java"; x = x.concat(" Rules!"); System.out.println("x = " + x); output is: x = Java Rules! // the Sólo dos de los 8 objetos no se perdieron en el proceso. x.toLowerCase(); // no assignment, create a new, abandoned String System.out.println("x = " + x); // no assignment, the output is still: x = Java Rules! x = x.toLowerCase(); // create a new String, assigned to x System.out.println("x = " + x); // the assignment causes the output: x = java rules! La discusión anterior tiene las claves para comprender la inmutabilidad de String. Si comprendemos bien los ejemplos y diagramas, podremos conseguir acertar el 80% de las preguntas de String. Veremos otros detalles de String, pero lo que hemos cubierto es de lejos la parte más importante para comprender el funcionamiento de los objetos String en Java. Finalizaremos la sección presentando un ejemplo de una pregunta con trampa sobre String. Tómate tiempo para trabajarlo (como pista, intenta mantener la traza de cuando objetos y variables de referencia tenemos y cuál apunta a cada objeto). String s1 = "spring "; String s2 = s1 + "summer "; s1.concat("fall ") ; s2.concat(s1); s1 += "winter "; System.out.println(s1 + " " + s2); ¿Cuál es la salida?. Para un punto extra, ¿cuántos objetos String y referencias fueron creados antes de la sentencia 'println'? Respuesta: El resultado es "spring winter spring summer". Tenemos dos variables de referencia, 's1' y 's2'. Fueron creados un todal de 8 objetos String en el siguiente orden: 1. 2. 3. 4. 5. 6. 7. 8. "spring" "summer" -> Perdido "spring summer" "fall" -> Perdido "spring fall" -> Perdido "spring summer spring" -> Perdido "winter" -> Perdido "spring winter" (en este punto se pierde"spring") 1.2. Cósas Importantes sobre los String y la Memoria En esta sección se discutirá sobre cómo maneja Java los objetos String en memoria y alguna de las razones para estos comportamientos. Uno de los principales objetivos de un buen lenguaje de programación es que hagan un uso eficiente de memoria. Para que Java sea más eficiente, la JVM mantiene un área especial de memoria llamada "String constant pool". Cuando el compilador encuentra un literal String, comprueba el pool por si hay un String idéntico. Si existe, la referencia al nuevo literal se redirige al String existente y no se crea uno nuevo. Ahora puede entenderse porqué los String son inmutables, puesto que varias referencias podrían apuntar al mismo String sin que lo supieran, propiciando fallos si alguna referencia cambia el valor del String. Además, para evitar que alguien sobrescriba la funcionalidad de la clase String y por tanto cause problemas en el pool, la clase String es marcada como 'final'. Por lo que podemos asegurar que los objetos String que tratemos serán siempre inmutables. Creando Nuevos Strings Veamos un par de ejemplos sobre cómo se crearía un String, asumiendo que no existen otros objetos String en el pool: String s = "abc"; // creates one String object and one reference variable En este caso, "abc" irá al pool y 's' apuntará hacia el String. String s = new String("abc"); // creates two objects, and one reference variable En este caso, puesto que hemos usado la palabra reservada 'new', Java creará un nuevo objeto String en la memoria normal (no-pool) y 's' apuntará a él. Además, el literal "abc" se localizará en el pool. 1.3. Métodos Importantes en la Clase String Los siguientes métodos son los métodos más comunes y también los únicos que encontraremos en el examen. charAt() Devuelve el carácter localizado en el índice especificado concat() Añade un String al final de otro ("+" es similar) equalsIgnoreCase() Determina la igualdad de dos Strings, ignorando mayúsculas length() Devuelve el número de caracteres de un String replace() Reemplaza las ocurrencias de un carácter por otro substring() Devuelve parte de un String toLowerCase() Devuelve un String pasado a mayúsculas toString() Devuelve el valor de un String toUpperCase() Devuelve un String pasado a minúsculas trim() Elimina el espacio en blanco del principio y del final de un String En el ejemplo de >"Atlantic ocean" es importante destacar que la variable 'x' cambia de valor. El operador '+=' es un operador de asignación por lo que la segunda línea (x += " ocean";) crea un nuevo String y lo asigna a la variable 'x'. Después de ejecutarse esta línea, el String original al que hacía referencia 'x' se abandona.> public boolean equalslgnoreCase(String s) El método devuelve un booleano en función de si el valor del String en el argumento es el mismo que el del String usado para invocar el método. Este método ignora mayúsculas. Por ejemplo: String x = "Exit"; System.out.println( x.equalsIgnoreCase("EXIT")); "true" System.out.println( x.equalsIgnoreCase("tixe")); "false" // is // is public int length() Devuelve el tamaño del String usado para invocar el método. Por ejemplo: String x = "01234567"; System.out.println( x.length() ); returns "8" // public String replace(char old, char new) El método devuelve un String donde se han reemplazado todas las ocurrencias del carácter 'old' por el carácter 'new'. Por ejemplo: Veamos estos métodos en más detalle. public char charAt(int index) Devuelve el carácter localizado en el índice especificado. Empieza en 0, por ejemplo: String x = "oxoxoxox"; System.out.println( x.replace('x', 'X') ); // output is "oXoXoXoX" String x = "airplane"; System.out.println( x.charAt(2) ); // output is 'r' public String concat(String s) Devuelve un String con el valor del String pasado en el método añadido al final del String usado para invocar el método. Ejemplo: String x = "taxi"; System.out.println( x.concat(" cab") ); // output is "taxi cab" Los operadores sobrecargados + y += funcionan de forma similar a este método. Ejemplo: String x = "library"; System.out.println( x + " card"); output is "library card" String x = "Atlantic"; x += " ocean"; System.out.println( x ); is "Atlantic ocean" // // output public String Substring(int Begin) public String substring(int begin, int end) Este método devuelve una parte del String usado para invocar el método. El primer argumento representa el principio de la sub-cadena (empezando en 0). Si sólo pasamos un argumento, la subcadena devuelta incluirá todos los caracteres hasta el final del String original. Si tiene dos argumentos, la subcadena devuelta terminará con el carácter localizado en la n-aba posición donde n es el segundo argumento. Importante: el segundo argumento no empieza en 0, por lo que si el segundo argumento fuera 7, el último carácter devuelto estará en la posición 7 del String original, cuyo índice es 6. Exam Watch 6.1 Veamos algunos ejemplos: String x = "0123456789"; // as if by magic, the value of each char is the same as its index! System.out.println( x.substring(5) ); // output is "56789" System.out.println( x.substring(5, 8)); // output is "567" public String toLowerCase() Este método devuelte un String con los caracteres del String original pasados a minúsculas. Por ejemplo: String x = "A New Moon"; System.out.println( x.toLowerCase() ); // output is "a new moon" public String toString() Este método devuelve el valor del String usado para invocar el método. Parece un método que no hace nada pero el método es heredado de Object. Veamos un ejemplo: String x = "big surprise"; System.out.println( x.toString() ); output - reader's exercise public String toUpperCase() Este método devuelte un String con los caracteres del String original pasados a mayúsculas. Por ejemplo: String x = "A New Moon"; System.out.println( x.toUpperCase() ); // output is "A NEW MOON" public String trim() Este método devuelve un String con el valor del String usado para invocar el método pero con los espacios en blanco del principio y del final eliminados. Por ejemplo: String x = " hi "; System, out.println ( x + "x" ); // result is " hi x" System.out.println( x.trim() + "x"); // result is "hix" 1.4. Las Clases StringBuffer y StringBuilder Las clases java.lang.StringBuffer y java.lang.StringBuilder deberían usarse cuando vayamos ha hacer muchas modificaciones a las cadenas. Como hemos visto antes, los String son inmutables por lo que si hacemos muchas // modificaciones acabaremos con muchos objetos abandonados en el pool. StringBuffer vs. StringBuilder La clase StringBuilder se añadió en Java 5. Tiene la misma API que StringBuffer pero no es segura en hilos. Es decir, los métodos no están sincronizados (más sobre seguridad en hilos en el Capítulo 9 Hilos). Sun recomienda el uso de StringBuilder siempre que sea posible (su ejecución es más rápida). El examen podrá usar estas clases en la creación de aplicaciones seguras en hilos y se discutirá en profundidad en el Capítulo 9 - Hilos. Usando StringBuilder y StringBuffer Antes vimos cómo podrían preguntarnos en el examen sobre cuestiones de la inmutabilidad con fragmentos de código como el siguiente: String x = "abc"; x.concat("def"); System.out.println("x = " + x); output is "x = abc" // Puesto que no se ha hecho asignación ninguna, el objeto String creado con concat() fue abandonado al instante. También hemos visto ejemplos como el siguiente: String x = "abc"; x = x.concat("def"); System.out.println("x = " + x); output is "x = abcdef" // Tenemos un nuevo String pero el antoguo String "abc" ha sido perdido en el pool de String, consumiendo memoria. Si estuviéramos usando un StringBuffer en lugar de un String el código sería el siguiente: StringBuffer sb = new StringBuffer("abc"); sb.append("def"); System.out.println("sb = " + sb); // output is "sb = abcdef" Todos los métodos de StringBuffer operan con el valor del objeto que invoca el método. De hecho, estos métodos pueden encadenarse: StringBuilder sb = new StringBuilder("abc"); sb.append("def").reverse().insert(3, "--"); System.out.println( sb ); // output is "fed --- cba" Observa que en cada ejemplo, sólo hay una llamada a 'new'. Cada ejemplo sólo necesita un único objeto StringXxx para ejecutarse. 1.5. Métodos Importantes en las Clases StringBuffer y StringBuilder Los siguientes métodos devuelven un objeto StringXxx con el valor del argumento añadido al valor del objeto que invoca el método. public synchronized StringBuffer append(String s) Como hemos visto antes, este método actualizará el valor del objeto que lo invocó, sin importar si el valor devuelto es asignado a una variable. Este método puede tomar varios tipos de argumentos, incluyendo boolean, char, double, float, int, long y otros, pero el más usado en el examen será un argumento String. Por ejemplo: StringBuffer sb = new StringBuffer("set "); sb.append("point"); System.out.println(sb); // output is "set point" StringBuffer sb2 = new StringBuffer("pi = "); sb2.append(3.14159f); System.out.println(sb2); // output is "pi = 3.14159" public StringBuilder delete(int start, int end) Este método devuelve un objeto StringBuilder y actualiza el valor del objeto que invocó el método. En ambos casos, una subcadena es eliminada del objeto original. El índice inicial de la subcadena eliminada es definido por el primer argumento (empezando en 0) y el índice final de la subcadena a eliminar es definido por el segundo argumento (¡empezando en 1!). Veamos el siguiente ejemplo detalladamente: StringBuilder sb = new StringBuilder("0123456789"); System.out.println(sb.delete(4,6)); // output is "01236789" Exam Watch 6.2 public StringBuilder insert(int offset, String s) Este método devuelve un objeto y actualiza el valor del objeto StringBuilder que invocó el método. En ambos casos, el String pasado como argumento se inserta en el StringBuilder original empezando en la localización representada por un offset del primer argumento (el offset empieza en 0). Al igual que antes, pueden pasarse otros tipos de datos en el segundo argumento (boolean, char, double, float, int, long, etc.). StringBuilder sb = new StringBuilder("01234567"); sb.insert(4, "---"); System.out.println( sb ); output is "0123---4567" // public synchronized StringBuffer reverse() Este método devuelve un objeto StringBuffer y actualiza el valor del objeto que invoca al método. En ambos casos, los caracteres en el StringBuffer se invierten. StringBuffer s = new StringBuffer("A man a plan a canal Panama"); sb.reverse(); System.out.println(sb); // output: "amanaP lanac a nalp a nam A" public String toString() Este método devuelve el valor del objeto StringBuffer que invoca la llamada como un String: StringBuffer sb = new StringBuffer("test string"); System.out.println( sb.toString() ); // output is "test string" Lo más importante de los StringBuffers y los StringBuilders es que, al contrario de los String, el valor de los objetos pueden cambiarse. Exam Watch 6.3 2. Objetivo de Certificación 3.2 Manejo de Ficheros y I/O Dado un escenario con navegación entre ficheros, lectura de ficheros o escritura de ficheros, desarrollar la solución correcta mediante las siguientes clases (algunas veces en combinación) del paquete java.io: BufferedReader, BufferedWriter, File, FileReader, FileWriter y PrintWriter. I/O tiene una historia extraña con la certificación SCJP. Fue incluido en todas las versiones del examen hasta la 1.2 inclusive, luego se eliminó en el examen de la 1.4 y fue introducido de nuevo para Java 5. I/O es un gran tema en general y las APIs de Java que tratan con I/O de una forma u otra son enormes. Una discusión general sobre I/O podría incluir temas como I/O de ficheros, de consola, de hilos, de alto rendimiento, orientado a bytes, orientado a caracteres, filtrado y envoltura, serialización y algunos más. Afortunadamente, los temas de I/O incluidos en el examen se restringen a I/O de ficheros para caracteres y serialización. A continuación se muestra un resumen de las clases de I/O que necesitamos comprender para el examen: File: La API dice que la clase FILE es "Una representación abstracta de rutas a ficheros y directorios". La clase File no se usa para leer o escribir datos. Es usada para trabajar a un alto nivel, creando nuevos ficheros vacíos, búsqueda de ficheros, eliminación de ficheros, creación de directorio y trabajos con rutas. FileReader: Esta clase se usa para leer ficheros de caracteres. Tiene métodos read() que trabajan a bajo nivel, permitiendo leer caracteres individuales, el flujo completo de caracteres o un número fijo de ellos. Los FileReader son normalmente envueltos por objetos de alto nivel como BufferedReader, que mejora el rendimiento y proporciona mejores formas de trabajar con los datos. BufferedReader: Esta clase se usa para hacer las clases Reader de bajo nivel (como FileReader) más eficientes y fáciles de usar. Comparado con los FileReader, esta clase lee grandes trozos de datos de un fichero de una vez y mantiene dichos datos en un buffer. Cuando preguntamos por el siguiente carácter o línea de datos, éstos son recuperados del buffer, que minimiza el número de veces que se realizan operaciones de lectura. Además, BufferedReader proporciona métodos más convenientes como readLine(), que nos permite obtener la siguiente línea de caracteres de un fichero. FileWriter: Esta clase se usa para escribir en ficheros de caracteres. Sus métodos write() nos permiten escribir caracteres o String en un fichero. Estos objetos suelen envolverse por objetos Writer de alto nivel como BufferedWriter o PrintWriter, que proporcionan mejor rendimiendo y métodos más flexibles. BufferedWriter: Esta clase se usa para que clases de más bajo nivel, como FileWriter, sean más eficientes y fáciles de usar. Escriben grandes cantidades de datos en un fichero de una vez, minimizando el número de veces que se realizan las operaciones de escritura. Además proporciona el método newLine() que hace más fácil crear separadores de línea específicos de la plataforma de forma automática. PrintWriter: Esta clase ha sido mejorada de forma significante en Java 5. Gracias a los nuevos métodos y constructores (como construir un PrintWriter a partir de un File o un String), podríamos encontrar que podemos usar esta clase en sitios donde antes se necesitaba un Writer envuelto con un FileWriter y/o un BufferedWriter. Los nuevos métodos como format(), printf() y append() hacen estos objetos más flexibles y potentes. Exam Watch 6.4 2.1. Creando Ficheros Mediante la Clase File Los objetos de tipo File se usan para representar los ficheros (no los datos) o directorios que existen físicamente. Veamos unos ejemplos de creación, escritura y lectura. En primer lugar crearemos un nuevo fichero y escribiremos unas cuantas líneas de datos en él: import java.io.*; // The Java 5 exam focuses on classes from java.io class Writer1 { public static void main(String [] args) { File file = new File("fileWrite1.txt"); // There's no file yet! } } Si compilamos y ejecutamos este programa, cuando miremos los contenidos del directorio veremos que no existe ningún fichero llamado fileWrite1.txt. Cuando creamos una nueva instancia de la clase File, no estamos creando un fichero sino un nombre de fichero. Una vez que tenemos un objeto File, hay varias formas de crear el fichero. Veamos que podemos hacer con un objeto File: import java.io.*; class Writer1 { public static void main(String [] args) { try { // warning: exceptions possible boolean newFile = false; File file = new File("fileWritel.txt"); // it's only an object System.out.println(file.exists()); // look for a real file newFile = file.createNewFile(); // maybe create a file! System.out.println(newFile); // already there? System.out.println(file.exists()); // look again } catch(IOException e) { } } } Un par de apuntes. Primero observar que se ha puesto el código de creación del fichero en un bloque try/catch. Esto suele ser así para la mayoría del código de I/O (suelen ser cosas de cierto riesgo). De momento se mantiene simple e ignoramos las excepciones, pero seguimos necesitanto seguir la regla de manejar o declarar desde que la mayoría de los métodos I/O declaran excepciones checked. Más tarde se hablará más sobre las excepciones de I/O. Veamos los métodos usados en el código: boolean exists() Devuelve 'true' si puede encontrar el fichero actual boolean createNewfile() Crea un nuevo fichero si no existe otro Exam Watch 6.5 Este código produce la salida: false true true 2.2. Usando FileWriter y FileReader Y también produce un fichero vacío en el directorio actual. Si ejecutamos el código una segunda vez, obtendremos la salida: true false true Veamos estos conjuntos de salida producida: Primera Ejecución: La primera llamada a exists() devuelve 'false', recuerda que llamar a new File() no crea el fichero en el disco. El método createNewFile() crea el fichero actual y devuelve 'true' indicando que se ha creado un nuevo fichero y que no existía uno anteriormente. Por último, se llama de nuevo a exists() devolviendo 'true' indicando que existe el fichero. Segunda Ejecución: La primera llamada a exists() devuelve 'true' porque se construyó el fichero en la primera ejecución. Luego createNewFile() devuelve 'false' porque el método no crea el fichero. Por supuesto, la última llamada a exists() devuelve 'true'. En la práctica, probablemente no usaremos estas clases sin "envolverlas". Dicho esto, veamos algo de código con estas clases: import java.io.*; class Writer2 { public static void main(String [] args) { char[] in = new char[50]; // to store input int size = 0; try { File file = new File("fileWrite2.txt");// just an object FileWriter fw = new FileWriter(file); // create an actual file & a FileWriter obj fw.write("howdy\nfolks\n"); // write characters to the file fw.flush(); // flush before closing fw.close(); // close file when done FileReader fr = new FileReader(file); // create a FileReader object size = fr.read(in); // read the whole file! System.out.print(size + " "); // how many bytes read for(char c : in) // print the array System.out.print(c); fr.close(); again, always close } catch(IOException } } // e) { } Que produce la siguiente salida: 12 howdy folks problemas si no sabemos el tamaño de antemano. Debido a estas limitaciones, normalmente querremos usar clases I/O de más alto nivel como BufferedWriter o BufferedReader en combinación con FileWriter o FileReader. Esto fue lo que pasó: 1. FileWriter fw = new FileWriter(file) hace tres cosas: 1. Crea una variable de referencia fw de tipo FileWriter 2. Crea un objeto FileWriter y lo asigna a fw 3. Crea un fichero vacío en el disco 2. Escribimos 12 caracteres en el fichero con el método write() y luego se llama a flush() y close() 3. Se crea un nuevo objeto FileReader, que se habre para lectura 4. El método read() lee todo el fichero, un caracter cada vez y lo pone en la variable in (de tipo char[]) 5. Se imprime el número de caracteres que leemos y lo recorremos en un bucle imprimiendo cada caracter 6. Por último se cierra el fichero Veamos que hacen los métodos flush y close. Como sabemos, cuando trabajamos con streams, parte de los datos enviados se almacenan en el buffer y no podemos saber exactamente cuando serán enviados. Invocar al método flush garantizamos que los últimos datos enviados al stream se escriben en el fichero. Por otro lado, siempre que usemos un fichero (para lectura o escritura) deberíamos invocar al método close. La llamada a dicho método liberará los recuros. Ahora volvamos al ejemplo. El programa funciona, pero tiene un par de cosas regular: 1. Cuando estamos escribiendo datos, insertamos manualmente los saltos de línea (\n) 2. Cuando estamos leyendo los datos, lo ponemos en un array de caracteres. Como es un array, tenemos que declarar antes su tamaño por lo que podríamos tener 2.3. Combinando Clases I/O El sistema I/O de Java fue diseñado con la idea de combinar clases. Normalmente esto se conoce como wrapping y a veces chaining. El paquete java.io contiene unas 50 clases, 10 interfaces y 15 excepciones. Cada clase en el paquete tiene un propósito muy específico (creando alta cohesión) y las clases están diseñadas para ser combinadas en muchas formas, para manejar una gran variedad de situaciones. Probablemente estaremos confusos acerca de la API java.io, intentando saber qué clases necesitamos y cómo unirlas. Para el examen sólo tendremos en cuenta las siguientes clases: Table 6-1: java.io Mini API Key Extend Constructor( java.io Class Key Methods s From s) Arguments createNewFil e() delete() File, String exists() File Object String isDirectory() String, String isFile() list() mkdir() renameTo() close() File FileWriter Writer flush() String write() close() BufferedWrit flush() Writer Writer er newLine() write() File (as of close() PrintWriter Writer Java 5) flush() String (as of format()*, Java 5) printf()* OutputStream print(), Writer println() write() File FileReader Reader read() String BufferedRead read() Reader Reader er readLine() *Discussed later Ahora nos plantearemos la mejor forma de escribir datos en un fichero y leer de nuevo los contenidos en memora. Empezaremos por la tarea de escribir datos en un fichero, a continuación se muestra el proceso para determinar que clases necesitaremos y cómo combinarlas: 1. Sabemos que necesitamos un objeto File. Por lo que, sin tener en cuenta el resto de clases que vamos a usar, una de ellas deberá tener un constructor que tome un objeto de tipo File. 2. Encontrar el mejor método. Si vemos la tabla anterior, podemos ver que BufferedWriter tiene un método newLine() lo que nos ahorra tener que insertar el carácter de salto de línea. Pero si seguimos avanzando encontramos PrintWriter que tiene un método llamado println(). Esa es la mejor opción. 3. Cuando vemos los constructores de PrintWriter, veremos que podemos construir un objeto de ese tipo si tenemos un objeto de tipo File. Podemos crear un objeto como se muestra a continuación: File file = new File("fileWrite2.txt"); // create a File PrintWriter pw = new PrintWriter(file); // pass file to the PrintWriter constructor Nota: Antes de Java 5, PrintWriter no tenía constructores que tomaran un String o un Fichero. Hay otra forma de resolver el problema: Primero, sabemos que crearemos un objeto File al final de la cadena y que queremos un PrintWriter como objeto final. Podemos ver que PrintWriter puede construirse mediante un objeto Writer. Writer no aparece en la tabla pero FileWriter extiende a dicha clase y tiene las dos características que estamos buscando: 1. Puede construirse usando un File 2. Extiende Writer Dada toda esta información, tendremos el siguiente código (recuerda que es un ejemplo de Java 1.4): File file = new File("fileWrite2.txt"); // create a File object FileWriter fw = new FileWriter(file); // create a FileWriter that will send its output to a File PrintWriter pw = new PrintWriter(fw); // create a PrintWriter that will send its output to a Writer pw.println("howdy"); // write the data pw.println("folks"); En este punto debería ser fácil juntar todo el código para leer datos del fichero. De nuevo, mirando la tabla vemos un método readLine() que parece que es la mejor forma. Haciendo un proceso similar tendríamos: File file = new File("fileWrite2.txt"); // create a File object AND open "fileWrite2.txt" FileReader fr = new FileReader(file); // create a FileReader to get data from 'file' BufferedReader br = new BufferedReader(fr); // create a BufferReader to get its data from a Reader String data = br.readLine(); // read some data Exam Watch 6.6 2.4. Trabajando con Ficheros y Directorios Antes hemos dicho que la clase File puede usarse para crear ficheros y directorios. Además, los métodos de File pueden usarse para eliminar ficheros, renombrarlos, determinar si existen, crear ficheros temporales, cambiar los atributos de los ficheors y diferenciar entre ficheros y directorios. Lo más confuso puede ser que la clase File se usa para representar tanto un fichero como un directorio. Dijimos antes que la sentencia: File file = new File("foo"); siempre crea un objeto File y luego hace dos cosas: 1. Si "foo" NO existe, no se crea ningún fichero 2. Si "foo" SÍ existe, entonces el nuevo objeto File apunta al fichero existente Tenemos dos formas de crear un fichero: 1. Invocar al método createNewFile() de un objeto File. Por ejemplo: File file = new File("foo"); // no file yet file.createNewFile(); // make a file, "foo" which is assigned to 'file' subdirectorio. A continuación se muestra una forma de escribir datos al fichero: PrintWriter pw = new Printwriter(myFile); pw.println("new stuff"); pw.flush(); pw.close(); Hay que tener cuidado cuando estemos creando nuevos directorios. Los constructores Reader o Writer crean el fichero automáticamente si no existe, pero esto no ocurre con directorios: File myDir = new File("mydir"); // myDir.mkdir(); call to mkdir() omitted! File myFile = new File(myDir, "myFile.txt"); myFile.createNewFile(); exception if no mkdir! // // Esto generará una excepción como la siguiente: 1. Crear in Reader o un Writer o un Stream. Específicamente, crear un FileReader, un FileWriter, un PrintWriter, un FileInputStream, o un FileOutputStream. Siempre que creamos una instancia de alguna de estas clases, automáticamente se creará un fichero, a no ser que ya exista uno. Por ejemplo: File file = new File("foo"); // no file yet PrintWriter pw = new PrintWriter(file); // make a PrintWriter object AND make a file, "foo" to which // 'file' is assigned, AND assign 'pw' to the PrintWriter Crear un directorio es similar a crear un fichero. De nuevo, usaremos la convención para referirinos a un objeto de tipo File que represente un directorio como objeto Directory File (D en mayúsculas). Crear un directorio es un proceso de dos pasos, primero creamos el objeto File y luego llamamos al método mkdir() para crear el directorio: File myDir = new File("mydir"); create an object myDir.mkdir(); create an actual directory // // Una vez que tenemos un directorio, ponemos lo ficheros y trabajamos con dichos ficheros: File myFile = new File(myDir, "myFile.txt"); myFile.createNewFile(); Este código crea un nuevo fichero en un java.io.IOException: No such file or directory Podemos asignar un objeto File a un fichero o directorio existene. Por ejemplo, si asumimos que tenemos un subdirectorio llamado existingDir en el cual reside un fichero existingDirFile.txt que contiene varias líneas de texto. Cuando ejecutamos el siguiente código: File existingDir = new File("existingDir"); // assign a dir System.out.println(existingDir.isDirectory ()); File existingDirFile = new File(existingDir, "existingDirFile.txt"); // assign a file System.out.println (existingDirFile.isFile()); FileReader fr = new FileReader(existingDirFile); BufferedReader br = new BufferedReader(fr); // make a Reader String s; while( (s = br.readLine()) != null) // read data System.out.printIn(s); br.close(); Generará la siguiente salida: true true existing sub-dir data line 2 of text line 3 of text Hay que tener cuidado con lo que devuelve el método readLine(). Cuando no quedan datos devuelve un null (señal de que debemos de parar de leer). Observa que no se invoca al método flush(). Cuando leemos un fichero no es necesario, por lo que no encontraremos un métdo flush() en ninguna clase Reader. Además de crear archivos, la clase File nos permite renombrar y eliminar ficheros. El siguiente código demuestra las ventajas e inconvenientes de eliminar ficheros y directorios (a través de delete()) y renombar ficheros y directorios (renameTo()): File delDir = new File("deldir"); make a directory delDir.mkdir(); // File delFile1 = new File(delDir, "delFile1.txt"); // add file to directory delFile1.createNewFile(); File delFile2 = new File(delDir, "delFile2.txt"); // add file to directory delFile2.createNewFile(); delFile1.delete(); delete a file found found found found found // delDir is false y crea un directorio llamado "newDir" que contiene un fichero llamado "newName.txt". A continuación se muestran unas reglas: // // dir1 dir2 dir3 file1.txt file2.txt // Esto produce la salida: String[] files = new String[100]; File search = new File("searchThis"); files = search.list(); create the list for(String fn : files) iterate through it System.out.println("found " + fn); Obtendremos la siguiente salida: System.out.println("delDir is " + delDir.delete()); // attempt to delete the directory File newName = new File(delDir, "newName.txt"); // a new object delFile2.renameTo(newName); // rename file File newDir = new File("newDir"); rename directory delDir.renameTo(newDir); una cosa más que discutir y trata sobre cómo buscar un fichero. Asumiendo que tenemos un directorio llamado searchThis en el que queremos hacer la búsqueda, el siguiente código usa el método File.list() para crear un array de ficheros y directorios, que usaremos en el buble for para iterar a través de el e imprimir los valores: delete() No podemos eliminar un directorio que no esté vacío, por lo que la invocación a delDir.delete() falla renameTo() Debemos dar al objeto existente File un nuevo objeto File con el nuevo nombre que queremos. Si newName es null obtendremos un NullPointerException renameTo() Es correcto renombrar un directorio, aunque no esté vacío Hay mucho más que aprender acerca del uso del paquete java.io, pero para el examen sólo tenemos 3. Objetivo de Certificación 3.3 Serialización Desarrollar código que serialice y/o de-serialice objetos mediante las siguientes APIs de java.io: DataInputStream, DataOutputStream, FilelnputStream, FileOutputStream, ObjectInputStream, ObjectOutputStream y Serializable. La serialización nos permite hacer lo siguiente: "salva el estado de los objetos excepto las variables que he marcado como transient". 3.1. Trabajando con ObjectOutputStream y ObjectInputStream La serialización básica se hace simplemente con dos métodos: uno para serializar objetos y escribirlos a un stream y otro para leer el stream y de-serializar objetos. ObjectOutputStream.writeObject() serialize and write // ObjectInputStream.readObject() and deserialize // read Las clases java.io.ObjectOutputStream y java.io.ObjectInputStream son consideradas como clases de alto-nivel en el paquete java.io y como aprendimos antes, esto significa que podemos envolver con ellas clases de bajo-nivel, como java.io.FileOutputStream y java.io.FilelnputStream. A continuación se muestra un ejemplo que crea un objeto (Cat), lo serializa y lo de-serializa: import java.io.*; class Cat implements Serializable { } //1 public class SerializeCat { public static void main(String[] args) { Cat c = new Cat(); // 2 try { FileOutputStream fs = new FileOutputStream("testSer.ser"); ObjectOutputStream os = new ObjectOutputStream(fs); os.writeObject(c); // 3 os.close(); } catch (Exception e) { e.printStackTrace (); } try { FileInputStream fis = new FileInputStream("testSer.ser"); ObjectInputStream ois = new ObjectInputStream(fis); c = (Cat) ois.readObject(); ois.close(); } catch (Exception e) { e.printStackTrace(); } } } // 4 Veamos los puntos claves del ejemplo: 1. Declaramos la clase Cat que implementa la interfaz Serializable. Serializable es una interfaz de marca. No tiene métodos que implementar. 2. Creamos un nuevo objeto Cat que sabemos que es serializable. 3. Serializamos el objeto Cat c invocando al método writeObject. Tenemos que hacer varias cosas antes de serializar el objeto. Primero tenemos que poner todo nuestro código en un bloque try/catch. A continuación creamos un FileOutputStream para escribir el objeto en él. Luego envolvemos el FileOutputStream en un ObjectOutputStream, que es la clase que hace la serialización. Recuerda que la invocación de writeObject() realiza dos tareas: serializa el objeto y luego escribe el objeto serializado en un fichero. 4. Ahora de-serializamos el objeto Cat invocando al método readObject(). Este método devuelve un Object, por lo que tenemos que hacerle un casting a un Cat. De nuevo tenemos que hacer las típicas cosas de I/O para configurarlo. A continuación se verán aspectos más complejos asociados con la serialización. 3.2. Representación de Objetos ¿Qué significa realmente salvar un objeto?. Si todas las variables son tipos primitivos es muy sencillo pero si alguna variable es una referencia a un objeto no es tan sencillo. Guardar el valor de dicha variable no tiene sentido porque dicho valor es una posición de memoria que en el momento de reataurar el objeto seguramente no apunte al objeto. Veamos un ejemplo sobre cómo trabajan las referencias: class Dog { private Collar theCollar; private int dogSize; public Dog(Collar collar, int size) { theCollar = collar; dogSize = size; } public Collar getCollar() { return theCollar; } } class Collar { private int collarSize; public Collar(int size) { collarSize = size; } public int getCollarSize(} { return collarSize; } } Para crear un Dog, primero creamos un Collar: Collar c = new Collar(3); Luego creamos un Dog y le pasamos un Collar: Dog d = new Dog(c, 8); Ahora si queremos salvar el estado del objeto 'd' también necesitaremos salvar el estado del objeto 'c'. Podría complicarse más aun si los objetos Collar tuvieran una referencia a otros objetos, por ejemplo Color. Afortunadamente, el mecanismo de serialización de Java se preocupa de todo esto. Cuando serializamos un objeto, la propia serialización se preocupa de salvar la representación completa del objeto. Nosotros sólo tendríamos que preocuparnos de serializar el objeto Dog, porque el resto de objetos necesarios para reconstruir dicho objeto se salvarán (y restaurarán) de forma automática mediante la serialización. Recuerda que debemos especificar de forma explícita que el objeto implementa la interfaz serializable. Si queremos salvar los objetos Dog tendríamos que especificarlo: class Dog implements Serializable { // the rest of the code as before // Serializable has no methods to implement } Dog d = new Dog(c, 5); System.out.println("before: collar size is " + d.getCollar().getCollarSize()); try { FileOutputStream fs = new FileOutputStream("testSer.ser"); ObjectOutputStream os = new ObjectOutputStream(fs); os.writeObject(d) ; os.close(); } catch (Exception e) { e.printStackTrace(); } try { FileInputStream fis = new FileInputStream("testSer.ser"); ObjectInputStream ois = new ObjectlnputStream(fis); d = (Dog) ois.readObject(); ois.close(); } catch (Exception e) { e.printStackTrace(); } System.out.println("after: collar size is " Y podremos salvar el objeto de la siguiente forma: import java.io.* ; public class SerializeDog { public static void main(String[] args) { Collar c = new Collar(3); Dog d = new Dog(c, 8); try { FileOutputStream fs = new FileOutputStream("testSer.ser"); ObjectOutputStream os = new ObjectOutputStream(fs); os.writeobject(d); os.close(); } catch (Exception e) { e.printStackTrace(); } } } Pero cuando ejecutamos el código obtenemos una excepción en tiempo de ejecución como la siguiente: java.io.NotSerializableException: Collar Collar TAMBIÉN debe ser Serializable. Debemos hacer lo mismo: class Collar implements Serializable { // same } Por último, el código completo: import java.io.*; public class SerializeDog { public static void main(String [] args) { Collar c = new Collar(3); + d.getCollar() .getCollarSize()); } } class Dog implements Serializable { private Collar theCollar; private int dogSize; public Dog(Collar collar, int size) { theCollar = collar; dogSize = size; } public Collar getCollar() { return theCollar; } } class Collar implements Serializable { private int collarSize; public Collar(int size) { collarSize = size; } public int getCollarSize() { return collarSize; } } El código anterior produce la siguiente salida: before: collar size is 3 after: collar size is 3 Pero ¿qué ocurre si no tenemos acceso al código fuente de Collar?. La primera solución que se nos ocurre es hacer una subclase Serializable de Collar y usar dicha subclase en el código de Dog, pero esta solución no siempre es posible por varias razones: 1. La clase Collar podría ser 'final' O 1. La clase Collar podría hacer referencia a otros objetos no-serializables y sin el conocimiento interno de la clase Collar no seremos capaces de hacer todos los cambios O 1. Hacer una subclase no es una opción por razones relacionadas con nuestro diseño. Aquí es donde entra en juego el modificador 'transient'. Si marcamos la instancia Collar de Dog con 'transient', la serialización no serializará dicha propiedad: class Dog implements Serializable { private **transient** Collar theCollar; // add transient // the rest of the class as before } class Collar { Serializable // same code } durante la serialización y deserialización. Es como si los métodos se hubieran definido en la interfaz Serializable, excepto que no están. Son parte del contrato especial de callback que ofrece el sistema de serialización. Básicamente dice: "Si tu tienes un par de métodos que encajan con esta signatura, dichos métodos se invocarán durante el proceso de serialización/deserialización". Estos métodos nos permiten situarnos en mitad de la serialización y deserialización. Podemos resolver el problema de la siguiente forma: "cuando se esté salvando un Dog, añadir el estado de la variable de Collar (un entero) al stream". Hemos añadido el estado de Collar manualmente aunque el propio Collar no se ha salvado. Por supuesto, necesitaremos restaurar el Collar durante la deserialización parando en medio y diciendo "leeré el entero salvado en el Strem de Dog y lo usaré para crear un nuevo Collar y asignarlo al Dog que está siendo deserializado". // no longer Ahora tenemos un Dog serializable con un collar no serializable. La salida de la clase SerializeDoges: before: collar size is 3 Exception in thread "main" java.lang.NullPointerException ¿Qué podemos hacer ahora? Los dos métodos especiales que debemos definir tienen que tener la siguiente signatura EXACTA: private void writeObject(ObjectOutputstream os) { // your code for saving the Collar variables } private void readObject(Objectlnputstream os) { // your code to read the Collar state, create a new Collar, // and assign it to the Dog } Veamos como debemos cambiar la clase Dog: 3.3. Usar WriteObject y ReadObject Considera el siguiente problema: tenemos un objeto Dog que queremos salvar. Dog tiene un Collar y el estado de Collar debería ser salvado como parte del estado de Dog. Pero Collar no es Serializable, por lo que lo marcamos como 'transient'. Esto significa que cuando deserializamos Dog, vuelve con un Collar null. ¿Qué podemos hacer para que cuando deserialicemos Dog, obtenga un nuevo Collar que encaje con el objeto Dog que fue salvado? La serialización de Java tiene un mecanismo automático para esto en forma de métodos privados que podemos implementar en nuestra clase que, si están presentes, serán invocados automáticamente class Dog implements Serializable { transient private Collar theCollar; // we can't serialize this private int dogSize; public Dog(Collar collar, int size) { theCollar = collar; dogSize = size; } public Collar getCollar() { return theCollar; } private void writeObject(ObjectOutputStream os) { // throws IOException { // 1 try { os.defaultWriteObject(); // 2 os.writeInt(theCollar.getCollarSize()); // 3 } catch (Exception e) { e.printStackTrace(); } } private void readObject(ObjectlnputStream is) { // throws IOException, ClassNotFoundException { // 4 try { is.defaultReadObject(); // 5 theCollar = new Collar(is.readInt()); // 6 } catch (Exception e) { e.printStackTrace(); } } } >>defaultReadObject() y >> >>defaultWriteObject> para que hagan el resto. Podría surgir la pregunta de ¿porqué no son todas las clases Java serializables?. ¿Porqué no lo es Object?. Algunas cosas podrían no ser serializables en Java porque son específicas de la ejecución. Sin embargo, saber que clases son Serializables en la API de Java no es parte del examen. Exam Watch 6.7 3.4. ¿Cómo afecta la Herencia a la Serialización? Veamos el código anterior. 1. Como la mayoría de los métodos relacionados con I/O, estos métodos pueden lanzar excepciones. Podemos declararlas pero lo correcto es mejorarlas. 2. Cuando invocamos a >defaultWriteObject() en el método, estamos diciéndole a la JVM que haga el proceso normal de serialización para este objeto. Este suele ser el procedimiento normal (primero el objeto y luego nuestra personalización).> 3. >En este caso hemos decidido escribir un entero al stream que está creando el Dog serializado. Podemos escribir antes o despuñes de invocar a >>defaultWriteObject(), PERO debemos leer en el mismo orden.> 4. >De nuevo, elegimos manejar las excepciones.> 5. >Cuando se deserializa, el métdo >>defaultReadObject() maneja la deserialización normal.> 6. >Por último, construimos un nuevo objeto Collar. Debemos invocar readInt() después de invocar >> >>defaultReadObject() o los datos del 'stream' podrían estar fuera de sincronización.> ¿Qué ocurre si una superclase no es marcada como Serializable pero la subclase si?. Imaginemos lo siguiente: La razón de usar estos métodos es para salvar parte del estado de un objeto de forma manual. Si queremos podemos escribir y leer todo el estado nosotros mismos pero lo normal es hacer sólo una parte del proceso e invocar a los métodos > Pero esto NO ocurre cuando se deserializa un objeto. En la deserialización, el constructor nos e ejecuta y las variables de instancia NO obtienen su valor inicial asignado. class Animal { } class Dog extends Animal implements Serializable { // the rest of the Dog code } Esto funciona, pero tiene implicaciones serias. Para comprenderlas veamos la diferencia entre un objeto que viene de una deserialización y un objeto creado mediante new. Cuando se construye un objeto con new ocurre lo siguiente: 1. A todas las variables de instancia se les asigna sus valores por defecto 2. Se invoca al constructor que inmediatamente invoca al constructor de la superclase (o a otro constructor sobrecargado hasta que uno invoca al constructor de la superclase). 3. Se completan todos los constructores de la superclase. 4. Se inicializan las variables de instancia con el valor en su declaración. 5. Se completa el constructor. Piensa que los constructores y las asignaciones a las variables de instancia son parte de un proceso de inicialización completo (de hecho se combierten en un método de inicialización en el bytecode). El punto clave es, cuando deserializamos un objeto NO queremos nada de la inicialización normal. Sólo queremos que se reasignen la parte del estado salvada del objeto. Por supuesto si tenemos variables marcadas como 'transient', no serán restauradas a su estado original (a no ser que implementemos los métodos privados defaultXxxxObject()), en su lugar se le dará el valor por defecto. En otras palabras, si tenemos: class Bar implements Serializable { transient int x = 42; } cuando deserializamos la instancia de Bar, la variable 'x' valdrá 0. Las referencias a objetos marcadas como 'transient' serán puestas a nulas, sin importar si fueron inicializadas en la declaración de la clase. Entonces, ¿qué ocurre cuando el objeto es deserializado y la clase del objeto serializado extiende directamente de Objeto sólo tiene clases serializables en su árbol de herencia?. Existe un caso curioso cuando la clase serializable tiene una o más clases no serializables. Volvamos a nuestro ejemplo de Animal no-serializable: class Animal { public String name; } class Dog extends Animal implements Serializable { // the rest of the Dog code } Puesto que Animal NO es serializable, cualquier estado mantenido en la clase Animal, aunque sea heredado por Dog, va a ser restaurado cuando Dog sea deserializado. La razón es que la parte Animal (no serializada) de Dog será reinicializada del mismo modo que si estuviéramos creando un nuevo Dog. Esto significa que todas las cosas que ocurrirían durante la construcción de un objeto, ocurrirán (pero sólo en la parte Animal de Dog). En otras palabras, las variables de instancia de Dog serán serializada y deserializadas correctamente, pero los valores heredados de la superclase Animal volverán a su valor por defecto o a sus valores asignados inicialmente en lugar de los valores que tuvieran en la serialización. Si tenemos una clase serializable, pero su superclase NO es serializable, entonces cualquier variable de instancia que HEREDEMOS de la superclase será reiniciada a los valores que son asignados durante la construcción original del objeto. Esto es debido a que el constructor de la clase no-serializable ¡SE EJECUTARÁ! De hecho, cada constructor A PARTIR del primer constructor de clase no-serializable se ejecutará, puesto que una vez que se ejcuta el primer super constructor, éste invoca su super constructor. Para el examen necesitaremos ser capaz de reconoer qué variables serán o no restauradas a sus valores apropiados cuando se deserializa un objeto por lo que debemos estudiar detenidamente el siguiente código y su salida: import java.io.*; class SuperNotSerial { public static void main(String [] args) { Dog d = new Dog(35, "Fido"); System.out.println("before: " + d.name + " " + d.weight); try { FileOutputStream fs = new FileOutputStream("testSer.ser"}; ObjectOutputStream os = new ObjectOutputStream(fs); os.writeObject(d); os.close (); } catch (Exception e) { e.printStackTrace(); } try { FileInputStream fis = new FileInputstream("testSer.ser"); ObjectInputStream ois = new ObjectInputStream(fis); d = (Dog) ois.readObject(); ois.close(); } catch (Exception e) { e.printStackTrace(); } System.out.println("after: " + d.name + " " + d.weight); } } class Dog extends Animal implements Serializable { String name; Dog(int w, String n) { weight = w; // inherited name = n; // not inherited } } class Animal { // not serializable ! int weight = 42; } El código produce la siguiente salida: before: Fido 35 after: Fido 42 El punto clave es que, como Animal no es serializable, cuando se deserializa Dog, el constructor de Animal se ejecutará y reiniciará la variable 'weight' heredada por Dog. Exam Watch 6.8 3.5. La Serialización no vale para los Estáticos Por último, podemos observar que SÓLO hemos hablado de variables de instancia, no variables estáticas. El estado de una variable estática podría ser importante, pero no es parte del estado de la instancia. La serialización se aplica a OBJECTOS, no ha clases. Las variables estáticas NUNCA son salvadas como parte del estado del objeto. Exam Watch 6.9 Los problemas de versiones pueden ocurrir en el mundo real. Si salvamos un obejto Dog usando una versión de la clase pero intentamos deserializarlo usando una versión diferente, la deserialización podría fallar. Consulta la API de Java para los detalles sobre los problemas de versiones y sus soluciones. usar el locale por defecto o uno específico. Describir el propósito y usar la clase java.util.Locale. La API de Java proporciona un conjunto amplio de clases para trabjar con fechas, números y divisas. El examen probará nuestro conocimiento de las clases y métodos básicos que usaremos para trabajar con fechas. Cuando acabemos la sección debemos ser capaces de crear objetos Date y Dateformat, convertir String a Date y a la inversa, realizar funciones de calendario, imprimir de forma correcta valores de moneda formateados y hacer todo esto para todas las localidades del mundo. De hecho, una gran parte de esta sección fue añadida para conocer las bases de la internacionalización (i18n). 4.1. Trabajando cn Fechas, Números y Divisas Si queremos trabajar con fechas de todo el mundo, necesitamos familiarizarnos con al menos 4 clases de los paquetes java.text y java.util. Veamos una descripción de las clases más importantes: 4. Objetivo de Certificación 3.4. - Fechas, Números y Divisas Usar las APIs estándar de J2SE en el paquete java.text para formatear o parsear de forma correcta fechas, números y valores de moneda para un locale específico; y, dado un escenario, determinar los métodos apropiados si queremos java.util.Date La mayoría de los métodos de esta clase han sido deprecados, pero podemos usar esta clase como puente entre las clases Calendar y DateFormat. Una instancia de Date representa una fecha y tiempo mutable en milisegundos. java.util.Calendar Esta clase proporciona una gran variedad de métodos que nos ayudan a convertir y manipular fechas y horas. java.text.DateFormat Esta clase se usa para formatear fechas no sólo proporcionando estilos como "01/01/08" o "January 1, 1970," sino también para formatear fechas para un gran número de locales. Java.text.NumberFormat Esta clase se usa para formatear números y divisas para locales. Java.util.Locale Esta clase se susa en conjunción con DateFormat y NumberFormat para formatear fechas, números y divisas para locales específicos. Organizando las Clases Relacionadas con Date y Number Cuando trabajamos con fechas y números, podríamos usar varias clases juntas. Es importante comprender cómo se relacionan las clases y cuando usarlas en conjunción. La siguiente tabla proporciona un vistazo rápido sobre los casos de uso más comunes relacionados con fechas y números así como soluciones usando las clases. Casos Pasos de uso Obtener 1. Crear un Date: Date d = new Date(); la fecha 2. Obtener su valor: String s = y hora d.toString(); actuales Obtener un objeto que nos permita 1. Crear un Calender: Calender c = realizar Calender.getInstance(); cálculos 2. Usar c.add(...) y c.roll(...) para realizar de hora manipulaciones de hora y fecha y fecha en nuestro local Obtener un 1. Crear un Locale: objeto Locale loc = new Locale que nos (language); // o permite Locale loc = new Locale (language, realizar country); cálculos 2. Crear un Calendar para dicho locale: de hora Calendar c = Calendar.getInstance(loc) y fecha 3. Usar c.add(...) y c.roll(...) para realizar en un manipulaciones de hora y fecha locale diferente Obtener 1. Crear un Calendar: Calendar c = un objeto Calendar.getInstance(); que nos 2. Crear un Locale para cada localización: permite Locale loc = new Locale(...); Convertir nuestor Calendar a un Date: realizar 3. Date d = c.getTime(); cálculos 4. Crear un DateFormat para cada Locale: de fecha DateFormat df = y hora y DateFormat.getDateInstance (style, loc); luego formatea 5. Usar el método format() para crear r la fechas formateadas: salida en String s = df.format(d); diferente s locales con diferente s estilos de datos Obtener un objeto 1. Crear un Locale para cada localización: Locale loc = new Locale(...); que nos 2. Crear un NumberFormat: permita NumberFormat nf = formatea NumberFormat.getInstance(loc); NumberFormat nf = r números NumberFormat.getCurrencyInstance(l o divisas oc); 3. Usar el método format() para crear la a través salida formateada: de String s = nf.format(someNumber); diferente s locales La Clase Date La API de esta clase no hace un buen trabajo manejando situaciones de i18n y localización. Actualmente la mayoría de sus métodos están deprecados y para la mayoría de los propósitos, usaremos la clase Calendar. Esta clase está en el examen por varias razones: podríamos encontrar su uso en código antiguo, es muy fácil de usar si lo que queremos es una forma rápida y sucia de obtener la fecha actual, es buena cuando queremos una hora universal que no está afectada por zonas horarias y, por último, la usaremos como puente temporal para formatear un objeto Calendar usando la clase DateFormat. Una instancia de la clase Date representa una única fecha y hora. De forma interna, la fecha y hora se almacena como un long. Este long almacena el número de milisegundos entre la fecha representada y el 01/01/1970. Es un número muy grande. Veamos la fecha si le pasamos un trillón de milisegundos: import java.util.*; class TestDates { public static void main(String[] args) { Date d1 = new Date(1000000000000L); // a trillion! System.out.println("1st date " + d1.tostring()); } } En una máquina vistual con el locale US, la salida sería: 1st date Sat Sep OB 19:46:40 MDT 2001 Ya sabemos que hay un trillón de milisegundos cada 31 y 2/3 de años. Aunque muchos métodos están deprecados, sigue siendo aceptable usar los métodos getTime y setTime, coomo se muestra a continuación. Añadams una hora a la instancia Date del ejemplo anterior: import java.util.*; class TestDates public static void main(String[] args) { Date d1 = new Date(1000000000000L); // a trillion! System.out.println("1st date " + d1.toString()); d1.setTime(d1.getTime() + 3600000); // 3600000 millis / hour System.out.println("new time " + d1.toString()); } } Que produce: 1st date Sat Sep 08 19:46:40 MDT 2001 new time Sat Sep 08 20:46:40 MDT 2001 Observa que los métodos usan la escala de milisegundos. Si queremos manipular fechas mediante la clase Date, ésta es nuestra única opción. Por ahora, la única otra cosa que debemos saber es que para crear una fecha que represente "ahora" se hace mediante el constructor sin argumentos: Date now = new Date(); La Clase Calendar La clase Calendar fue diseñada para facilitar la manipulación de fechas. Aunque la clase Calendar tiene muchos campos y métodos, una vez que hemos visto unos cuantos, el resto trabaja de la misma manera. Cuando intentamos usar la clase Calendar por primera notaremos que es una clase abstracta. No podemos hacer lo siguiente: Calendar c = new Calendar(); Calendar is abstract[[code]] // illegal, Para crear una instancia Calendar, tenemos que usar uno de los métodos estáticos de factoría sobrecargados getInstance(): [[code format="java5"]] Calendar cal = Calendar.getInstance(); Cuando obtenemos una referencia Calendar como 'cal', nuestra variable de referencia está apuntando a una instancia de una subclase concreta de Calendar. No podemos estar seguros de qué subclase obtendremos (seguramente GregorianCalendar) pero no tenemos que preocuparnos de eso. Nosotros usaremos la API de Calendar. Ahora que tenemos una instancia de Calendar, volvamos a nuestro ejemplo anterior y veamos en qué día de la semana estamos. A continuación se añadirá un mes a la fecha: import java.util.*; class Dates2 { public static void main(String[] args) { Date d1 = new Date(1000000000000L); System.out.println("1st date " + d1.toString()); Calendar c = Calendar. getInstance(); c.setTime(d1); #1 // if(c.SUNDAY == c.getFirstDayOfWeek()) // #2 System.out.println("Sunday is the first day of the week"); System.out.println("trillionth milli day of week is " + c.get(c.DAY_OF_WEEK)); // #3 c. add(Calendar. MONTH, 1); // #4 y #5 Date d2 = c.getTime(); // #6 System.out.println("new date " + d2.toString() ); } } Lo que produce la siguiente salida: 1st date Sat Sep 08 19:46:40 MDT 2001 Sunday is the first day of the week trillionth milli day of week is 7 new date Mon Oct 08 20:46:40 MDT 2001 Veamos qué significan cada una de las líneas marcadas: 1. Asignamos el objeto Date d1 a la instancia Calendar c 2. Usaremos el campo SUNDAY de Calendar para determinar si, para nuestra JVM, SUNDAY se considera el primer día de la semana. (En algunos locales, el primer día de la semana es MONDAY). La clase Calendar proporciona campos para todos los días, meses, días del mes, días del año, etc. 3. Usamos el campo DAY_OF_WEEK para encontrar el día de la semana en el que nos encontramos. 4. Hasta ahora hemos usado métodos getter y setter. Ahora se usará el método add(). Este método nos permite añadir o substraer unidades de tiempo apropiadas para cualquier campo de Calendar que especifiquemos. Por ejemplo: 5. c.add(Calendar.HOUR, -4); // subtract 4 hours from c's value 6. c.add(Calendar.YEAR, 2); // add 2 years to c's value 7. c.add(Calendar.DAY_OF_WEEK, -2); // subtract two days from c's value 1. Convertir el valor de 'c' de vuelta a una instancia de Date. El otro método de Calendar que deberíamos conocer es el método roll(). Este método actúa como el método add(), con la excepción de que cuando se incrementa o decrementa una parte de Date, las grandes partes de Date no serán incrementadas o decrementadas. Por ejemplo: // assume c is October 8, 2001 c.roll(Calendar.MONTH, 9); // notice the year in the output Date d4 = c.getTime(); System.out.println("new date " + d4.toString() ); La salida será la siguiente: new date Fri Jul 08 19:46:40 MDT 2001 Observa que no ha cambiado el año, aunque hayamos añadido 9 meses a la fecha. En un modo similar, invocando roll() con HOUR no cambiará la fecha, el mes o el año. Para el examen no tenemos que memorizar los campos de la clase Calendar. Si los necesitamos para una pregunta serán proporcionados como parte de la misma. La Clase DateFormat Una vez aprendido cómo crear fechas y manipularlas, veamos cómo formatearlas. A continuación se muestra un primer ejemplo: import java.text.*; import java.util.*; class Dates3 { public static void main(String[] args) { Date d1 = new Date(1000000000000L); DateFormat[] dfa = new DateFormat[6]; dfa[0] = DateFormat.getInstance(); dfa[1] = DateFormat.getDateInstance(); dfa[2] = DateFormat.getDateInstance(DateFormat.SHOR T); dfa[3] = DateFormat.getDateInstance(DateFormat.MEDI UM); dfa[4] = DateFormat.getDateInstance(DateFormat.LONG ); dfa[5] = DateFormat.getDateInstance(DateFormat.FULL ); for(DateFormat df : dfa) System.out.println(df.format(d1)); } } Que en nuestra JVM produce: 9/8/01 7:46 PM Sep 8, 2001 9/8/01 Sep 8, 2001 September 8, 2001 Saturday, September 8, 2001 Examinando el código podemos ver un par de cosas. Lo primero es que DateFormat es otra clase abstracta, por lo que no podemos usar 'new' para crear instancias de DateFormat. En este caso hemos usado dos métodos de factoría, getInstance() y getDateInstance(). Observa que getDateInstance() está sobrecargado. Cuando veamos los locales se verá otra versión de getDateInstance() que necesitaremos para el examen. A continuación, hemos usado campos estáticos de la clase DateFormat para personalizar nuestras instancias de DateFormat. Cada uno de estos campos representa un estilo. En este caso parece que la version getDateInstance() sin argumentos nos da el mismo estilo que la versión MEDIUM del método, pero esto no es una regla. Por último, hemos suado el método format() para crear Strings que representan de forma correcta las versiones formateadas de Date. El úlitmo método (parse()) toma un String formateado en el estilo de la instancia DateFormat y lo convierte a un objeto Date. Como podemos imaginar, esta operación es de riesgo puesto que el método podría recibir fácilmente un String mal formado. Debido a esto, parse() puede lanzar una ParseException. El siguiente código crea una instancia Date, usa DateFormat.format() para convertirla a String y luego usa DateFormat.parse() para cambiarla de nuevo a Date: Date d1 = new Date(1000000000000L); System.out.println("d1 = " + d1.tostring()); DateFormat df = DateFormat.getDateInstance( DateFormat.SHORT); String s = df.format(d1); System.out.println(s); try { Date d2 = df.parse(s); System.out.println("parsed = " + d2.toString()); } catch (ParseException pe) { System.out.println("parse exc"); } Locale(String language) Locale(String language, String country) El argumento 'language' representa un código de lenguaje ISO 639, por ejemplo si queremos formatear nuestras fechas y números en Wallom (lenguaje usado en el sur de Bélgica) tenemos que usar "wa" como argumento 'language'. Existen unos 500 códigos, incluyendo uno para Klingon ("thl"), aunque desafortunadamente Java no proporciona soporte para el locale Klingon. No tenemos que memoriazar todos los códigos (unos 240) para el examen. Veamos cómo podríamos usar estos códigos. Si queremos representar italiano básico en nuestra aplicación, todo lo que necesitamos es el código del lenguaje. Por otro lado, si queremos representar el Italiano usado en Suiza, tenemos que indicar que el país es Suiza: Locale locPT = new Locale("it"); Italian Locale locBR = new Locale("it", "CH"); Switzerland // // Que produce la siguiente salida: d1 = Sat Sep 08 19:46:40 MDT 2001 9/8/01 parsed = Sat Sep 08 00:00:00 MDT 2001 Observa que al usar el estilo SHORT, perdemos precisión cuando convertimos el Date aString. La pérdida de precisión se hace palpable cuando convertimos de vuelta el String a Date y vuelve de las 7:46 a la media noche. Importante: La API para DateFormat.parse() explica que, por defecto, el método parse() es indulgente cuando parsea fechas. Nuestra experiencia es que no es tan indulgente. Hay que tener cuidado al usar el método. La Clase Locale Antes dijimos que una gran parte de este objetivo es comprobar nuestra habilidad para hacer algunas tareas de i18n. La clase Locale es nuestra llave para esto. Las clases DateFormat y NumberFortmat pueden usar una instancia de Locale para personalizar la salida formateada a un locale específico. La API define un locale como "una región geográfica, política o cultural específica". Los dos constructores que necesitamos para el examen son: Usando estos dos locales en una fecha podría dar una salida como la siguiente: sabato 1 ottobre 2005 sabato, 1. ottobre 2005 Ahora juntemos todo en un código que crea un objeto Calendar, especifica una fecha y luego la convierte a Date. Después tomaremos el objeto Date y lo imprimirá usando locales de todo el mundo: Calendar c = Calendar.getInstance(); c.set(2010, 11, 14);// December 14, 2010 (month is 0-based) Date d2 = c.getTime(); Locale locIT Italy Locale locPT Portugal Locale locBR Brazil Locale locIN India Locale locJA Japan = new Locale("it", "IT"); // = new Locale("pt"); // = new Locale("pt", "BR"); // = new Locale("hi", "IN"); // = new Locale("ja"); // DateFormat dfUS = DateFormat.getInstance(); System.out.println("US " + dfUS.format(d2)); DateFormat dfUSfull = DateFormat.getDateInstance(DateFormat.FULL ); System.out.println("US full " + dfUSfull.format(d2)); DateFormat dfIT = DateFormat.getDateInstance(DateFormat.FULL , locIT); System.out.println("Italy " + dfIT.format(d2)); DateFormat dfPT = DateFormat.getDateInstance(DateFormat.FULL , locPT); System.out.println("Portugal " + dfPT.format(d2)); DateFormat dfBR = DateFormat.getDateInstance(DateFormat.FULL , locBR); System.out.println("Brazil " + dfBR.format(d2)); DateFormat dfIN = DateFormat.getDateInstance(DateFormat.FULL , locIN); System.out.println("India " + dfIN.format(d2)); DateFormat dfJA = DateFormat.getDateInstance(DateFormat.FULL , locJA); System.out.println("Japan " + dfJA.format(d2)); Esto produce la siguiente salida US US full Italy Portugal Brazil. India Japan 12/14/10 3:32 PM Sunday, December 14, 2010 domenica 14 dicembre 2010 Domingo, 14 de Dezembro de 2010 Domingo, 14 de Dezembro de 2010 ??????, ?? ??????, ???? 2010?12?14? Vaya, nuestra máquina no está configurada para soportar locales de India o Japón, pero podemos ver cómo un único objeto Date puede formatearse para trabajar con muchos locales. Exam Watch 6.10 Hay otro par de métodos en Locale (getDisplayCountry() y getDisplayLanguage()) que necesitamos conocer para el examen. Estos métodos nos permiten crear Strings que representa un locale de país y un lenguaje determinado.Nos permiten presentar su nombre por defecto y su nombre asociado con otro locale: Calendar c = Calendar.getInstance(); c.set(2010, 11, 14); Date d2 = c.getTime(); Locale locBR = new Locale("pt", "BR"); Brazil Locale locDK = new Locale("da", "DK"); Denmark Locale locIT = new Locale("it", "IT"); Italy // // // System.out.println("def " + locBR.getDisplayCountry()); System.out.println("loc " + locBR.getDisplayCountry(locBR)); System.out.println("def " + locDK.getDisplayLanguage()); System.out.println("loc " + locDK.getDisplayLanguage(locDK)); System.out.println("D>I " + locDK.getDisplayLanguage(locIT)); Esto produce la salida: def loc def loc D>I Brazil Brasil Danish dansk danese Aquí puede observarse que en Brazil el país se llama "Brasil" y que en Dinamarca, su lenguage se llam "dansk". Además, en italia, el lengua de Dinamarca se conoce como "danese". La Clase NumberFormat Cubriremos este objetivo discutiendo sobre la clase NumberFormat. Como la calse DateFormat, esta clase es abstracta, por lo que usaremos una versión de getInstance()o getCurrencyInstance() para creare un objeto NumberFormat. Usaremos esta clase para representar formatos de números o valores de moneda: float f1 = 123.4567f; Locale locFR = new Locale("fr"); // France NumberFormat[] nfa = new NumberFormat[4]; nfa[0] = NumberFormat.getInstance(); nfa[1] = NumberFormat.getInstance(locFR); nfa[2] = NumberFormat.getCurrencyInstance(); nfa[3] = NumberFormat.getCurrencylnstance(locFR); for(NumberFormat nf : nfa) System.out.println(nf.format(f1)); Tal y como hemos visto, varias de las clases cubiertas en este objetivo sob abstractas. Además, para todas las clases, la funcionalidad clave de cada instancia se establece en el tiempo de creación. La siguiente tabla resume los constructores o métodos usados para crear instancias de todas las clases que hemos discutido en esta sección: Class Key Instance Creation Options Esto produce (en su máquina virtual): 123.457 123,457 $123.46 123,46 ? No debemos preocuparnos si no se muestran los símbolos para francos, pounds, rupias, yenes, etc. No debemos conocer todos los símbolos usados para las monedas, si necesitamos alguno se nos especificará en la pregunta. Podríamos en el examen encontrar otros métodos a parte de format. A continuación se muestra un código que utiliza getMaximumFractionDigits(), setMaximumFractionDigits(),parse(), y setParseIntegerOnly(): float f1 = 123.45678f; NumberFormat nf = NumberFormat.getInstance(); System.out.print(nf.getMaximumFractionDigi ts() + " "); System.out.print(nf.format(fl) + " "); nf.setMaximumFractionDigits(5); System.out.println(nf.format(fl) + " ") ; try { System.out.println(nf.parse("1234.567")); nf.setParselntegerOnly(true); System.out.println(nf.parse("1234.567")); } catch (ParseException pe) { System.out.println("parse exc"); } new Date(); new Date(long millisecondsSince010170); util.Calenda Calendar.getInstance(); r Calendar.getInstance(Locale); Locale.getDefault(); new Locale(String language); util.Locale new Locale(String language,String country); DateFormat.getInstance(); DateFormat.getDateInstance(); text.DateFor DateFormat.getDateInstance(st mat yle); DateFormat.getDateInstance(st yle, Locale); NumberFormat.getInstance() NumberFormat.getInstance(Loca le) NumberFormat.getNumberInstanc e() text.NumberF NumberFormat.getNumberlnstanc ormat e(Locale) NumberFormat.getCurrencyInsta nce() NumberFormat.getCurrencyInsta nce(Locale) util.Date Esto produce la siguiente salida: 3 123.457 1234.567 1234 123.45678 Observa que en este caso, el número inicial de dígitos fraccionales por defecto en NumberFormat es tres y que el método format() redondea el valor de f1 y no lo trunca. Después de cambiar los dígitos fraccionales de nf, se muestra el valor entero de f1. Luego, observa que el método parse() debe ejecutarse en un bloque try/catch y que el método setParseInteger() toma un booleano y en este caso, causa llamadas subsecuentes a parse() para devolver sólo la parte entera de los Strings formateados como números punto-flotante. 5. Objetivo de Certificación 3.5 Parseo, Tokens y Formateo Escribir código que use las APIs estándar J2SE en los paquetes java.util y java.util.regex para formatear o parsear cadenas y streams (flujos). Para cadenas, escribir código que use las clases Pattern y Matcher y el método String.split. Reconocer y utilizar patrones de expresiones regulares (limitados a .(punto), *(asterisco), +(signo más), ?, \d, \s, \w, [],()). El uso de *, + y ? será limitado a greedy quantifiers y el operador paréntesis sólo se usará como mecanismo de agrupación, no para capturar contenido. Para flujos, escribir código que use las clases Formatter y Scanner y los métodos format y printf de PrintWriter. Reconocer y usar parámetros de formateo (limitado a %b, %c, %d, %f, %s) en Strings de formato. En esta sección cubriremos tres ideas básicas: Encontrar cosas: Tendremos grandes cantidades de texto que tendremos que examinar. Quizás estamos haciendo algo de Screen Scraping o leyendo de un fichero. En cualquier caso, necesitaremos formas fáciles de encontrar agujas de texto en montones de paja de texto. Usaremos las clases java.regex.Pattern, java.regex.Matcher y java.util.Scanner para ayudarnos. Tokenizar cosas: Tenemos un fichero delimitado del que queremos obtener datos útiles. Queremos transformar una parte de un fichero de texto con el siguiente aspecto: "1500.00,343.77,123.4" en varias variables float. Mostraremos las bases del uso del método String.split() y la clase java.util.Scanner para tokenizar nuestros datos. Formatear cosas: Tenemos que crear un informe y necesitamos tomar una variable float con un valor de 32500.000f y transformarla en un String con un valor de "$32,500.00". Mostraremos la clase java.util.Formatter y los métodos printf() y format(). 5.1. Tutorial de Búsqueda Siempre que estemos buscando o tokenizando algo, muchos conceptos serán los mismos por lo que empezaremos con las cosas básicas. No importa el lenguaje que estemos usando. Las expresiones regulares (regex) son un tipo de lenguaje en un lenguaje, diseñado para ayudar a los programadores en estas tareas de búsqueda. Todo lenguaje que proporciona soporte para regex usa uno o más motores de regex. Estos motores buscan a través de datos de texto usando instrucciones que son codificadas en expresiones. Una regex es como un pequeño programa o script. Cuando invocamos un motor de regex le pasaremos el trozo de texto que queremos procesar (en Java normalmente será un String o un Stream) y la expresión que queremos usar para buscar a través de los datos. A lo largo de la sección trataremos las expresiones regulares como un lenguaje del que sólo usaremos una parte. Búsquedas Simples Para nuestro primer ejemplo, queremos buscar a través del siguiente String abaaaba todas las ocurrencias de la expresión: ab En todos los ejemplos asumiremos que nuestras fuentes de datos usan índices basados en 0. Por ejemplo: source: abaaaba index: 0123456 Podemos ver que tenemos dos ocurrencias de la expresión 'ab': una que empieza en la posición 0 y otra que empieza en la sección 4. Si enviamos la cadena de texto y la expresión a un motor de regex, podría respondernos las posiciones de dichas ocurrencias: import java.util.regex.*; class RegexSmall { public static void main(String[] args) { Pattern p = Pattern.compile("ab"); // the expression Matcher m = p.matcher("abaaaba"); // the source boolean b = false; while(b = m.find()) { System.out.print(m.start() + " "); } } } Esto produce: 0 4 No vamos a explicar este código de momento. Por ahora vamos a ver más sintaxis de regex. Una vez que hemos comprendido mejor las regex, los ejemplos se vovlerán más complejos. A continuación otro ejemplo más complicado: source: abababa index: 0123456 expression: aba ¿Cuantas ocurrencias tendremos en este caos?. Bien, hay cierta ocurrencia empezando en la posición 0, y otra en la posición 4. Pero, ¿y en la posición 2?. De forma general, la ocurrencia de la posición 2 no será considerada una ocurrencia válida. La primera regla general de búsqueda en regex es: En general, una búsqueda regex se ejecuta de izquierda a derecha y una vez que un carácter ha sido usado en una ocurrencia, no puede ser reutilizado. Por lo que en nuestro ejemplo, la primera ocurrencia usa las posiciones 0, 1 y 2 (otra forma de decirlo es que los 3 primeros caracteres fueron consumidos). Por tanto, el carácter de la posición 2 no puede volverse a usar. Esta es la forma normal de funcionamiento, sin embargo veremos una excepción a la regla anterior. Hemos encajado un par de cadenas exactas, pero ¿que pasa si queremos hacerlo más dinámico?. Por ejemplo, ¿y si queremos encontrar todas las ocurrencias de números hexadecimales o números de teléfono o códigos postales?. Búsquedas Usando Metacaracteres Las regex son un potente mecanismo apra tratar con los casos que hemos descrito arriba. El corazón de este mecanismo es la idea de un metacaracter. Como ejemplo simple, digamos que queremos buscar a través de código fuentes buscando todas las ocurrencias de dígitos numéricos. En regex, la siguiente expresión se usa para buscar dígitos numéricos: \d \s Un espacio en blanco Un carácter de palabra (letras, dígitos o "_" \w (guión subrayado)) Por ejemplo, dado lo siguiente: source: "a 1 56 _Z" index: 012345678 pattern: \w El programa devolverá las posiciones 0, 2, 4, 5, 7 y 8. Los únicos caracteres en esta cadena que no encajan con un carácter de palabra son los espaciones en blanco. (Nota: En este ejemplo hemos encerrado los datos fuentes entre comillas para indicar que no hay espacios en blanco al principio ni al final). También podemos especificar conjuntos de caracteres para buscar mediante los corchetes y guiones: [abc] Busca sólo las letras a, b o c [a–f] Busca sólo las letras a, b, c, d, e o f Además, podemos buscar a través de varios rangos de una vez. La siguiente expresión está buscando ocurrencias para las letras a-f o A-F, no busca la combinación fA: [a-fA-F] Busca las primeras seis letras del alfabeto Por ejemplo, source: "cafeBABE" index: 01234567 pattern: [a-cA-C] Devuelve las posiciones 0, 1,4, 5, 6. Búsquedas Usando Cuantificadores Digamos que queremos crear un patrón regex para buscar literales hexadecimales. Como un primer paso podemos resolver el problema para un dígito hexadecimal: 0 [xX] [0-9a-fA-F] Si cambiamos al programa anterior para aplicar la expresión \d a la siguiente cadena: source: a12c3e456f index: 0123456789 Nos dirá que se han encontrado en las posiciones 1, 2, 4, 6, 7, y 8. (Si queremos probarlo, debemos "escapar" el argumento "\d" poniendo otra barra inversa delante "\\d"). Las regex proporcionan un conjunto rico de metacaracteres que podemos encontrar descritos en la documentación API de java.util.regex.Pattern. Sólo discutiremos las del examen: \d Un dígito La expresión anterior podría leerse como: "Encuentra un conjunto de caracteres en el cual el primer carácter es un "0", el segundo es "x" o "X" y el trecer carácter es un dígito entre 0 y 9, una letra entre a y f o una mayúscula entre A y F. Usando la expresión anterior y los siguientes datos, source: index: "12 0x 0x12 0Xf 0xg" 012345678901234567 devolverá 6 y 11 (Nota: 0x y 0xg no son números válidos en hexadecimal) Como segundo paso, pensemos en un problema más fácil. ¿Y si queremos una regex que encuentre ocurrencias de enteros?. Los integers pueden ser uno o más dígitos, por lo que sería genial poder decir "uno o más" en una epxresión. Hay un conjunto de constructores reges llamados cuandotificadores que nos permiten especificar cosas como "uno o más". La otra duda que puede surgir es que cuando estamos buscando algo cuyo tamaño es variable, ontemer sólo la posición inicial como valor de retornoe s algo limitado. Por lo que, además de devolver las posiciones en las que empieza, otra información que puede devolver el motor de regex es la ocurrencia entera o grupo que encuentra. Vamos a cambiar la forma en la que representamos los valores devueltos por una regex especificando cada valor de retorno en una lína, recordando que ahora para cada valor devuelto obtendremos la posición inicial y el grupo: source: "1 a12 234b" pattern: \d+ Podemos leer esta expresión: "Encuentra uno o más dígitos en una fila". Esta expresión produce la salida: 0 1 3 12 6 234 Volviendo a nuestro problema hexadecimal, lo último que tenemos que saber es cómo especificar el uso de un cuantificador sólo para una parte de una expresión. La siguiente expresión añade paréntesis para limitar el cuantificador "+" para únicamente dígitos hexadecimales: 0[xX] ([0–9a-fA-F])+ Los paréntesis y "+" indican que, una vez encontrado "Ox" o "OX", podemos encontrar una o más ocurrencias de dígitos hexadecimales. Los cuantificadores siempre cuantifican la parte de la expresión que los precede. Los otros dos cuantificadores que vamos a ver son: * Cero o más ocurrencias ? Cero o una ocurrencia Digamos que tenemos un fichero de texto que contiene una lista delimitada por comas de todos los nombres de fichero en un directorio que contienen objetos muy importantes. Queremos crear una lsita de todos aquellos nombres que empiezan con proj1. Primero vemos como sería una parte del texto fuente: ..."proj3.txt,proj1sched.pdf,proj1,proj2,p roj1.java"... Para resolver este problema vamos a usar el operador ^. Este operador no entra en el examen, pero nos ayudará a crear una solución limpia a nuestro problema. El símbolo ^ es de negación. Por ejemplo, si queremos encontrar algo sin a, b o c, podríamos decir: [^abc] Ahora, con los operadores ^ y * podemos crear lo siguiente: proj1([^,])* Si aplicamos esto a la porción de texto vista antes, la regex devolvería: 10 proj1sched.pdf 25 proj1 37 proj1.java El punto clave en esto es "dame cero o más caracteres que no son coma". Ahora digamos que nuestro trabajo ahora es buscar un fichero de texto y encontrar algo que podría ser local, un número de teléfonod e 7 dígitos. Vamos a decir, de forma arbitraria, que si encontramos 7 dígitos en una línea, o tres dígitos seguidiso por un guión o un espacio en blando seguido de 4 dígitos, ya tenemos un candidato. A continuación vemos ejemplos de nímeros de teléfono "válidos": 1234567 123 4567 123–4567 La clave es crear una expresión que acepte "cero o una instancia" de un espacio o un guión en medio de nuestros dígitos: \d\d\d([-\s])?\d\d\d\d El Punto Predefinido Además de los metaracteres que hemos discutido (\s, \d y \w) también tenemos que comprender el metacarácter "." (punto). Cuando utilizamos este carácter en una expresión regular, significa que "vale cualquier carácter·. Por ejemplo, dado lo siguiente: source: "ac abc a c" pattern: a.c producirá la siguiente salida: 3 abc 7 a c usando el cuantificador greedy * produce: El "." es capaz de encajar con "b" y " " en los datos fuentes. Greedy Quantifiers Cuando usamos los cuantificadores *, + y ? podemos personalizarlos un poco para producir comportamiento conocido como "greedy", "reluctant" o "possessive". Aunque necesitemos comprender sólo el cuantificador "greedy" para el examen, también vamos a discutir el cuantificador "reluctant". Primero la sintaxis: ? es greedy, ?? es reluctant, para 0 o 1 * es greedy, *? es reluctant, para 0 o más + es greedy, +? es reluctant, para 1 o más ¿Qué ocurre cuando tenemos lo siguiente? source: yyxxxyxx pattern: .*xx En primer lugar, estamos haciendo algo un poco diferente buscando caracteres que se anteponen a la porción estática (xx) de la expresión. Estamos pensando en lago como: "Encuentra conjuntos de caracteres que acaban con xx". Antes de ver cual sería la salida, podemos plantear que hay dos hipotéticos resultados. Recuerda que antes hemos dicho que, de forma general, los motores de regex funcionan de izquierda a derecha y consumen caracteres tal y como los encuentran. Por tanto, trabajando de izquierda a derecha, podríamos predecir que el motor podría buscar los 4 primeros caracteres (0-3), encontrar xx en la posición 2 y tener su primera ocurrencia. Luego podría proseguir y encontrar el segundo "xx" en la posición 6. Esto se resume con el resultado: 0 yyxx 4 xyxx El otro resultado posible podría ser: 0 yyxxxyxx La forma de pensar sobre esto es considerar el nombre greedy (codicioso). La segunda respuesta sería la correcta, ya que el motor regex examinaría todos los datos fuentes antes de quedarse con la primera ocurrencia que encaja (hemos usado el operador greedy). El otro resultado puede obtenerse mediante el cuantificador reluctant. Veámoslo: source: yyxxxyxx pattern: .*xx 0 yyxxxyxx Si cambiamos el patrón a: source: yyxxxyxx pattern: .*?xx estamos usando el cualificador reluctant y obtendremos lo siguiente: 0 yyxx 4 xyxx Por tanto, el cuantificador greedy lee los datos fuentes enteros y luego vuelve (desde la derecha) hasta que encuentra la ocurrencia más a la derecha. Cuando Colisionan los Metacaracteres y los String Estamos hablando de regex desde una perspectiva teórica. Cuando es hora de implementar regex en nuestro código, suele ser común que nuestros datos fuentes y/o expresiones sean almacenadas e String. El problema es que los metacaracteres y String no se mezclan del todo bien. Por ejemplo, supongamos que queremos un patrón regex simple que busque dígitos. Podríamos intentar algo parecido a lo siguiente: String pattern = "\d"; error! // compiler Esta línea de código no compila. El compilador ve \ y piensa: "Ok, ahora viene una secuencia de escape, quizñas una nueva línea". Pero no, a continuación viene la "d" y el compilador dice "nunca he oido hablar de la secuencia de escape \d". La forma de satisfacer al compilador es añadir otra barra invertida: String pattern = "\\d"; metacharacter // a compilable La primera barra le dice al compilador que lo que venga a continuación debe tomarse de forma literal, no como una secuencia de escape. ¿Y qué pasa con el metacaracter punto (.)?. Si queremos que un punto en nuestra expresión sea usado como un metacaracter no hay problema, pero ¿y si queremos que se tome de forma literal?. A continuación se muestran las opciones: String p = "."; // regex sees this as the "." metacharacter String p = "\."; // the compiler sees this as an **illegal** Java escape sequence String p = "\\."; // the compiler is happy, and regex sees a dot, not a metacharacter Un problema similar puede ocurrir cuando manejamos metacaracteres en un programa Java pasados mediante argumentos de línea de comandos. Si queremos pasar el metacaracter \d en nuestro programa Java, nuestra JVM hace lo correcto si escribimos: % java DoRegex "\d" Pero podría no funcionar. Si tenemos problemas ejecutando los siguientes ejemplos, podríamos intentar añadir otra barra a nuestros metacaracteres de línea de comandos. No debemos preocuparnos porque no veremos metacaracteres de línea de comandos en el examen. El lenguaje Java define varias secuencias de escape, incluyendo: \n = salto de línea \b = backspace \t = tab Y otros, pero no tendremos que aprenderlos para el examen. En este punto, hemos aprendido suficiente sobre el lenguaje regex para empezar a usarlo en nuestros programas. 5.2. Localizando Datos a través de Patrones Una vez que conocemos un poco de regex, usar las clases java.util.regex.Pattern (Pattern) y java.util.regex.Matcher (Matcher) es fácil. La clase Pattern se usa para almacenar una representación de una expresión regular, por lo que usarse y reutilizarse por instancias de la clase Matcher. La clase Matcher se usa para invocar al motoro de reges con la intención de realizar operaciones de ocurrencias. El siguiente programa muestra Pattern y Matcher en acción: import java.util.regex.*; class Regex { public static void main(String [] args) { Pattern p = Pattern.compile(args[0]); Matcher m = p.matcher(args[1] ); boolean b = false; System.out.println("Pattern is " + m.pattern()); while(b = m.find()) { System.out.println(m.start() + " " + m.group()); } } } El programa usa el primer comando para representar la regex que queremos usar y utiliza el segundo argumento para representar los datos fuentes que queremos buscar. Hagamos una prueba: % java Regex "\d\w" "ab4 56_7ab" Produce la siguiente salida: Pattern is \d\w 4 56 7 7a Como normalmente tendremos caracteres especiales o espacios en blanco como parte de nuestros argumentos, probablemente querremos hacernos al hábito de encerrar los argumentos entre comillas. Veamos el código en más detalle. Primero, observa que no estamos usando 'new' para crear un Pattern. Si comprobamos la API veremos que no se muestran constructores. Usaremos el método sobrecargado static compile() (que toma la expresión en cadena) para crear una instancia de Pattern. Para el examen, todo lo que necesitamos saber es crear un Matcher, es usar el método Pattenr.matcher() que tomca un String con los fuentes. El método importante en el programa es find(). Este es el método que inicia el motor regex y realiza la búsqueda. El método devuelve true si encuentra una ocurrencia y recuerda la posición inicial de dicha ocurrencia. Si el método ha devuelto true, podemos llamar al método start() para obtener la posición inicial de la ocurrencia y al método group() para obtener la cadena que representa el trozo de datos que han encajado. Nota: Un uso común de las regex es para realizar operaciones de búscar y reemplazar. Aunque las operaciones de reemplazar no son parte del examen deberíamos saber que la clase Matcher proporciona varios métodos que realizan dichas operaciones. Para más detalles ver los métodos appendReplacement(), appendTail() y replaceAll(). La clase Matcher nos permite mirar subconjuntos de datos fuentes usando un concepto llamado regiones. En la vida real, las regiones pueden mejorar el rendimiento, pero no necesitamos conocer nada sobre eso para el examen. leer un fichero delimitado para obtener sus contenidos y almacenarlos en arrays o colecciones. Veremos dos clases de la API que proporcionan estas capacidades: String (a través de su método split()) y Scanner, que contiene muchos métodos. Búsquedas usando la clase Scanner Aunque la clase java.util.Scanner está pensada principalmente para tokenizar datos (que será lo próximo que se verá) también puede ser usada para encontrar cosas, como las clases Pattern y Matcher. Aunque Scanner no proporciona información de localización o funcionalidad de buscar y reemplazar, podemos usarlo para aplicar regex a datos fuentes para saber cuantas instancias existen de una expresión. El siguiente ejemplo usa el primer argumento de línea de comandos como regex, luego solicita una entrada a través del System.in. Imprime un mensaje cada vez que se encuentra una ocurrencia: Tokens y Delimitadores Cuando hablamos de tokenizar, estamos hablando de datos compuestos por tokens y delimitadores. Los tokens son las piezas actuales de datos y los delimitadores son las expresiones que separan los tokens unos de otros. Los delimitadores normalmente son de un sólo carácter (coma, espacio en blanco, etc.) pero pueden ser cualquier cosa que distinga una regex. Veamos cómo tokenizaríamos una pequeña parte de datos fuentes usando un par de delimitadores diferentes: import java.util.*; class ScanIn public static void main(String[] args) { System.out.print("input: "); System.out.flush(); try { Scanner s = new Scanner(System.in); String token; do { token = s.findlnLine(args[0]); System.out.println("found " + token); } while (token != null); } catch (Exception e) { System.out.println("scan exc"); } } } ab cd5b 6x z4 La siguiente invocación y entrada: java ScanIn "\d\d" input: 1b2c335f456 produce la siguiente salida: found 33 found 45 found null 5.3. Tokenizando Tokenizar es el proceso de tomar grandes porciones de datos, dividirlos en pequeñas partes y almacenar dichas partes en variables. El ejemplo más común es source: "ab,cd5b,6x,z4" Si el delimitador fuera la coma, tendríamos cuatro tokens: Si ahora el delimitador es un \d, tendríamos tres tokens: ab,cd b, X,Z De forma general, cuando tokenizamos datos, los delimitadores suelen descartarse. En el segundo examen, hemos definido como delimitadores los dígitos, por lo que los números 5, 6 y 4 no aparecen entre los tokens. Tokenizando con String.split() El método split() toma una regex como argumento y devuelve un array de String con los tokens producidos por el proceso de tokenización. Esta es una simple de tokenizar pequeñas porciones de datos. El siguiente programa usa args[0] para almacenar una cadena fuente y args[1] para almacenar el patrón regex a usar como delimitador: import java.util.*; class SplitTest { public static void main(String[] args) { String[] tokens = args[0] .split(args[1] ) ; System.out.println("count " + tokens.length); for(String s : tokens) System.out.println(">" + s + "<"); } } Todo se realiza de una vez cuando se invoca el método split(). La cadena fuente se divide en piezas y las piezas son cargadas en un array de String. El resto del código verifica lo que se ha generado. La siguiente invocación: % java SplitTest "ab5 ccc 45 @" "\d" produce count 4 >ab< > ccc < >< > @//<// Nota: Recuerda que para representar "\" en una cadena necesitamos la secuencia de escape "\\". Se han puesto los tokens entre "> <" para mostrar los espacios en blanco. Observa que como cada dígito se usa como delimitador, dos dígitos seguidos producen un token vacío. Un inconveniente del método split() es que algunas veces podríamos estar buscando algo y querer parar la búsqueda una vez encontrado lo que buscamos. La clase Scanner proporciona una API completa para hacer operaciones de tokenización. int i; String s, hits = " "; Scanner s1 = new Scanner(args[0]); Scanner s2 = new Scanner(args[0]); while(b = sl.hasNext()) { s = s1.next(); hits += "s"; } while(b = s2.hasNext()) { if (s2.hasNextInt()) { i = s2.nextlnt(); hits += "i"; } else if (s2.hasNextBoolean()) { b2 = s2.nextBoolean(); hits += "b"; } else { s2.next(); hits += "s2"; } } System.out.printl.n("hits " + hits); } } Si el programa se invica con: java ScanNext "1 true 34 hi" produce: hits ssssibis2 La clase Scanner tiene métodos nextXxx() y hasNextXxx() para cada tipo primitivo excepto para char. Además tiene el método useDelimiter() que nos permite cambiar el delimitador. Exam Watch 6.11 Tokenizando con Scanner La clase java.util.Scanner proporciona las siguientes características: Pueden construirse mediante ficheros, streams o Strings La tokenización se hace en un bucle que puede interrumpirse en cualquier punto Los tokens pueden convertirse de forma automática a su tipo primitivo Veamos un programa que demuestra el uso de varios métodos y capacidades de Scanner. El delimitador por defecto de Scanner es el espacio en blanco. El ejemplo crea dos objetos Scanner: s1 es iterado a través del método next(), que devuelve cada token como un String, y s2 que es analizado con varios métodos especializados nextXxx() (donde Xxx es un tipo primitivo): import java.util.Scanner; class ScanNext { public static void main(String [] args) { boolean b2, b; 5.4. Formateo con printf() y format() En Java 5 fueron añadidos los métodos printf() y format() a la clase java.io.PrintStream. Ambos métodos se comportan de la misma forma, por lo que lo que se diga sobre un método puede aplicarse al otro. Por detrás, el método format() usa la clase java.util.Formatter para hacer el trabajo duro. Podemos usar directamente la clase Formatter, pero para el examen sólo necesitamos conocer la sintaxis básica de los argumentos que pasamos al método format(). La documentación de esto puede encontrarse en la API de Formatter. Veamos lo que necesitamos saber sobre la sintaxis de las cadenas de formateo. Empezcemos por lo que se aparece en la documentación API sobre las cadenas de formateo: printf("format string", argument(s)); La cadena "format string" puede contener una cadena de información literal que no está asociada a ningún argumento y datos de formateo específicos de argumentos. La forma de determinar si estamos tratanto con datos de formateo o no es que éstos siempre empiezan con un signo de porcentaje (%). Veamos un ejemplo: System.out.printf("%2$d + %1$d", 123, 456); Produce: 456 + 123 Veamos que ha ocurrido. Dentro de las dobles comillas hay: Una cadena de formato Un signo más (+) Otra cadena de formato Observa que hemos mezclado literales en las cadenas de formato. Ahora profundizaremos un poco en la construcción de estas cadenas: %[arg_index$] [flags] [width] [.precision]conversion char Los valores entre corchetes son opcinales. Lo único obligatorio en una cadena de formato es el signo % y un carácter de conversión. En el ejemplo anterior los únicos valores opcionales que hemos usado son los de índice. 2$ representa el segundo argumento y 1$ el primer argumento. (Observa que no hay problema en intercambiar el orden). El carácter 'd' es el carácter de conversión. Veamos la descripción de cada parte: arg_index Un entero seguido de $, indica el argumento que será impreso en esta posción flags Aunque hay muchas disponibles, las que necesitamos saber para el examen son: o "-" Alinea a la izquierda el argumento o "+" Incluye signo (+ o -) con este argumento o "0" Rellena el argumento con ceros o "," Usa separadores de grupos específicos del locale o "(" Encierra los números negativos entre paréntesis width Indica el mínimo número de caracteres a imprimir precision Para el examen sólo lo necesitamos con números de punto flotante y en este caso, la precisión indica el número de dígitos a imprimir después del punto decimal conversion El tipo de argumento que estamos formateando. Necesitamos saber: o b boolean o c char o d integer o f floating point o s string Veamos algunos ejemplos: int i1 = -123; int i2 = 12345; System.out.printf(">%1$(7d< \n", i1); System.out.printf(">%0,7d< \n", i2); System.out.format(">%+-7d< \n", i2); System.out.printf(">%2$b + %1$5d< \n", i1, false); Esto produce: > (123)< >012,345< >+12345 < >false + -123< Por último hay que recordar que si no encaja el tipo especificado con el argumento, obtendremos una excepción en tiempo de ejecución: System.out.format("%d", 12.3); Produce lo siguiente: Exception in thread "main" java.util.IllegalFormatConversionException: d != java.lang.Double