Tema 6. Lectura y escritura Autor: José C. Riquelme 1. Lectura: la clase Scanner 1.1 Definición Java proporciona en el paquete java.util una clase que se denomina Scanner que nos permitirá leer datos desde ficheros de texto o incluso desde teclado. Los objetos de tipo Scanner mediante la invocación de distintos métodos permitirán leer datos de cualquier tipo (String, enteros, reales, etc) con diversas posibilidades de separadores. Asimismo permitirá leer un fichero de texto línea a línea, guardando cada línea en un objeto de tipo String. Para construir un objeto de tipo Scanner se invocará al constructor de la clase pasándole como argumento un objeto de tipo File (que se encuentra en el paquete java.io). Los objetos de tipo File relacionan un fichero con su nombre y path en el sistema de archivos del ordenador. Un objeto de tipo File se crea mediante un constructor al que se le pasa como argumento una cadena de caracteres con el nombre y el path del fichero que se quiere leer o escribir. Por ejemplo: File f = new File("palabras.txt"); Indica que el fichero palabras.txt se encuentra en el directorio raíz de la carpeta que contenga nuestro proyecto Java. Si quisiéramos que el fichero estuviera en una carpeta concreta podríamos poner un constructor como: File f = new File("c:\Usuarios\pedro\clases\poo\tema6\palabras.txt"); Y en la carpeta del paquete test del proyecto podríamos poner: File f = new File(".\src\test\palabras.txt"); Una vez creado el fichero f se puede invocar al constructor de la clase Scanner: Scanner sc = new Scanner(f); Sin embargo lo usual es hacerlo todo en la misma sentencia ya que el objeto de tipo File normalmente no se va a usar: Scanner sc = new Scanner(new File("palabras.txt")); 1.2 Métodos de la clase Scanner La clase Scanner proporciona un conjunto de métodos para leer el contenido del fichero de texto que se ha conectado mediante la invocación del constructor. En la siguiente tabla se exponen algunos de los más interesantes, como siempre se puede ver la relación completa en la documentación de Java: http://docs.oracle.com/javase/7/docs/api/java/util/Scanner.html 2 Introducción a la Programación void close() Closes this scanner. boolean hasNext() Returns true if this scanner has another token in its input. boolean hasNextDouble() Returns true if the next token in this scanner's input can be interpreted as a double value using the nextDouble() method. boolean hasNextInt() Returns true if the next token in this scanner's input can be interpreted as an int value in the default radix using the nextInt() method. boolean hasNextLine() Returns true if there is another line in the input of this scanner. boolean hasNextLong() Returns true if the next token in this scanner's input can be interpreted as a long value in the default radix using the nextLong() method. String next() Finds and returns the next complete token from this scanner. double nextDouble() Scans the next token of the input as a double. int nextInt() Scans the next token of the input as an int. String nextLine() Advances this scanner past the current line and returns the input that was skipped. long nextLong() Scans the next token of the input as a long. Scanner useDelimiter(String pattern) Sets this scanner's delimiting pattern to a pattern constructed from the specified String. Antes de explicar el uso de estos métodos hay que reseñar un par de cuestiones: Java permite crear objetos de tipo Scanner invocando distintos constructores, no solo con argumentos de tipo File, sino también InputStream, Readable o incluso un String. Estos constructores se mantienen por compatibilidad entre versiones de Java y para permitir lecturas a nivel de byte. El método useDelimiter sobre un objeto Scanner lleva como argumento un objeto de tipo Pattern: http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html que es una representación como cadena de una expresión regular: http://es.wikipedia.org/wiki/Expresi%C3%B3n_regular En esta asignatura sólo lo vamos a usar para indicar separadores entre los datos a leer distintos del espacio en blanco que es el separador por defecto de la clase Scanner. Hay que tener en cuenta que el constructor de Scanner lanza la excepción de tipo FileNotFoundException, por tanto el método que contenga la invocación debe lanzar (throws) una excepción. 6. Lectura y escritura 1.3 Uso de la clase Scanner Como hemos podido observar en la tabla anterior la clase Scanner nos permite leer un fichero de texto de diversas maneras: línea a línea, String a String (con algún carácter separador) o incluso leer otros tipos de datos como enteros, reales, etc. Ejemplo 1 Supongamos que queremos leer un fichero de texto línea a línea guardando cada línea en un String: Scanner sc=new Scanner(new File("palabras.txt")); while (sc.hasNextLine()) { String s=sc.nextLine(); //tratamiento de s, por ejemplo: mostrar(s); } sc.close(); Como podemos ver los métodos usados son hasNextLine que nos devuelve cierto mientras haya más líneas por leer y falso cuando el fichero se acaba y nextLine que devuelve la siguiente línea del fichero en un String. De esta forma si el fichero palabras.txt tuviera este contenido: cinco cinco cinco seis seis siete siete ocho La salida del programa anterior sería una copia del fichero línea a línea: cinco cinco cinco seis seis siete siete ocho Ejemplo 2 Supongamos que queremos leer un fichero de texto palabra a palabra separadas por un blanco guardando cada palabra en un String: Scanner sc=new Scanner(new File("palabras.txt")); while (sc.hasNext()) { String s=sc.next(); //tratamiento de s, por ejemplo: mostrar(s); } sc.close(); 3 4 Introducción a la Programación Como podemos ver ahora los métodos usados son hasNext que nos devuelve cierto mientras haya más palabras por leer y falso cuando el fichero se acaba y next que devuelve la siguiente palabra del fichero en un String. De esta forma si el fichero palabras.txt tuviera este contenido: cinco cinco cinco seis seis siete siete ocho La salida del programa anterior sería: cinco cinco cinco seis seis siete siete ocho Ejemplo 3 Supongamos que queremos leer un fichero de texto palabra a palabra separadas por una coma guardando cada palabra en un String: Scanner sc=new Scanner(new File("palabrascomas.txt")).useDelimiter(","); while (sc.hasNext()) { String s=sc.next(); //tratamiento de s, por ejemplo: mostrar(s); } sc.close(); Como podemos ver la diferencia con el anterior ejemplo es el uso del método useDelimiter que permite leer separando por otros caracteres distintos de blanco. Si el fichero palabrascomas.txt tuviera este contenido: cinco,cinco,cinco,seis,seis,siete,siete,ocho La salida del programa anterior sería: cinco cinco cinco seis 6. Lectura y escritura seis siete siete ocho Ejemplo 4 Si nuestro fichero contuviera datos de tipo numérico también se podrían leer con la clase Scanner. Por ejemplo si quisiéramos construir una lista de tipo Integer con el contenido del siguiente fichero enteros.txt: 23 24 35 45 36 34 37 Escribiríamos el siguiente código: List<Integer> ls = new LinkedList<Integer>(); Scanner sc = new Scanner(new File("numeros.txt")); while (sc.hasNextInt()) { ls.add(sc.nextInt()); } sc.close(); mostrar("los números leídos: ",ls); y la salida sería los números leídos: [23, 24, 35, 45, 36, 34, 37] Si en vez de leer números enteros quisiéramos leer números reales sustituiríamos los métodos hasNextInt y nextInt por hasNextDouble y nextDouble. Ejemplo 5 Finalmente la clase Scanner también nos permite leer datos desde teclado. Para ello en el constructor pasamos como argumento el objeto System.in, atributo de la clase System para señalar la entrada estándar o teclado. Así por ejemplo si quisiéramos leer números enteros desde teclado y añadirlos a una lista hasta leer uno negativo, esto es si introducimos por teclado la secuencia: 23 45 65 23 45 67 -6 78 76 45 La salida sería: los números leídos: [23, 45, 65, 23, 45, 67] El código para esto es: 5 6 Introducción a la Programación List<Integer>ls = new LinkedList<Integer>(); Scanner sc = new Scanner(System.in); boolean fin=false; while (sc.hasNext() && !fin) { Integer i= sc.nextInt(); if (i>=0) ls.add(i); else fin=true; } sc.close(); mostrar("los números leídos: ",ls); Si quisiéramos leer una secuencia completa, para terminar introduciríamos en la consola de entrada Ctrl-Z que es la secuencia que indica fin de lectura. 1.4 Encapsulación de la lectura Los trozos de código de los ejemplos anteriores no están situados en ningún método en concreto. Como ya sabemos de temas anteriores, la programación orientada a objetos debe encapsular las líneas de código de una determinada acción en un método, bien sea un método de una clase, bien sea un método de utilidad. Por tanto, si estamos codificando un método de una clase que necesita realizar una lectura de datos, en el método correspondiente escribiríamos las sentencias anteriores. También hay que señalar que en los trozos de código anteriores no se ha incluido el tratamiento de la excepción que puede generar la construcción del objeto de tipo Scanner. Por tanto, el método donde se incluya la invocación del constructor de Scanner debe lanzar (throws) o capturar (try/catch) la excepción FileNotFoundException. Sin embargo, una forma habitual de usar la lectura de datos es encapsularla en un método de utilidad que devuelva un objeto de tipo List con los objetos leídos. Incluso se puede suponer que los objetos siempre serán de tipo String ya que a partir de estos es fácil obtener los tipos numéricos habituales mediante la invocación de los correspondientes constructores. Ejemplo 6 Supongamos que queremos obtener datos de un fichero, sin importarnos el tipo, sólo sabemos que están separados por comas y guardarlos en un List de String. Vamos a construir un método estático en una clase Utiles de la siguiente forma: public static List<String> leeFichero(String fileName, String del) { List<String> listaleida= new LinkedList<String>(); try { Scanner sc = new Scanner(new File(fileName)).useDelimiter("\\s*"+del+"\\s*"); while (sc.hasNext()) { listaleida.add(sc.next()); } sc.close(); } catch (FileNotFoundException e) { 6. Lectura y escritura System.out.println("Fichero no encontrado "+fileName); } return listaleida; } Algunas cuestiones respecto al código anterior: El método leeFichero devuelve una lista de String independientemente de cuales sean los datos contenidos en el fichero. El método recibe un String con el nombre del fichero a leer y una cadena con el carácter separador. La invocación al método useDelimiter se hace concatenando el carácter separador con cualquier combinación de blancos, tabuladores o saltos de línea delante o detrás. Esa combinación es indicada por la expresión regular “\\s*”. La construcción del objeto Scanner está incluida en el cuerpo de una sentencia try/catch para capturar una posible excepción de fichero no encontrado, que indicará que el fichero o no existe o no está donde se espera. La invocación a este método por ejemplo para leer los enteros de este fichero: 23, 24, 35, 45, 36, 34, 37, 56, 45, 67 Sería en una clase TestLectura como sigue: public static void main(String[] args) { List<String> lst = Util.leeFichero("numeros_comas.txt",","); List<Integer> li = new LinkedList<Integer>(); for(String s: lst){ li.add(new Integer(s)); } mostrar("Los números leídos son ",li); } Donde la salida sería Los números leídos son [23, 24, 35, 45, 36, 34, 37, 56, 45, 67] Para leer datos separados por blancos podríamos usar una sobrecarga del método que no tuviera parámetro delimitador. Su código sería: public static List<String> leeFichero(String fileName) { List<String> listaleida= new LinkedList<String>(); try { 7 8 Introducción a la Programación Scanner sc = new Scanner(new File(fileName)).useDelimiter("\\s+"); while (sc.hasNext()) { listaleida.add(sc.next()); } sc.close(); } catch (FileNotFoundException e) { System.out.println("Fichero no encontrado"); } return listaleida; } Dónde la expresión "\\s+" indica cualquier combinación de al menos un blanco, un tabulador o un salto de línea. Ejemplo 7 Supongamos ahora que se necesita leer un fichero de texto con los valores de los atributos de una clase, de forma que la salida sea una colección de objetos creados a partir de esos valores. Así si tenemos el fichero personas.txt con el siguiente contenido: Pedro Gómez,11111111A,25 Luisa Espinel,222222222B,24 Pedro Gómez,33333333C,24 Mariana Guerrero,44444444D,28 Vamos a crear un método de utilidad que devuelva un List<Persona> con los cuatro objetos de tipo Persona, que estarán organizados por filas, de forma que cada fila contiene los valores de los atributos de un objeto separados por comas. El método recibirá un String con el nombre del fichero y lo leerá línea a línea. Cada una de estas líneas contiene los valores de los atributos (nombre, DNI y edad) necesarios para construir un objeto Persona. Para separar estos valores se va a usar un método auxiliar que hemos denominado separaElementos para a partir de otro objeto de tipo Scanner segmentar la línea en una List de String con los atributos. El código es: public static List<Persona> leePersonas(String nomFich) throws FileNotFoundException{ List <Persona> lp = new LinkedList<Persona>(); Scanner sc=new Scanner(new File(nomFich)); while (sc.hasNextLine()) { String linea = sc.nextLine(); List<String> lisat= separaElementos(linea); Persona p=new PersonaImpl(lisat.get(0),lisat.get(1),new Integer(lisat.get(2))); lp.add(p); } return lp; } public static List<String> separaElementos(String s){ List<String> ls = new LinkedList<String>(); Scanner sc1=new Scanner(s).useDelimiter(","); while (sc1.hasNext()){ 6. Lectura y escritura ls.add(sc1.next()); } return ls; } Algunas cuestiones respecto al código anterior: La excepción FileNotFoundException no ha sido tratada mediante un try/catch para no dificultar la lectura del código. Por eso sólo se ha propagado (throws) hacia el método invocante. En un código “bien hecho” debería estar la sentencia try/catch. El método separaElementos no depende de Persona o del tipo que estemos construyendo. Es un método de propósito general que recibe un String s y devuelve un List de String con los “trozos” de s que se encuentran separados por comas. Como se puede observar el tipo Scanner también es capaz de “leer” de una cadena de caracteres, simplemente poniéndola en el lugar del objeto de tipo File en la invocación al constructor. El constructor de PersonaImpl que estamos invocando recibe dos String (nombre y DNI) y un Integer (edad) que es convertido a Integer en la invocación al constructor. También debería incluirse el lanzamiento de una excepción si el número de elementos de la lista lisat de atributos n tiene los tres elementos que debería tener si el fichero está bien construido. Esto es, se debe lanzar una excepción que advierta si el fichero tiene tres valores separados por comas en cada línea. 2. Escritura: la clase printWriter 2.1 Definición Java proporciona dos clases básicas para escribir en un fichero de texto: PrintStream y PrintWriter. Su uso es muy parecido y las diferencias son más internas que externas para un programador no avanzado. La principal diferencia es que PrintWriter (que es posterior a printStream) no puede ser usado para escribir bytes “en crudo” (raw en inglés), y por tanto su uso habitual es para datos que tengan una representación en texto o dicho de otra manera para tipos que tengan el método toString. Nosotros vamos a usar la clase PrintWriter aunque sus métodos para escribir objetos en formato carácter son similares a los de PrintStream. Una ventaja de PrintWriter es que sus métodos no lanzan excepciones, aunque sí alguno de sus constructores. El constructor de la clase PrintWriter recibe un objeto de File y puede lanzar la excepción FileNotFoundException si se produce algún problema para su apertura (normalmente disco lleno o protegido). Por tanto un posible código para la creación de un objeto PrintWriter es: File file = new File(filename); try { PrintWriter ps = new PrintWriter(file); } catch (FileNotFoundException e) { System.out.println("Fichero no encontrado "+filename); } 9 10 Introducción a la Programación 2.2 Métodos de la clase PrintWriter Una selección de los métodos que java proporciona a la clase PrintWriter son los de la siguiente tabla. Como siempre se pueden consultar todos en: http://docs.oracle.com/javase/7/docs/api/java/io/PrintWriter.html void close() cierra el fichero void print(Tipo t) Escribe un valor t de tipo Tipo, donde Tipo puede ser char, boolean, char[ ], double, int, String, long, float u Object void println() Salta de línea void println(Tipo t) Escribe un objeto de tipo boolean, char, char [ ], String, double, float, int, long u Object y después salta de línea 2.3 Uso de la clase PrintWriter Como se puede ver de la tabla anterior escribir en un fichero de texto es igual de sencillo que en la consola. Basta con definir un objeto de la clase PrintWriter e invocar con él a los métodos print o println según queramos saltar o no de línea al final del objeto a escribir que pasaremos como argumento. Ejemplo 7 Para escribir en un fichero de texto los valores de una lista de números enteros de manera que estén uno por línea, el código sería: File file = new File("enteros.txt"); PrintWriter ps = null; try { ps = new PrintWriter(file); for (Integer elem : lista) { ps.println(elem); } ps.close(); } catch (FileNotFoundException e) { System.out.println("Fichero no encontrado "+filename); } 2.4 Encapsulación de la escritura Igual que se hizo en la lectura de ficheros, podemos hacer un método estático que reciba una Collection de elementos de cualquier tipo y los escriba en un fichero de texto. Para ello podemos usar los tipos genéricos que se estudiaron al final del Tema 3. El código podría ser: public static <T> void escribeFichero(Collection<T> it, String filename) { 6. Lectura y escritura File file = new File(filename); try { PrintWriter ps = new PrintWriter(file); for (T elem : it) { ps.println(elem); } ps.close(); } catch (FileNotFoundException e) { System.out.println("Fichero no encontrado "+filename); } } Si quisiéramos que no escribiera cada dato en una nueva línea sino separar los objetos por un espacio en blanco o por una coma, bastaría con sustituir: ps.println(elem); por ps.print(elem+ " "); // ps.print(elem+ ","); 3. Ejercicios 1. Escriba en una clase de utilidad los métodos que nos permiten encapsular la lectura y escritura de ficheros, añadiendo cuando sea necesario el tratamiento de Excepciones mediante sentencias try/catch. 2. Lea desde teclado una lista de números reales y escríbala en un fichero de texto separados por blancos y en otro cada uno en una línea. 3. Lea una lista de palabras desde teclado, ordénela alfabéticamente y escriba el resultado en un fichero de texto de forma que cada palabra esté en una línea y éstas estén numeradas. 11