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