Navegación de Archivos y I/O en Java

Buenas tardes, en esta entrada vamos a ver lo referente a la navegación de Archivos y I/O.


El tema de I/O es un tema muy grande en general, y las APIs de Java que trabajan con los elementos de I/O de una manera u otra son bastante grandes. Una discusión general sobre I/O podría incluir tópicos como I/O de archivos, I/O de consola, I/O de threads o hilos, rendimiento de I/O, I/O orientado a byte, I/O orientado a caracter, filtro y encapsulación de I/O, serialización, y mas.

Auí hay un sumario de las clases I/O que necesitaremos entender para empezar con este asunto:

  • File – La API dice que la clase File es una representación abstracta de rutas de archivo y directorios. La clase File no es usada actualmente para leer o escribir información, es usada para trabajar en un alto nivel, creando nuevos archivos vación, buscar archivos, borrar archivos, hacer directorios y trabajar con rutas.
  • FileReader – Esta clase es usada para leer archivos de caracteres. Su método read() es usado a bajo nivel, permitiendo leer caracteres de manera singular, todo el stream de caracteres, o un número fijado de caracteres. Los FileReaders están usualmente envueltos en objetos de alto nivel como BufferedReaders, el cual incrementan el rendimiento y proveen una manera conveniente de trabajar con información.
  • BufferedReader – Esta clase es usada para hacer clases Reader de bajo nivel como FileReader pero de una manera mas eficiente y mas fácil de usar. Comparado con los FileReaders, los BufferedReaders leen relativamente grandes cantidades de un archivo a la vez, y mantienen esta información en el buffer. Cuando preguntamos por el siguiente caracter o la siguiente linea de información, es recuperado del buffer, lo que minimiza el número de veces que se tiene que leer desde el archivo, lo cual es una operación mas lenta. En adición, la clase BufferedReader proveen métodos mas convenientes como readLine(), el cual nos permite leer la siguiente linea de caracteres de un archivo.
  • FileWriter – Esta clase es usada para escribir caracteres en archivos. Su método write() permite escribir caracteres o strings a un fichero. Esta clase normalmente está envuelta en objetos Writer de mas alto nivel como BufferedWriter o PrintWriters, lo cual proveen un mayor rendimiento y un alto nivel, con métodos mas flexibles para escribir información.
  • BufferedWriter – Esta clase es usada para hacer clases de bajo nivel como FileWriters de una manera mas eficiente y mas fáciles de usar. Comparado con las clases FileWriter, los BufferedWriters escribe relativamente grandes cantidades de información a un archivo, lo cual minimiza el número de veces que las operaciones de escritura de archivos se llevan a cabo, las cuales son operaciones mas lentas. La clase BufferedWriter tambien provee un método llamado newLine() el cual crea separadores de linea específicos de la plataforma de manera automática.
  • Console – Esta clase llegó nueva en Java 6, y provee de métodos para leer entradas desde la consola y escribir salidas formateadas a la consola.

  • Crear Ficheros Usando la Clase File

    Los objetos del tipo File son usados para representar afchivos (pero no los datos de los archivos) o directorios que existen en el disco físico de un ordenador. Vamos a empezar con algunos ejemplos fáciles de creación de archivos, estribir en ellos, y leyendo de ellos. Lo primero de todo, es crear un archivo y escribir unas cuantas lineas con algo en ellos:

    		import java.io.*;
    		
    		class Writer1 {
    			public static void main (String[] args){
    				File file = new File("fileWrite1.txt");	// Aún no existe el fichero
    			}
    		}
    		

    Si compilamos y ejecutamos este programa, cuando miramos el contenido de nuestro directorio actual para el proyecto, veremos que no hay ni existe ningúna inficación de un archivo llamado fileWrite1.txt. Cuando hacemos una nueva instancia de la clase File, no estamos haciendo un archivo, solo estamos creando el nombre del archivo. Una vez que tenemos el objeto File, hay diferentes maneras de crear un fichero. Vamos a ver que podemos hacer con el objeto File que acabamos de hacer:

    		import java.io.*;
    		
    		class Writer1 {
    			public static void main (String[] args){
    				try{										// Pueden ocurrir excepciones
    					boolean newFile = false;
    					File file = new File("fileWrite1.txt");	// Solo es un objeto
    					System.out.println(file.exists());		// Vemos si existe
    					newFile = file.createNewFile();			// Puede crear el archivo
    					System.out.println(newFile);			// ¿Existe?
    					System.out.println(file.exists());		// Comprobamos si existe
    				}catch(IOException e){ }
    		

    Esto producirá la salida:

    		false
    		true
    		true
    		

    Y producirá un archivo nuevo vación en nuestro directorio actual. Si ejecutamos el archivo de nuevo, obtendremos la salida:

    		true
    		false
    		true
    		

    Vamos a examinar que pasa:

    • Primera Ejecución: La primera llamada a exists() retorna false, lo cual ya esperabamos, porque el new File no crea un fichero en el disco. El método createNewFile() crea un archivo actual, y retorna true, indicando que un nuevo archivo fué creado, y que este no existía previamente. Finalmente, hemos llamado a exists() de nuevo, y esta vez retorna true, indicando que el fichero ya existe en el disco.
    • Segunda Ejecución: La primera llamada a exists() retorna true porque hemos construido el archivo en la anterior ejecución. Entonces llamamos al método createNewFile() que retorna false ya que el método no crea un archivo esta vez. Por supuesto, la última llamada a exists() retorna true.

    Han pasado un par de cosas nuevas en este código. Primero vemos que hemos metido nuestro código de creación del fichero en un bloque try/catch. Esto debe ser así para todas las operaciones de ficheros sobre I/O que escribamos. Las operaciones de I/O son operaciones arriesgadas. Vamos a mantenernos así, y vamos a ignoar las excepciones, pero tenemos que seguir la regla de manejar o declararlas ya que las excepciones I/O son excepciones comprobadas. Hablaremos sobre esto mas tarde. Hemos usado un par de métodos de File en este código:

    • boolean exists(): Este método retorna true si se ha encontrado el archivo actual.
    • boolean createNewFile(): Este método crea un nuevo fichero si no existía con anterioridad.
  • Usando FileWriter y FileReader

    En la práctica, probablemente no usemos las clases FileWriter y FileReader sin que estén envueltas (que veremos mas tarde). Como dijimos, vamos a hacer algo al archivo que creamos con anterioridad:

    		import java.io.*;
    		
    		class Writer2{
    			public static void main (String[] args){
    				char[] in = new char[50];		// Para guardar la entrada
    				int size = 0;
    				try {
    					File file = new File("fileWrite2.txt");	// Solo es un objeto
    					FileWriter fw = new FileWriter(file);	// Creamos un fichero actual y un
    															// objeto FileWriter
    					fw.write("howdynfolksn");				// Escribimos caracteres en el fichero
    					fw.flush();								// Limpiamos
    					fw.close();								// Cerramos el archivo cuando todo ha terminado
    					
    					FileReader fr = new FileReader(file)	// Creamos un objeto FileReader
    					size = fr.read(in);			// Leemos el archivo entero
    					System.out.println(size + " ");  // Cantidad de bytes leidos
    					for (char c : in){
    						System.print(c);		// Imprimimos el array
    					}
    					fr.close();		// De nuevo, siempre cerramos
    				}catch(IOException ex){ }
    			}
    		}
    		

    Esto producirá la salida:

    		12 howdy
    		folks
    		

    Aquí vemos todo lo que ha pasado:

    • FileWriter fw = new FIleWriter(file) ha hecho 3 cosas:
      1. Crea una nueva variable de referencia de FileWriter, fw.
      2. Crea un nuevo objeto FileWriter, y lo asigna a fw.
      3. Crea un archivo vación en el disco.
    • Escribe 12 caracteres en el fichero con el método write(), y hacemos flush() y close().
    • Hemos hecho un nuevo objeto FileReader, el cual ha abierto el archivo en el disco para lectura.
    • El método read() lee el archivo entero, caracter a caracter, y lo ha guardado en char[] in.
    • Hemos imprimido por pantalla el número de caracteres que leimos mediante la variable size, y hemos creado un bucle que recorre el array imprimiendo cada caracter que hemos leido, y luego hemos cerrado el archivo.

    Vamos a analizar los métodos flush() y close(). Cuando escribimos datos mediante un flujo, puede ocurrir que existe algo aún en el buffer, y no sabemos con exactitud cuando se enviará lo último que hayamos enviado. Es por ello por lo que debemos realizar el método flush() en operaciones de escritura, el cual garantiza que todo lo que hemos enviado se escriba en el fichero. Por otro lado, siempre que hayamos terminado de hacer cosas en un fichero, ya sean operaciones de lectura oe scritura, debemos invocar el método close(). Cuando realizamos operaciones de I/O estamos usando una camtidad de operaciones de recursos del sistema bastante grande, por tanto, cuando hayamos terminado, invocando el método close() liberaremos esos recursos.

    Ahora, volvemos a nuestro anterior ejemplo. Este programa funciona, pero pueden ocurrir varias cosas en un par de situaciones:

    1. Cuando estabamos escribiendo datos, hemos insertado manualmente separadores de línea (En este caso n), en nuestra información.
    2. Cuando estamos leyendo información, hemos puesto toda ella en un array de caracteres. Al ser un array, hemos de definir su tamaño, por lo que podemos tener un serio problema si no lo hemos hecho lo suficientemente grande. Podriamos haber leido la información caracter a caracter hasta encontrar el fin del archivo antes de cada read(), pero es igual de peligroso.

    Debido a estas limitaciones, usaremos en mayor medida las clases I/O de mayor nivel como BufferedWritter o BufferedReader en combinación con FileWriter y FileReader.

  • Combinando Clases I/O

    El sistema de I/O en Java fue diseñado con la idea de usar una gran cantidad de clases en combinación entre unas y otras. Combinando clases I/O es a menudo llamado “wrapping” y otras veces “chaining”. El package de java.io contiene cerca de 50 clases, 10 interfaces y 15 excepciones. Cada clase en el package tiene un propósito específico (Creando una alta cohesión), y la sclases fueron diseñadas para ser combinadas las unas con las otras de múltiples maneras, para manejar la gran variedad de situaciones que se nos pueden poner por delante.

    Cuando es tiempo de hacer algo de I/O en la vida real, podemos encontrarnos en el problema de que hacer con la API de java.io, intentando saber que clases necesitaremos, y como usarlas entre ellas. Es por ello que con esta tabla podemos encontrar alguna ayuda:

    Clase java.io Hereda de Argumentos del Constructor Métodos clave
    File Object File, String
    String
    String, String
    createNewFile()
    delete()
    exists()
    isDirectory()
    isFile()
    list()
    mkdir()
    renameTo
    FileWriter Writer File
    String
    close()
    flush()
    write()
    BufferedWriter Writer Writer close()
    flush()
    newLine()
    write()
    PrintWriter Writer File (como en Java 5)
    String (como en Java 5)
    OutputStream
    Writer
    close()
    flush()
    format(), printf()
    print(), println()
    write()
    FileReader Reader File
    String
    read()
    BufferedReader Reader Reader read()
    readLine()

    Ahora vamos a decir que queremos encontrar una manera menos dolorosa de escribir información en un archivo y leer el contenido del fichero para pasarlo a memoria. Empezando con esta tarea de escribir información, aquí vemos un proceso para determinar que clases necesitaremos, y como las podemos combinar entre ellas:

    1. Sabemos que queremos usar el objeto File. Por lo que otras clases que vayamos a usar, una de ellas debe tener un constructor que tenga como argumento un objeto del tipo File.
    2. Buscar un método que suene potente, con la manera mas facil de lograr la tarea. Si miramos la tabla anterior podemos ver que la clase BufferedWriter tiene un método newLine(). Esto suena mejor para situaciones en las que tengamos que insertar un salto de linea manualmente, pero si miramos mas vemos que la clase PrintWriter tiene un método llamado println(). Esta parece la manera mas fácil para el enfoque de la tarea, por lo que usaremos esta.
    3. Cuando miramos el constructor de PrintWriter, vemos que podemos construir un objeto PrintWriter si tenemos un objeto del tipo File, por lo que lo único que necesitamos para construir el objeto PrintWriter es lo siguiente:
      				File file = new File("fileWriter2.txt");	// Creamos un fichero
      				PrintWriter pw = new PrintWriter(file);		// Pasamos el fichero al constructor de PrintWriter
      				

    Ahora vamos a ver una cuestión. Antes de Java 5, PrintWriter no tenía constructores que cogiera o un String o un File. Si queríamos escribir algo de código I/O en Java 1.4, ¿Como haríamos que un PrintWriter escribiera información en un archivo?. Podríamos consultarlo viendo la tabla anterior.

    Aquí vemos una manera de resolver este puzzle: Primero, sabemos que vamos a crear un archivo File y un archivo PrintWriter. Podemos ver en la tabla anterior que un PrintWriter puede ser construido tambien usando un objeto Writer. Sin embargo la clase Writer no es una clase que veamos en la tabla, por lo cual para nuestros propósitos está bien: cualquier clase que herede de Writer es un candidato. Por tanto, podemos ver que FileWriter tiene 2 atributos que estabamos buscando:

    1. Puede ser construido usando File.
    2. Hereda de Writer.

    Con toda esta información, podemos ponerlo todo como en el siguiente código (recordemos, que este ejemplo es de Java 1.4):

    		File file = new File("fileWrite2.txt");
    		FileWriter fw = new FileWriter(file);
    		PrintWriter pw = new PrintWriter(fw);	// Crea un objeto PrintWriter
    												// que enviará la salida a un Writer
    		pw.println("howdy");					// Escribe los datos
    		pw.println("folks");
    		

    En este punt debería ser mas fácil poner el código para que de manera mas fácil pudieramos leer los datos del archivo puestos en memoria. De nuevo, mirando la tabla, vemos que hay un método llamado readLine() que suena mucho mejor que otro para leer datos. Haciendo un proceso similar llevamos al siguiente código:

    		File file = new File("fileWrite2.txt");	// Creamos el objeto File y abrimos "fileWrite2.txt"
    		
    		FileReader fr = new FileReader(file);	// Creamos un FileReader pra obtener los datos de file
    		
    		BufferedReader br = new BufferedReader(fr);	// Creamos un BufferedReader para obtener los datos
    													// de un Reader
    		
    		String data = br.readLine();		// Leemos algo de información
    		
  • Trabajando con Ficheros y Directorios

    Antes hemos visto que la clase File es usada para crear ficheros y directorios. En adición, los métodos de File pueden ser usados para borrar archivos, renombrar archivos, determinar si el archivo existe, crear archivos temporales, cambiar los atributos del archivo y diferenciar entre ficheros y directorios. Un punto que puede llegar a confundir es que un objeto del tipo File puede usarse para representar ya sea archivos o directorios. Vamos a hablar sobre ambos casos a continuación.

    Anteriormente vimos la siguiente sentencia:

    	File file = new File("foo");
    	

    Esta anterior sentencia siempre creará un objeto File, y hará las 2 siguientes cosas:

    1. Si “foo” no existe, no se creará ningún archivo.
    2. Si “foo” ya existía, el nuevo objeto File se referirá al archivo existente.

    Vemos que la sentencia File file = new File(“foo”); NUNCA crea un archivo. Hay 2 maneras de crear un archivo:

    1. Invocar el método createNewFile() en el objeto File. Por ejemplo:
      			File file = new File("foo");	// No hay fichero aún
      			file.createNewFile();			// Hace un fichero, "foo" el cual es asignado a file
      			
    2. Crear un Writer o un Stream. Específicamente, crear un FileWriter, un PrintWriter o un FileOutputStream. En cualquier lugar donde hayamos creado cualquiera de estas clases, automáticamente crearemos un archivo, a no ser que ya exista, por ejemplo:
      			File file = new File("foo");	// No hay fichero aún
      			PrintWriter pw = new PrintWriter(file);		// Crea un objeto PrintWriter y hace el archivo, "foo"
      														// el cual es asignado, y asigna pw al PrintWriter
      			

    Crear un directorio es similar a crear un fichero. Tal como hemos creado un fichero, crear un directorio consta de 2 procesos, primero creamos un objeto File, y entonces creamos el directorio usando el método mkdir() que vemos a continuación:

    	File myDir = new File("myDir");		// Crea un objeto
    	myDir.mkdir();		// Crea el directorio
    	

    Una vez que tenemos el directorio, ponemos un archivo dentro de el, y trabajaremos con estos ficheros:

    	File myFile = new File(myDir, "myFile.txt");
    	myFile.createNewFile();
    	

    Este código está creando un nuevo archivo en un subdirectorio. Ya que hemos proveído de un subdirectorio al constructor, podemos usar su variable de referencia del archivo. En este caso, aquí hay una manera en la que podríamos escribir algo de información al fichero myFile:

    	PrintWriter pw = new PrintWriter(myFile);
    	pw.println("new stuff");
    	pw.flush();
    	pw.close();
    	

    Tenemos que tener cuidado aún asi al crear nuevos directorios. Como hemos visto, construir un Writer o un Stream siempre creará un archivo automáticamente si no existe, pero no es así para un directorio:

    	File myDir = new File("myDir");
    	// myDir.mkdir();		// Omitimos el mkdir()
    	File myFile = new File(myDir, "myFile.txt");
    	myFile.createNewFile();		// Excepción si no hay mkdir
    	

    Esto generará una excepción como lo siguiente:

    	java.io.IOException: No such file or directory
    	

    Podemos referir un objeto File a un fichero o directorio existente. Por ejemplo, asumimos que hemos creado un subdirectorio llamado existingDir en el cual reside el archivo existingDirFile.txt, el cual contiene varias lineas de texto. Cuando ejecutamos el siguiente código:

    	File existingDir = new File("existingDir");	// asignamos un directorio
    	System.out.println(existingDir.isDirectory());
    	File existingDirFile = new File(existingDir, "existingDirFile.txt");	// Asignamos el fichero
    	System.out.println(existingDirFile.isFile());
    	FileReader fr = new FileReader(existingDirFile);
    	BufferedReader br = new BufferedReader(fr);	// creamos un Reader
    	String s;
    	while ((s = br.readLine()) != null) {		// Leemos información
    		System.out.println(s);
    	}
    	br.close();
    	

    La siguiente salida será generada:

    	true
    	true
    	existing sub-dir data
    	line 2 of text
    	line 3 of text
    	

    Pongamos especial atención a lo que el método readLine() retorna. Cuando no hay mas datos que leer, readLine() retorna null, lo cual es nuestra señal para parar de leer el fichero. Tambien, atención a ue no hemos invocado el método flush(). Cuando leemos un archivo, no se requiere el uso de este método, por lo que no vamos a encontrar ningún método flush() en cualquier subtipo de la clase Reader.

    En adición a crear ficheros, la clase File tambien nos permite hacer otras cosas como renombrar o borrar ficheros. El siguiente código demuestra un poco los métodos mas comunes para borrar ficheros y directorios (usando delete()) y renombrar archivos y directorios (usando renameTo()):

    	File delDir = new File("deldir");
    	delDir.mkdir();		// Crea el directorio deldir
    	
    	File delFile1 = new File(delDir, "delFile1.txt");
    	delFile1.createNewFile();	// Crea un fichero en el directorio
    	
    	File delFile2 = new File(delDir, "delFile2.txt");
    	delFile2.createNewFile();	// crea otro archivo en el directorio
    	
    	delFile1.delete();	// Borra un archivo
    	System.out.println("delDir is " + delDir.delete()); // Se intenta borrar el directorio
    	
    	File newName = new File(delDir, "newName.txt");
    	delFile2.renameTo(newName);	// Se renombra el archivo
    	
    	File newDir = new File("newDir");
    	delDir.renameTo(newDir);	// Renombra el directorio
    	
    	

    Esto retornará:

    	delDir is false
    	

    Tras todo este código, nos queda un directorio llamado newDir que contiene un fichero llamado newName.txt. Aquí tenemos varias reglas que podemos deducir del resultado:

    • delete(): No podemos borrar un directorio si no está vacio, lo cual es por lo que la invocacion de delDir.delete() ha fallado.
    • renameTo(): Debemos dar al objeto File existente un nuevo objeto File válido con el nuevo nombre que queramos. (Si newName había sido null habríamos tenido un NullPointerException).
    • renameTo(): Podemos renombrar el directorio, incluso aunque no esté vacío.

    Hay mucho mas que aprender en el package java.io, pero con esto podremos empezar a crear soluciones básicas y aprender el uso de las operaciones de lectura y escritura en Java. Tambien podemos buscar un archivo. Asumiendo que tenemos un directorio llamado searchThis en el que queremos buscar podemos usar el método File.list() para crear un array de String de ficheros y directorios, el cual hará uso del bucle for mejorado para iterar a través de el e imprimirlo:

    	String[] files = new String[100];
    	File search = new File("searchThis");
    	files = search.list();		// Crea la lista
    	
    	for(String fn : files){
    		System.out.println("found " + fn);
    	}
    	

    Podríamso obtener el resultado siguiente en el caso de que tuvieramos los siguientes directorios o archivos:

    	found dir 1
    	found dir2
    	found dir3
    	dounf dile1.txt
    	found file2.txt
    	

    En esta sección hemos investigado la superficie de lo que tenemos disponible en el package java.io. Libros enteros se han escrito sobre este package, pero nosotros obviamente hemos cubierto solo una porción muy pequeña de la API, pero que sin duda es frecuentemente usado. Por otra parte, si entendemos bien todo lo que hemos usado, tendremos mayor facilidad para aplicarlo y para mejorar en ello.

  • La Clase java.io.Console

    En Java 6 se introdujo la clase Console. En este contexto, la consola es un dispositivo físico con un teclado y una pantalla. Si estamos ejecutando Java 6 desde la linea de comandos, tendremos acceso al objeto Console, en el que podemos obtener la referencia invocando System.console(). Tenemos que tener en cuenta tambien que puede que sea posible que nuestro programa Java se esté ejecutando en un entorno que no tiene acceso al objeto Console, por lo que tenemos que estar seguros que nuestra invocación de System.console() retorna una referencia válida de la consola y no es null.

    La clase Console hace mas fácil el uso de aceptar entradas desde la línea de comandos, los cuales pueden ser mostrados o no mostrados (como un password), y hace mas facil el uso de escribir salidas formateadas a la linea de comandos.

    Para el lado de las entradas, los métodos que tenemos que entender son readLine y readPassword. El método readLine retorna un string que contiene lo que hayamos introducido. Sin embargo, el método readPassword no retorna un string, retorna un array de caracteres. Aquí está la razón de ello: Una vez que tenemos un password, queremos verificarlo y entonces queremos removerlo de la memoria. Si se devuelve un string, podría existir en algún lugar del pool y por tanto algún hacker podría encontrarlo.

    Vamos a ver un pequeño programa que usa la consola para soportar el testeo de otra clase:

    	import java.io.Console;
    	
    	public class NewConsole{
    		public static void main (String[] args){
    			Console c = System.console();		// obtenemos la consola
    			char[] pw;
    			pw = c.readPassword("%s", "PW: ");	// Retorna un char[]
    			for (char ch : pw)
    				c.format("%c ", ch);		// Salida formateada
    			c.format("n");
    			
    			MyUtility m = new MyUtility();
    			while (true){
    				name = c.readLine("%s", "input?: ");	// Retorna un string
    				
    				c.format("output: %s n", mu.doStuff(name));
    			}
    		}
    	}
    	
    	class MyUtility{
    		String doStuff(String arg1){
    			return "result is " + arg1;
    		}
    	} 
    	

    Vamos a ver un repaso al código:

    • En la línea 1, obtenemos un nuevo objeto Console. Recordemos que no podemos decir lo siguiente:
      			Console c = new Console();
      			
    • En la linea 2, hemos invocado el readPasswod, el cual retorna un char[], no un String. Cuando probamos este código, el password no se va mostrando en la pantalla.
    • En la linea 3, estamos mostrando el password manualmente, separando cada caracter con un espacio.
    • En la linea 4, invocamos el método readLine, el cual retorna un String.
    • En la línea 5 está la clase que queremos testear.

    La clase Console tiene mas capacidades de las que hemos visto aquí, pero con esto ya tenemos una idea previa de lo que es y como se usa.


Con esta entrada hemos visto el uso básico de las clases mas usadas, o usadas con mas frecuencia del package java.io de Java, el cual es usado para el tratamiento de archivos y directorios, así como para escribir en archivos o leer información de estos. Hemos visto las principales diferencias entre el archivo File, el cual no crea un archivo, sino que contiene su ruta o nombre, y luego mediante otros métodos podemos crear el archivo, borrarlo, renombrarlo o moverlo donde queramos.

Sin mas, cualquier aporte o corrección es bienvenido.

Saludos!!!