Introducción a NIO.2, el sistema de entrada/salida de Java

Escrito por el , actualizado el .
blog java planeta-codigo programacion
Enlace permanente Comentarios

Una de las tareas más importante que realizan algunas aplicaciones es el manejo de la entrada y salida ya sea al sistema de ficheros o a la red. Desde las versiones iniciales de Java se ha mejorado soporte añadiendo programación asíncrona de E/S, permitir obtener información de atributos propios del sistema de archivos, reconocimiento de enlaces simbólicos y facilitado de algunas operaciones básicas.

Java

En las primeras versiones de Java el sistema de entrada/salida proporcionado en el paquete java.io era básico. En la versión 1.4 de Java se añadió un nuevo sistema de entrada/salida llamado NIO para suplir algunas de sus deficiencias que posteriormente en Java 7 se mejoró aún más con NIO.2. Entre las mejoras se incluyen permitir navegación de directorios sencillo, soporte para reconocer enlaces simbólicos, leer atributos de ficheros como permisos e información como última fecha de modificación, soporte de entrada/salida asíncrona y soporte para operaciones básicas sobre ficheros como copiar y mover ficheros.

Las clases principales de esta nueva API para el manejo de rutas, ficheros y operaciones de entrada/salida son las siguientes:

  • Path: es una abstracción sobre una ruta de un sistema de ficheros. No tiene porque existir en el sistema de ficheros pero si si cuando se hacen algunas operaciones como la lectura del fichero que representa. Puede usarse como reemplazo completo de java.io.File pero si fuera necesario con los métodos File.toPath() y Path.toFile() se ofrece compatibilidad entre ambas representaciones.
  • Files: es una clase de utilidad con operaciones básicas sobre ficheros.
  • FileSystems: otra clase de utilidad como punto de entrada para obtener referencias a sistemas de archivos.

Con la clase Path se pueden hacer operaciones sobre rutas como obtener la ruta absoluta de un Path relativo o el Path relativo de una ruta absoluta, de cuanto elementos se compone la ruta, obtener el Path padre o una parte de una ruta. Otros métodos interesantes son relativize(), normalize(), toAbsolutePath(), resolve(), startsWith() y endsWith().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Path
System.out.println("# info");
Path relative = Paths.get(".");
Path absolute = relative.toAbsolutePath().normalize();

System.out.printf("Relative: %s%n", relative);
System.out.printf("Absolute: %s%n", absolute);
System.out.printf("Name count: %d%n", absolute.getNameCount());
System.out.printf("Parent: %s%n", absolute.getParent());
System.out.printf("Subpath(0, 2): %s%n", absolute.subpath(0, 2));
Main-1.java
1
2
3
4
5
6
# info
Relative: .
Absolute: /home/picodotdev/Software/personal/blog-ejemplos/JavaNIO
Name count: 6
Parent: /home/picodotdev/Software/personal/blog-ejemplos
Subpath(0, 2): home/picodotdev
info.out

Utilizando estas clases expondré algunos ejemplos siendo el primero recorrer el listado de archivos o también se podría hacer el listado de forma recursiva de un directorio e imprimir la información de cada archivo como nombre, si es un enlace simbólico, permisos propietario, fecha de última modificación y tamaño utilizando los siguiente métodos similar a lo que hace el comando ls de GNU/Linux:

Al igual que es posible leer los permisos también es posible establecerlos con el método Files.setPosixFilePermissions().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ls -la
System.out.println();
System.out.println("# ls -la");
Files.walk(relative, 1).sorted((p1, p2) -> {
    return p1.getFileName().toString().compareTo(p2.getFileName().toString());
}).forEach(path -> {
    try {
        System.out.println(toLine(path));
    } catch (Exception e) {
        e.printStackTrace();
    }
});

...

private static String toLine(Path path) throws IOException  {
    Map<String, Object> attributtes = Files.readAttributes(path, "posix:*");
    PosixFileAttributes posixFileAttributes = Files.getFileAttributeView(path, PosixFileAttributeView.class).readAttributes();
    String type = (posixFileAttributes.isDirectory()) ? "d": "-";
    String permissions = PosixFilePermissions.toString(posixFileAttributes.permissions());
    String owner = posixFileAttributes.owner().getName();
    String group = posixFileAttributes.group().getName();
    long size = posixFileAttributes.size();
    ZonedDateTime date = posixFileAttributes.lastModifiedTime().toInstant().atZone(ZoneId.of("Europe/Madrid"));
    String lasModified = DateTimeFormatter.ofPattern("d MMM HH:mm").format(date);
    String name = path.getFileName().toString();
    return String.format("%s%s %16s %10s %5d %12s %s", type, permissions, owner, group, size, lasModified, name);
}
Main-2.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ ls -la
total 40
drwxr-xr-x  11 picodotdev  110277442   352  7 abr 12:11 .
drwxr-xr-x  78 picodotdev  110277442  2496  7 abr 10:41 ..
drwxr-xr-x   5 picodotdev  110277442   160  7 abr 10:47 .gradle
drwxr-xr-x   6 picodotdev  110277442   192  7 abr 12:23 .idea
drwxr-xr-x   4 picodotdev  110277442   128  7 abr 10:50 build
-rw-r--r--   1 picodotdev  110277442   156  7 abr 10:49 build.gradle
drwxr-xr-x   3 picodotdev  110277442    96  7 abr 10:42 gradle
-rwxr-xr-x   1 picodotdev  110277442  5296  7 abr 10:42 gradlew
-rw-r--r--   1 picodotdev  110277442  2260  7 abr 10:42 gradlew.bat
-rw-r--r--   1 picodotdev  110277442   354  7 abr 10:42 settings.gradle
drwxr-xr-x   4 picodotdev  110277442   128  7 abr 10:48 src

