Présentation de Java NIO
Introduction à Java NIO et NIO.2 — canaux, tampons, sélecteurs et le package java.nio.file.
Les quinze chapitres précédents portaient sur java.io — flux, Reader/Writer, File, Serializable. Cette API est le système d'E/S d'origine de Java, et elle est encore très utilisée. NIO est la famille d'API que Java a ajoutée par la suite pour couvrir ce que java.io ne pouvait pas faire. Elle se divise en deux parties qui partagent un préfixe de package et peu d'autre chose :
- NIO (Java 1.4, 2002) —
java.nio.*— canaux, tampons, sélecteurs. Une forme différente pour les E/S : basée sur les tampons d'octets, éventuellement non bloquante, conçue pour les serveurs à haut débit. - NIO.2 (Java 7, 2011) —
java.nio.file.*— les classesPath,Files,FileSystemetWatchService. Un remplaçant plus convivial pourjava.io.Fileet un endroit pour les fonctionnalités du système de fichiers quejava.ion'a jamais eues (liens symboliques, attributs étendus, E/S de fichiers asynchrones, surveillance de répertoires).
Vous utilisez des éléments de NIO.2 depuis le début de cette partie : Path, Files.newBufferedReader, Files.newInputStream font tous partie de java.nio.file. Ce chapitre prend du recul et montre où s'inscrivent ces éléments, et à quoi sert le reste du package.
Flux vs canal : deux formes différentes
InputStream.read() retourne un octet. OutputStream.write(int) écrit un octet. Le modèle mental est un tube traitement-octet-par-octet. Les décorateurs avec tampon le rendent rapide, mais l'abstraction est séquentielle et unidirectionnelle.
Un canal (java.nio.channels.Channel) est bidirectionnel, orienté tampon d'octets, et prend en charge des opérations qu'InputStream ne peut pas exprimer :
- Lire dans un
ByteBufferet écrire depuis unByteBuffer— pas unbyte[]. - Mapper en mémoire une région de fichier dans la RAM et la lire/écrire comme un tampon.
- Diffuser une lecture dans plusieurs tampons (en-tête → l'un, charge utile → l'autre).
- Regrouper une écriture depuis plusieurs tampons (un seul
write()produit une sortie contiguë). - Marquer un canal comme non bloquant et laisser un
Selectormultiplexer des milliers d'entre eux sur un seul thread.
La contrepartie est la verbosité. Le code canal lit et écrit via un ByteBuffer avec des appels explicites à flip() et position() ; java.io cache tout cela derrière read(byte[]). Pour la lecture de fichiers classique, préférez les API java.io/Files. Descendez au niveau des canaux uniquement lorsque vous avez besoin d'une des fonctionnalités exclusives aux canaux.
// channel-shaped read into a 1 KB buffer
try (FileChannel ch = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(1024);
int n = ch.read(buf); // fills the buffer; updates position
buf.flip(); // switch to "read what was just written"
while (buf.hasRemaining()) {
process(buf.get());
}
}L'étape flip() est le moment où les gens découvrent que ByteBuffer possède sa propre petite machine à états.
ByteBuffer : position, limite, capacité
Un ByteBuffer est un byte[] de taille fixe (ou un bloc de mémoire hors tas) plus trois indices :
position— le prochain octet à lire ou écrire.limit— l'index après le dernier octet que vous êtes autorisé à toucher.capacity— la taille fixe du tampon ; ne peut pas changer.
0 ─────── position ─────── limit ─────── capacity
(consumed) (active region) (untouchable / empty)Le tampon est par convention dans l'un de deux modes :
- Mode écriture (par défaut) : vous y faites des
put(byte).positionavance ;limit == capacity. - Mode lecture : vous en retirez des
get().positionavance ;limitest là où vous avez arrêté d'écrire.
flip() passe du mode écriture au mode lecture : il définit limit = position (marque où se terminent les données) et réinitialise position = 0 (commence à lire depuis le début). clear() revient en mode écriture (position = 0, limit = capacity). Les erreurs ici sont la source la plus courante de frustration du type « je lis zéro octet ; pourquoi ? ».
Les tampons hors tas (ByteBuffer.allocateDirect(n)) contournent le tas de la JVM et permettent au système d'exploitation de les lire/écrire directement sans copie supplémentaire. Ils sont plus lents à allouer, plus rapides pour les E/S, et le bon choix uniquement pour le code d'E/S sur le chemin critique.
Sélecteurs : un thread, de nombreux canaux
Avant les threads virtuels (Java 21), gérer des milliers de connexions réseau simultanées en Java signifiait soit des milliers de threads du système d'exploitation (un par connexion — coûteux) soit un seul thread multiplexant avec un Selector :
Selector sel = Selector.open();
serverChannel.register(sel, SelectionKey.OP_ACCEPT);
while (true) {
sel.select(); // blocks until any channel is ready
for (SelectionKey k : sel.selectedKeys()) {
if (k.isAcceptable()) accept(k);
if (k.isReadable()) read(k);
}
}Le système d'exploitation notifie la JVM quand un canal enregistré peut progresser ; la JVM vous remet l'ensemble prêt ; vous effectuez une lecture ou écriture non bloquante et revenez à select(). Le code de framework sous Netty, gRPC et Spring WebFlux a cette forme.
Avec les threads virtuels (Thread.startVirtualThread(...)), le modèle plus simple « un thread par requête » s'adapte aux mêmes nombres de connexions sans la chorégraphie du Selector — les threads virtuels se bloquent sur les E/S bloquantes essentiellement sans coût. Pour le nouveau code d'application sur Java 21+, la boucle de sélecteur est de plus en plus une préoccupation de bibliothèque ; vous ne l'écrivez généralement pas à la main. Pour le code de bibliothèque et les JVM pré-Loom, c'est le modèle standard.
java.nio.file : l'API de fichiers moderne
C'est la moitié de NIO que vous utiliserez dans le code quotidien. Elle remplace java.io.File et la plupart des parties relatives aux fichiers de java.io :
java.io | java.nio.file | Pourquoi le remplacement |
|---|---|---|
File | Path | Immuable, indépendant du système d'exploitation, pas de méthodes d'E/S intégrées |
File.list() | Files.list(Path), Files.walk(Path) | Stream<Path> ; fermable ; respecte les liens symboliques |
new FileInputStream(...) | Files.newInputStream(path) | Variantes avec prise en charge des jeux de caractères pour le texte ; une API d'ouverture cohérente |
file.delete() retournant false en cas d'échec | Files.delete(path) levant IOException | Les échecs sont visibles, pas silencieux |
| pas d'équivalent | Files.walkFileTree, WatchService, API des liens symboliques, vues des attributs de fichiers | Fonctionnalités que java.io n'a jamais eues |
Les deux prochains chapitres couvrent Path et Files en profondeur. La règle générale : pour le travail sur les fichiers en Java 2024+, utilisez java.nio.file. java.io.File est encore présent parce que l'ancien code l'utilise, mais le nouveau code devrait par défaut utiliser Path.
Un exemple concret : aller-retour d'un fichier via un canal et un tampon
Le programme ci-dessous copie un petit fichier texte de la manière canal-et-tampon pour rendre position/limit/flip concrets. Il ouvre la source comme un FileChannel, lit dans un ByteBuffer, fait le flip, écrit vers un FileChannel de destination, et affiche l'état du tampon à chaque étape pour que vous puissiez voir comment les indices se déplacent.
Ce qu'il faut retenir de l'exécution :
- La boucle a affiché l'état du tampon à chaque étape. Après un
read(),positioncorrespondait au nombre d'octets lus etlimitétait toujourscapacity— c'est le « mode écriture » : de la place encore à la fin. Aprèsflip(),position = 0etlimit = le-nombre-venant-d'être-lu— c'est le « mode lecture » : les octets se trouvent entre 0 etlimit. Les deux indices encodent « où vivent les données » sans les copier. - Le tampon faisait 16 octets ; le fichier en faisait 44. La boucle a effectué trois itérations : 16, 16, 12. Une fois le tampon vide (après que
writel'a drainé),clear()l'a réinitialisé en « mode écriture » pour que le prochainread()puisse le remplir à nouveau. Voici le modèle canal en miniature : remplir, flip, drainer, effacer, répéter. transferToa effectué la même copie en une ligne sans aucunByteBufferimpliqué. Sous Linux, cela correspond à un seul appel systèmesendfile()— les octets voyagent de noyau à noyau sans traverser la JVM. Quand vous déplacez des données entre deux canaux et n'avez pas besoin de les examiner, c'est le bon outil.- Notez que le fichier source a été créé avec
Files.writeStringet la destination relue avecFiles.readString— deux fonctions en une ligne dejava.nio.filequi cachent entièrement canaux et tampons. La boucle détaillée avec canal au milieu est ce que vous écririez uniquement lorsque vous avez besoin d'un accès direct au tampon (analyse binaire personnalisée, mappage mémoire, scatter/gather). Pour « copier un fichier »,transferToouFiles.copyest plus court et au moins aussi rapide. - Le constructeur
FileChannel.open(path, OPTION)est le parallèle deFiles.newInputStream(path). L'énumérationStandardOpenOption(READ, WRITE, CREATE, APPEND, TRUNCATE_EXISTING, ...) contrôle le comportement à l'ouverture — il n'y a qu'un seul endroit où chercher. Cette énumération d'options d'ouverture revient dans le prochain chapitre.
Et ensuite
Ce chapitre a nommé les éléments — canaux, tampons, sélecteurs, java.nio.file. Le prochain chapitre, Classe Java Path, approfondit le plus convivial de ces éléments — Path — et les méthodes (resolve, relativize, normalize) que vous utiliserez chaque fois que vous manipulez un chemin de système de fichiers.