# Java - Lire et écrire des fichiers avec java.nio

# L'API java.nio.Files

Le package java.nio a été introduit en Java 7 avec la JSR 203 (opens new window) pour remplacer l'API existante java.io.File. Comme on va le voir, la nouvelle API de java.nio.Files (opens new window) rend la lecture et l'écriture de fichiers très simple. Cet article décrit ces méthodes de lecture et d'écriture de fichier mais l'API comporte également tout ce qu'il faut pour:

# Lecture et écriture de petits fichiers

Pour la lecture et l'écriture de petits fichiers - ou des usages simple - l'API NIO fournit des méthodes permettant de lire et écrire les fichiers dans leur intégralité, qu'ils soient textuels ou binaires.

# Fichiers textes

La méthode Files.write(Path path, Iterable<? extends CharSequence> path, OpenOption... options) écrit chaque CharSequence d'un Iterable (par exemple List<String>) comme une ligne du fichier avec l'encodage UTF-8. Si aucune OpenOption n'est précisée, cette méthode utilise par défaut CREATE, TRUNCATE_EXISTING et WRITE, créant le fichier s'il n'existe pas et le vidant avant d'écrire s'il existe.







 





@Test
@Order(1)
public void writeAllLines() {
  final var pathToFile = someDir.resolve("file.txt");
  final var lines = List.of("Hello", "World", "!");
  try {
    Files.write(pathToFile, lines);
  } catch (IOException e) {
    fail(String.format("Unable to write to %s", pathToFile), e);
  }
}
1
2
3
4
5
6
7
8
9
10
11

TIP

En plus des options pour la création et l'ouverture du fichier (voir StandardOpenOption (opens new window) pour une liste exhaustive), il est possible de préciser le Charset via la méthode Files.write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption... options)

La lecture du fichier passe quant à elle par la méthode Files.readAllLines(Path) qui renvoie directement les lignes du fichier dans une List<String>.






 






@Test
@Order(2)
public void readAllLines() {
  final var pathToFile = someDir.resolve("file.txt");
  try {
    final var lines = Files.readAllLines(pathToFile);
    assertThat(lines).containsExactly("Hello", "World", "!");
  } catch (IOException e) {
    fail(String.format("Unable to read %s", pathToFile), e);
  }
}
1
2
3
4
5
6
7
8
9
10
11

TIP

Là encore, le charset par défaut est UTF-8, il peut se changer via la méthode Files.readAllLines(Path path, Charset cs)

# Fichiers binaires

Des méthodes équivalentes existent pour la lecture et l'écriture de fichiers binaires, modulo la gestion du charset et de la notion de lignes.

Pour l'écriture, la méthode Files.write(Path path, byte[] bytes, OpenOption... options) écrit un tableau de bytes.







 





@Test
@Order(1)
public void writeAllBytes() {
  final var pathToFile = someDir.resolve("file.bin");
  final var content = "Hello World !".getBytes();
  try {
    Files.write(pathToFile, content);
  } catch (IOException e) {
    fail(String.format("Unable to write bytes to %s", pathToFile), e);
  }
}
1
2
3
4
5
6
7
8
9
10
11

Pour la lecture, la méthode Files.readAllBytes(Path path) renvoit le tableau de bytes correspondant.






 






@Test
@Order(2)
public void readAllBytes() {
  final var pathToFile = someDir.resolve("file.bin");
  try {
    final var content = Files.readAllBytes(pathToFile);
    assertThat(content).isEqualTo("Hello World !".getBytes());
  } catch (IOException e) {
    fail(String.format("Unable to read %s", pathToFile), e);
  }
}
1
2
3
4
5
6
7
8
9
10
11

# Lecture et écriture bufferisée

Suivant la taille du fichier et les besoins de performances, il n'est pas toujours adapté d'écrire ou de lire l'intégralité du contenu en une fois. L'utilisation d'un buffer permet l'accès aux fichiers par morceau sans pour autant générer des I/Os pour chaque bit.

# Fichiers textes

Pour les fichiers textes, des méthodes d'accès bufferisées sont directement accessibles sur la classe Files.

La méthode Files.newBufferedWriter(Path path, OpenOption... options) retourne un BufferedWriter que l'on peut utiliser pour écrire le contenu d'un fichier.






 

 







