# 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:
- la manipulation des fichiers et des répertoires,
- le listage du contenu d'un répertoire,
- la notification des changements dans un répertoire et
- la manipulation de
FileSystem
(opens new window).
# 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);
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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!");
}
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);
}
}
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);
}
}
2
3
4
5
6
7
8
9
10
11
12