# ls -la (with Java)
drwxr-xr-x  picodotdev  110277442   352  7 abr 12:11 .
drwxr-xr-x  picodotdev  110277442   160  7 abr 10:47 .gradle
drwxr-xr-x  picodotdev  110277442   192  7 abr 12:23 .idea
drwxr-xr-x  picodotdev  110277442   128  7 abr 10:50 build
-rw-r--r--  picodotdev  110277442   156  7 abr 10:49 build.gradle
drwxr-xr-x  picodotdev  110277442    96  7 abr 10:42 gradle
-rwxr-xr-x  picodotdev  110277442  5296  7 abr 10:42 gradlew
-rw-r--r--  picodotdev  110277442  2260  7 abr 10:42 gradlew.bat
-rw-r--r--  picodotdev  110277442   354  7 abr 10:42 settings.gradle
drwxr-xr-x  picodotdev  110277442   128  7 abr 10:48 src
ls.out

Las operaciones de crear directorios o archivos, copiar archivos, moverlos y eliminarlos son muy comunes de modo que la clase Files ofrece varios métodos que con una única línea permite hacer estas operaciones de forma sencilla. El siguiente ejemplo crea un archivo, lo copia, lo mueve y finalmente lo elimina.

1
2
3
4
5
6
7
// File operations
Path file = Paths.get("build.gradle");
Path backup = Paths.get("build.gradle.backup");
Path rename = Paths.get("build.gradle.backup.1");
Files.copy(file, backup, StandardCopyOption.REPLACE_EXISTING);
Files.move(backup, rename, StandardCopyOption.REPLACE_EXISTING);
Files.delete(rename);
Main-3.java

Para leer el contenido de archivos la clase Files ofrece los métodos newBufferedReader(), newBufferedWrite(), newInputStream() y newOutputStream() junto con otros como readAllLines() y readAllBytes().

1
2
3
4
5
6
// Read
System.out.println("");
System.out.println("# build.gradle");
Files.readAllLines(file).stream().forEach(l -> {
    System.out.println(l);
});
Main-4.java

En cuanto a la programación de entrada/salida asíncrona se ofrecen dos paradigmas uno basado en la clase Future y otro en funciones de rellamada o callbacks. La programación asíncrona evita bloquear el hilo que ejecuta el código y aprovecha mejor los procesadores multinúcleo con lo que se mejora el rendimiento de las aplicaciones. Para los ficheros se usa la clase AsynchronousFileChannel y para flujos de red AsynchronousSocketChannel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//
System.out.println("");
System.out.println("# async with Future");
{
    AsynchronousFileChannel channel = AsynchronousFileChannel.open(file, StandardOpenOption.READ);
    ByteBuffer buffer = ByteBuffer.allocate(100_000);
    Future<Integer> result = channel.read(buffer, 0);
    // ...
    Integer bytesRead = result.get();
    System.out.println(new String(buffer.array(), "utf-8"));
}
Main-5.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
System.out.println("");
System.out.println("# async with callback");
{
    AsynchronousFileChannel channel = AsynchronousFileChannel.open(file, StandardOpenOption.READ);
    ByteBuffer buffer = ByteBuffer.allocate(100_000);
    channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
        public void completed(Integer result, ByteBuffer buffer) {
            try {
                System.out.println(new String(buffer.array(), "utf-8"));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        public void failed(Throwable exception, ByteBuffer attachment) {
            System.out.println(exception.getMessage());
        }
    });

    Thread.sleep(2000);
}
Main-6.java

Si se desea profundizar más en NIO y NIO.2 el libro The Well-Grounded Java Developer dedica un capítulo introductorio en el que me he basado para realizar este artículo, el libro Java I/O, NIO and NIO.2 está completamente dedicado al nuevo sistema de entrada/salida de Java y el tutorial Java Basic I/O también está muy bien como introducción.

En el artículo monitorizar archivos con Java muestro como recibir eventos cuando se añade, elimina o modifica algún archivo de los observados usando la clase WatchService.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: