W3docs

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 classes Path, Files, FileSystem et WatchService. Un remplaçant plus convivial pour java.io.File et un endroit pour les fonctionnalités du système de fichiers que java.io n'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 ByteBuffer et écrire depuis un ByteBuffer — pas un byte[].
  • 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 Selector multiplexer 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). position avance ; limit == capacity.
  • Mode lecture : vous en retirez des get(). position avance ; limit est 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.iojava.nio.filePourquoi le remplacement
FilePathImmuable, 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'échecFiles.delete(path) levant IOExceptionLes échecs sont visibles, pas silencieux
pas d'équivalentFiles.walkFileTree, WatchService, API des liens symboliques, vues des attributs de fichiersFonctionnalité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.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • La boucle a affiché l'état du tampon à chaque étape. Après un read(), position correspondait au nombre d'octets lus et limit était toujours capacity — c'est le « mode écriture » : de la place encore à la fin. Après flip(), position = 0 et limit = le-nombre-venant-d'être-lu — c'est le « mode lecture » : les octets se trouvent entre 0 et limit. 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 write l'a drainé), clear() l'a réinitialisé en « mode écriture » pour que le prochain read() puisse le remplir à nouveau. Voici le modèle canal en miniature : remplir, flip, drainer, effacer, répéter.
  • transferTo a effectué la même copie en une ligne sans aucun ByteBuffer impliqué. Sous Linux, cela correspond à un seul appel système sendfile() — 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.writeString et la destination relue avec Files.readString — deux fonctions en une ligne de java.nio.file qui 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 », transferTo ou Files.copy est plus court et au moins aussi rapide.
  • Le constructeur FileChannel.open(path, OPTION) est le parallèle de Files.newInputStream(path). L'énumération StandardOpenOption (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.

Pratique

Pratique
Vous lisez 10 octets depuis un canal dans un `ByteBuffer` de capacité 1024. Vous souhaitez écrire ces 10 octets dans un autre canal. Que devez-vous faire entre le `read()` et le `write()` ?
Vous lisez 10 octets depuis un canal dans un `ByteBuffer` de capacité 1024. Vous souhaitez écrire ces 10 octets dans un autre canal. Que devez-vous faire entre le `read()` et le `write()` ?
Was this page helpful?