@Test
@Order(1)
public void writeBufferedLines() {
  final var pathToFile = someDir.resolve("file.txt");
  final var lines = List.of("Hello", "World", "!");
  try (final var writer = Files.newBufferedWriter(pathToFile)) {
    for (String line : lines) {
      writer.write(line);
      writer.newLine();
    }
  } catch (IOException e) {
    fail(String.format("Unable to write to %s", pathToFile), e);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

TIP

Comme pour la méthode readAllLines discutée précédemment, il est là aussi possible de préciser le charset en plus des options d'ouverture et de création du fichier avec la méthode Files.newBufferedWriter(Path path, Charset cs, OpenOption... options)

La méthode Files.newBufferedReader(Path path) est l'équivalent pour la lecture du fichier. Elle renvoie un BufferedReader qui expose des méthodes pour la lecture bufferisée des lignes du fichier.






 


 








@Test
@Order(2)
public void readBufferedLines() {
  final var expectedLines = List.of("Hello", "World", "!");
  final var pathToFile = someDir.resolve("file.txt");
  try (final var reader = Files.newBufferedReader(pathToFile)) {
    String line;
    var i = 0;
    while ((line = reader.readLine()) != null) {
      assertThat(line).isEqualTo(expectedLines.get(i));
      i++;
    }
  } catch (IOException e) {
    fail(String.format("Unable to read %s", pathToFile), e);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Fichiers binaires, le ByteChannel

L'API NIO.2 introduit les channels I/O qui donnent un accès en lecture/écriture sur les fichiers. Ils offrent également des fonctionnalités avancés:

  • random access: la possibilité de changer de position,
  • mapping d'une région du fichier directement en mémoire,
  • lock d'une région d'un fichier.

Un ByteChannel s'obtient directement en appelant la méthode Files.newByteChannel​(Path path, OpenOption... options), qui retourne un SeekableByteChannel.

Si aucune options n'est spécifiée, le channel est ouvert en lecture uniquement; pour l'ouvrir en écriture, il faut a minima spécifier StandardOpenOption.WRITE. Dans l'exemple ci-dessous, comme le channel n'est ouvert qu'en écriture, le SeekableByteChannel est casté en WritableByteChannel afin de limiter les méthodes disponibles.







 









@Test
@Order(1)
public void writeToByteChannel() {
  final var words = List.of("Hello", "World", "!");
  final var pathToFile = someDir.resolve("file.chn");
  final var options = Set.of(WRITE, CREATE, TRUNCATE_EXISTING);
  try (WritableByteChannel byteChannel = Files.newByteChannel(pathToFile, options)) {
    for (var word : words) {
      var byteBuffer = ByteBuffer.wrap(word.getBytes());
      byteChannel.write(byteBuffer);
    }
  } catch (IOException e) {
    fail(String.format("Unable to write to %s", pathToFile), e);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

TIP

Lors de la création d'un fichier (options CREATE ou CREATE_NEW) il est également possible de spécifier les attributs de ce fichier via la méthode newByteChannel​(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs). Un exemple de la création de ces attributs est donnée ici (opens new window)

De la même manière, lorsque le channel n'est ouvert qu'en lecture, il est conseillé de le caster en ReadableByteChannel.







 












@Test
@Order(2)
public void readFromByteChannel() {
  final var encoding = System.getProperty("file.encoding");
  final var pathToFile = someDir.resolve("file.chn");
  final var content = new StringBuilder();
  try (ReadableByteChannel byteChannel = Files.newByteChannel(pathToFile)) {
    final var buffer = ByteBuffer.allocate(5);
    int byteCount;
    while ((byteCount = byteChannel.read(buffer)) > 0) {
      content.append(Charset.forName(encoding).decode(buffer.slice(0, byteCount)));
      buffer.rewind();
    }
  } catch (IOException e) {
    fail(String.format("Unable to read %s", pathToFile), e);
  }
  assertThat(content.toString()).isEqualTo("HelloWorld!");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Lecture et écriture en flux non bufferisée

la classe Files donne également accès aux InputStream et OutputStream de l'API NIO qui sont des streams non bufferisées sur le contenu des fichiers. Ces deux classes peuvent néanmoins être utilisées pour créer des reader/writer de bytes bufferisés.

Dans l'exemple ci-dessous, la méthode newOutputStream(Path path) est utilisée pour créer un BufferedOutputStream.






 

 





@Test
@Order(1)
public void writeBufferedBytes() {
  final var pathToFile = someDir.resolve("file.bin");
  final var content = "Hello World !".getBytes();
  try (final var outputStream = Files.newOutputStream(pathToFile);
      final var bufferedOutputStream = new BufferedOutputStream(outputStream)) {
    bufferedOutputStream.write(content);
  } catch (IOException e) {
    fail(String.format("Unable to write to %s", pathToFile), e);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

De la même manière, la méthode newInputStream(Path path) peut être utilisée pour créer un BufferedInputStream.





 

 






@Test
@Order(2)
public void readBufferedBytes() {
  final var pathToFile = someDir.resolve("file.bin");
  try (final var inputStream = Files.newInputStream(pathToFile);
      final var bufferedInputStream = new BufferedInputStream(inputStream)) {
    final var content = bufferedInputStream.readAllBytes();
    assertThat(content).isEqualTo("Hello World !".getBytes());
  } catch (IOException e) {
    fail(String.format("Unable to read %s", pathToFile), e);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
Dernière mise à jour: 4/6/2022, 8:36:36 PM