Programación 3

Universidad de Alicante, 2023–2024

Sistema de entrada/salida de Java

Java dispone de diversos paquetes para gestionar la entrada/salida de una aplicación. El que describimos aquí es el paquete java.io (Java IO), que se basa en el concepto de streams (flujos de datos).

Otro paquete dedicado a la gestión de entrada/salida es java.nio (Java New IO). Al final de esta página hay una breve descripción de este paquete.

El paquete Java IO se centra principalmente en la entrada y salida de archivos, conexiones de red, búferes de memoria, o dispositivos como la entrada y salida estándares.

Flujos (Streams)

Los flujos (nos referiremos de ahora en adelante a ellos como streams) son un concepto central en el sistema Java IO. Un stream es, conceptualmente, un flujo de datos sin fin. Podemos leer o escribir en un stream. Dependiendo de si lo usamos para leer o escribir, un stream estará conectado a una fuente de datos o a un destino de datos. Los streams pueden estar basados en bytes o en caracteres, es decir, pueden ser flujos de datos binarios o de texto.

InputStream/Reader, OutputStream/Writer

Las clases InputStream y OutputStream son clases abstractas que representan, respectivamente, un flujo de datos de entrada binario y un flujo de salida binario. Java IO contiene diversas subclases de ambas, dependiendo de cual sea la fuente o destino de los datos: FileInputStream, AudioInputStream, ByteArrayOutputStream, etc.

Para leer datos de una fuente utilizaremos un InputStream. Para escribir datos en un destino usaremos OutputStream.

Las clases Readery Writer son equivalentes a InputStream y OutputStream, pero basadas en caracteres. Para leer o escribir texto utilizaremos estas clases.

En las secciones Lectura de ficheros y Escritura de ficheros te contamos como hacerlo.

La clase File

File representa la ruta de acceso a un fichero o directorio. No se puede usar directamente para leer o escribir a fichero, sino como fuente o destino de un stream. La clase File permite además ciertas operaciones a nivel de sistema de archivos, como crear ficheros, borrarlos, saber si la ruta apunta a un directorio, recorrer un directorio, etc.

Una ruta tiene dos componentes:

  • Un prefijo opcional, como la letra de una unidad de disco en Windows (“C:") o”/” en sistemas UNIX para denotar la raíz del sistema de archivos.
  • Una secuencia de cero o más nombres, separadores por el separador de directorios del sistema (“/” en UNIX, “\” en Windows), de manera que cada nombre denota un directorio, excepto el último que puede se un directorio o un fichero.

Una ruta puede ser absoluta o relativa. Las rutas absolutas llevan prefijo, mientras que las relativas no. Por defecto, las clases en java.io resuelven las rutas relativas a partir del directorio de usuario actual, que típicamente es el directorio desde donde se invocó a la máquina virtual de Java.

Las instancias de File son inmutables. Una vez creadas no se les puede cambiar la ruta de archivo que representan.

Operaciones con File

Instanciar File
File file = new File("/tmp/my_file.txt");

El constructor toma como parámetro la ruta del archivo al que desea que apunte la instancia de File. Ten en cuenta que la ruta del archivo o directorio en realidad no tiene que hacer referencia a un archivo o directorio existente. Esto no producirá ninguna excepción. Es útil cuando queremos, por ejemplo, verificar si existe un archivo o crear uno nuevo.

Crear un fichero
File file = new File("/tmp/my_file.txt");

boolean fileCreated = file.createNewFile();

File.createNewFile() devuelve cierto si se pudo crear el fichero y falso si éste ya existía.

Comprobar si un fichero existe
File file = new File("/tmp/my_file.txt");

if (file.exists()) { ... }

También funciona con directorios.

Crear un directorio
File file = new File("/tmp/my_folder/my_subfolder");

if (file.mkdirs()) { /* path to my_subfolder created */ }
else { /* folder not created for any reason */ }

En este ejemplo, File.mkdirs() creará el directorio my_subfolder, pero también tmp y my_folder si estos no existían previamente. El método File.mkdir() (sin ‘s’ final) sólo intentaría crear my_subfolder.

Tamaño de un archivo
File file = new File("/tmp/my_file.txt");

long length = file.length();

length() devuelve el tamaño en bytes del fichero.

Renombrar o mover un fichero o directorio
    File file = new File("/tmp/myfile");
    if (file.renameTo(new File("/tmp/yourfile"))) { ... }

Ten en cuenta que, incluso si la operación tiene éxito, la instancia file sigue representado la ruta original a myfile. Recuerda que son instancias inmutables, por eso se pasa como argumento un nuevo objeto File.

Borrar un fichero o un directorio vacío
    File file = new File("/tmp/myfile");
    boolean deleted = file.delete();
Comprobar si una ruta es un fichero o un directorio
    File file = new File("/tmp/myfile");
    boolean isDirectory = file.isDirectory();
Leer el contenido de un directorio
    File file = new File("/tmp");
    String[] fileNames = file.list();
    File[]   files = file.listFiles();

Lectura de ficheros

Si dado un objeto File, queremos leer o escribir de él, primero tendremos que asociarlo a un stream. La mayoría de clases que nos permiten leer o escribir ficheros aceptan también directamente una cadena con la ruta del fichero.

Para leer de ficheros binarios:

    File file = new File("/tmp/myfile");
    InputStream input = new FileInputStream(file);
    int a = input.read();
    // leer y guardar en un array:
    byte[] bytes = new byte[10];
    int bytesRead = input.read(bytes);
    ...
    input.close();
    // no olvides cerrar siempre un 'stream' después de usarlo

Para leer ficheros de texto caracter a caracter:

    File file = new File("/tmp/myfile");
    Reader input = new FileReader(file);
    char c = input.read();
    ...
    input.close();

La clase Scanner

Normalmente es más cómodo leer cadenas de ficheros de texto. Para ello es más conveniente usar la clase java.util.Scanner. Además de proporcionar métodos para leer cadenas, esta clase ofrece métodos específicos para leer ciertos tipos de datos, como números int, long, double, etc., almacenados en ficheros de texto.

Scanner ‘trocea’ su entrada en tokens, secuencias de caracteres separados por delimitadores. Por defecto, los delimitadores son los espacios en blanco, tabuladores y saltos de línea.

Tomemos este fichero de texto (“myfile.txt”) como ejemplo:

Linea 1
Linea 2 con mas tokens
Linea 3 1234 12.34
Scanner sc = new Scanner(new File("myfile.txt"));
while (sc.hasNext()) {
    String token = sc.next();
    System.out.println(token);
}
sc.close(); // no olvides cerrarlo tras usarlo.

producirá la siguiente salida

Linea
1
Linea
2
con
mas
tokens
...

Podemos también leer líneas completas:

Scanner sc = new Scanner(new File("myfile.txt"));
while (sc.hasNextLine()) {
    String line = sc.nextLine();
    System.out.println(line);
}
sc.close();

El resultado en la salida estándar será

Linea 1
Linea 2 con mas tokens
Linea 3 1234 12.34

Podemos leer tipos específicos de datos numéricos. Supongamos que sabemos que “myfile.txt” tiene esta estructura:

12 12.34
14 1.234

con un entero seguido de un número real en cada línea:

Scanner sc = new Scanner(new File("myfile.txt"));
while (sc.hasNext()) {
    int i = sc.nextInt();
    double d = sc.nextDouble();
    System.out.println("Suman "+(d+i));
}
sc.close();

produce

Suman 24.34
Suman 15.234

Escritura de ficheros

Para escribir ficheros binarios:

    File file = new File("/tmp/myfile");
    // sobrescribe el fichero si ya existe
    OutputStream output = new FileOutputStream(file);
    // abre el fichero si ya existe y escribe al final del fichero.
    OutputStream output = new FileOutputStream(file, true);
    byte b = ...;
    output.write(b);
    byte[] bytes = new byte[10];
    // ...
    output.write(bytes);
    output.close();

Para escribir ficheros de texto, carácter a carácter:

    File file = new File("/tmp/myfile");
    Writer output = new FileWriter(file);
    char c = 'A';
    output.write(c);
    output.close();

La clase PrintStream

Normalmente es más cómodo escribir a fichero de texto a partir de cadenas u otros tipos de datos (siempre que tengan implementado el método toString()). Para ello podemos usar la clase java.io.PrintStream, que tiene métodos print/println() para escribir cadenas y otros tipos de datos primitivos a un stream en modo texto. De hecho, System.out es una instancia de PrintStream.

PrintStream output = new PrintStream(new File("myfile.txt"));
// ó simplemente
// PrintStream output = new PrintStream("myfile.txt");
 

El fichero se creará vacío y listo para escribir texto en él. Si ya existía se borrará su contenido, por lo que PrintStream no sirve para añadir texto a un fichero ya existente.

El stream así creado usa un buffer interno, por lo que la escritura a fichero no es inmediata. Debemos usar el método flush() si queremos que el contenido del buffer se vuelque a fichero. Al cerrarlo, se invoca automáticamente a flush().

output.print("Programacion 3");
output.println(" - entrada/salida");
output.flush(); // write pending text to file
output.println(23.4); // double
output.println(345); // int
// etc...
output.close(); // flush + close

PrintStream también posee un método format() para escribir texto con formato. Aquí puedes ver más información.

Java NIO

Java también tiene otra API de entrada/salida llamada Java NIO. Se encuentra en el paquete java.nio y contiene clases que hacen casi lo mismo que Java IO, pero Java NIO puede funcionar en modo no bloqueante. Por ejemplo, un proceso puede pedirle a un canal que lea datos en un búfer. Mientras el canal lee datos en el búfer, el proceso puede estar haciendo otra cosa: no tiene que esperar a que la operación de entrada/salida acabe. Una vez que los datos se leen en el búfer, el proceso puede continuar procesándolos. Lo mismo ocurre con la escritura de datos en los canales.

En la API de entrada/salida estándar, trabajamos con secuencias (streams) de bytes y secuencias de caracteres. En NIO trabajamos con canales y buffers. Los datos siempre se leen desde un canal a un búfer, o se escriben desde un búfer a un canal.

Java NIO también tiene el concepto de “selectores”. Un selector es un objeto que puede monitorizar múltiples canales para responder a ciertos eventos (p. ej.: conexión abierta, datos disponibles, etc.). Por lo tanto, un proceso puede monitorizar múltiples canales en busca de datos.

Te dejamos un par de enlaces útiles para usar Java NIO:

Tutorial de Java NIO

API oficial de Java NIO