Skip to content

33. APIs des fichiers et des chemins

Table des matières


Cette section se concentre sur la création de localisateurs de système de fichiers en utilisant l’API legacy java.io.File et l’API moderne java.nio.file.Path : comment convertir entre eux et comprendre les surcharges, les valeurs par défaut et les pièges courants.

33.1 File legacy et Path NIO : création et conversion

33.1.1 Créer un File (Legacy)

Une instance File représente un pathname du système de fichiers (absolu ou relatif).

En créer une n’accède pas au système de fichiers et ne lance pas IOException.

Constructeurs principaux (les plus courants) :

  • new File(String pathname)
  • new File(String parent, String child)
  • new File(File parent, String child)
  • new File(URI uri) (typiquement file:...)
import java.io.File;
import java.net.URI;

File f1 = new File("data.txt"); // relatif
File f2 = new File("/tmp", "data.txt"); // parent + enfant
File f3 = new File(new File("/tmp"), "data.txt");

File f4 = new File(URI.create("file:///tmp/data.txt"));

Note

  • new File(...) n’ouvre jamais le fichier.
  • Existence/permissions sont vérifiées seulement lorsque vous appelez des méthodes comme exists(), length(), ou lorsque vous ouvrez un stream/channel.

33.1.2 Créer un Path (NIO v.2)

Un Path est aussi juste un locator.

Comme File, créer un Path n’accède pas au système de fichiers.

Factories principales :

  • Path.of(String first, String... more) (Java 11+)
  • Paths.get(String first, String... more) (style plus ancien ; toujours valide)
  • Path.of(URI uri) (ex. file:///...)
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;

Path p1 = Path.of("data.txt"); // relatif
Path p2 = Path.of("/tmp", "data.txt"); // parent + enfant

Path p3 = Paths.get("data.txt"); // style factory legacy
Path p4 = Path.of(URI.create("file:///tmp/data.txt"));

Note

  • Path.of(...) et Paths.get(...) sont équivalents pour le système de fichiers par défaut.
  • Préférez Path.of dans le code moderne.

33.1.3 Absolu vs relatif : ce que signifie “relatif”

File et Path peuvent être créés comme chemins relatifs.

Les chemins relatifs sont résolus par rapport au répertoire de travail du processus (typiquement System.getProperty("user.dir")).

import java.io.File;
import java.nio.file.Path;

File rf = new File("data.txt");
Path rp = Path.of("data.txt");

System.out.println(rf.isAbsolute()); // false
System.out.println(rp.isAbsolute()); // false

System.out.println(rf.getAbsolutePath());
System.out.println(rp.toAbsolutePath());

Note

Les chemins relatifs sont une source courante de bugs “works on my machine” parce que user.dir dépend de la manière/où la JVM a été lancée.

33.1.4 Joindre / construire des paths

  • Le File legacy utilise des constructeurs (parent + enfant).
  • NIO utilise resolve et des méthodes associées.
Tâche Legacy (File) NIO (Path)
Joindre parent + enfant new File(parent, child) parent.resolve(child)
Joindre plusieurs segments Constructeurs répétés Path.of(a, b, c) ou resolve() chaîné
import java.io.File;
import java.nio.file.Path;

File f = new File("/tmp", "a.txt");

Path base = Path.of("/tmp");
Path p = base.resolve("a.txt"); // /tmp/a.txt
Path p2 = base.resolve("dir").resolve("a.txt"); // /tmp/dir/a.txt

33.1.4.1 resolve()

Combine des paths d’une manière filesystem-aware.

  • Les paths relatifs sont ajoutés
  • Un argument absolu remplace le path de base

Note

Path.resolve(...) a une règle : si l’argument est absolu, il retourne l’argument et ignore la base (vous ne pouvez pas combiner deux paths absolus en utilisant resolve).

33.1.4.2 relativize()

Path.relativize calcule un path relatif d’un path à un autre. Le path résultant, lorsqu’il est resolved contre le path source, donne le path cible.

Autrement dit :

  • Il répond à la question : “Comment aller du path A au path B ?”
  • Le résultat est toujours un path relatif
  • Aucun accès au système de fichiers n’a lieu

Règles fondamentales

relativize a des préconditions strictes. Les violer lance une exception.

Règle Explication
Les deux paths doivent être absolus ou tous les deux relatifs
Les deux paths doivent appartenir au même système de fichiers même provider
Les composants de root doivent correspondre même root (sur Windows, même drive)
Le résultat n’est jamais absolu toujours relatif

Note

Si un path est absolu et l’autre relatif, IllegalArgumentException est lancée.

Exemple relatif simple :

Les deux paths sont relatifs, donc la relativisation est autorisée.

Path p1 = Path.of("docs/manual");
Path p2 = Path.of("docs/images/logo.png");

Path relative = p1.relativize(p2);
System.out.println(relative);
../images/logo.png

Interprétation : depuis docs/manual, monter d’un niveau, puis entrer dans images/logo.png.

Exemple de paths absolus :

Les paths absolus fonctionnent exactement de la même manière.

Path base = Path.of("/home/user/projects");
Path target = Path.of("/home/user/docs/readme.txt");

Path relative = base.relativize(target);
System.out.println(relative);
../docs/readme.txt

Utiliser resolve pour vérifier le résultat

Une propriété clé de relativize est cette identité :

base.resolve(base.relativize(target)).equals(target)
Path base = Path.of("/a/b/c");
Path target = Path.of("/a/d/e");

Path r = base.relativize(target);
System.out.println(r); // ../../d/e
System.out.println(base.resolve(r)); // /a/d/e

Exemple : mélanger des paths absolus et relatifs (CAS ERREUR)

C’est l’une des erreurs les plus courantes.

Path abs = Path.of("/a/b");
Path rel = Path.of("c/d");

abs.relativize(rel); // lance une exception
Exception in thread "main" java.lang.IllegalArgumentException

Note

relativize NE tente PAS de convertir automatiquement des paths en absolus.

Exemple : roots différentes (piège spécifique Windows)

Sur Windows, des paths avec des lettres de drive différentes ne peuvent pas être relativisés.

Path p1 = Path.of("C:\\data\\a");
Path p2 = Path.of("D:\\data\\b");

p1.relativize(p2); // IllegalArgumentException

Note

Sur les systèmes Unix-like, la root est toujours /, donc ce problème ne se produit pas.

33.1.5 Convertir entre File et Path

La conversion est directe et sans perte pour les paths normaux du système de fichiers local.

Conversion Comment
File → Path file.toPath()
Path → File path.toFile()
import java.io.File;
import java.nio.file.Path;

File f = new File("data.txt");
Path p = f.toPath();

File back = p.toFile();

Note

La conversion ne valide pas l’existence. Elle convertit seulement des représentations.

33.1.6 Conversion URI (quand nécessaire)

Les URI sont utiles lorsque les paths doivent être représentés dans une forme standard et absolue (par ex. interopérer avec des ressources réseau ou de la configuration).

Les deux APIs supportent la conversion URI.

Direction Legacy (File) NIO (Path)
Depuis URI new File(uri) Path.of(uri)
Vers URI file.toURI() path.toUri()
import java.io.File;
import java.net.URI;
import java.nio.file.Path;

File f = new File("/tmp/data.txt");
URI u1 = f.toURI();

Path p = Path.of("/tmp/data.txt");
URI u2 = p.toUri();

Path pFromUri = Path.of(u2);
File fFromUri = new File(u1);

Note

new File(URI) requiert un URI file: et lance IllegalArgumentException si l’URI n’est pas hiérarchique ou n’est pas un file URI.

33.1.7 Canonique vs absolu vs normalisé (différences fondamentales)

Ces termes sont souvent mélangés. Ils ne sont pas identiques.

Concept Legacy (File) NIO (Path) Touche le système de fichiers
Absolu getAbsoluteFile() toAbsolutePath() Non
Normalisé (pas de normalize pur, utiliser canonical)* normalize() normalize(): Non
Canonique / Réel getCanonicalFile() toRealPath() Oui

Note

File.getCanonicalFile() et Path.toRealPath() peuvent résoudre des symlinks et exigent que le path existe, donc ils peuvent lancer IOException.

File ne fournit pas de méthode pour une normalisation purement syntaxique : historiquement beaucoup de développeurs utilisaient getCanonicalFile(), mais ceci accède au système de fichiers et peut échouer.

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;

File f = new File("a/../data.txt");
System.out.println(f.getAbsolutePath()); // absolu, peut encore contenir ".."

try {
    System.out.println(f.getCanonicalPath()); // résout "..", peut toucher le système de fichiers
} catch (IOException e) {
    System.out.println("Canonical failed: " + e.getMessage());
}

Path p = Path.of("a/../data.txt");
System.out.println(p.toAbsolutePath()); // absolu, peut encore contenir ".."
System.out.println(p.normalize()); // purement syntaxique

try {
    System.out.println(p.toRealPath()); // résout symlinks, exige l’existence
} catch (IOException e) {
    System.out.println("RealPath failed: " + e.getMessage());
}

33.1.7.1 normalize()

Supprime des éléments de nom redondants comme . et ...

  • Purement syntaxique
  • Ne vérifie pas si le path existe

Note

normalize() est purement syntaxique, ne vérifie pas l’existence, et peut produire des paths invalides s’il est mal utilisé.

33.1.8 Tableau de comparaison rapide (création + conversion)

Besoin Legacy (File) NIO (Path) Préféré aujourd’hui
Créer depuis string new File("x") Path.of("x") Path
Parent + enfant new File(p, c) Path.of(p, c) ou resolve() Path
Convertir entre APIs toPath() toFile() Path-centric
Normaliser getCanonicalFile() (basé filesystem) normalize() (syntactique seulement) Path
Résoudre symlinks Canonical toRealPath() Path

33.2 Gérer les fichiers et répertoires : créer, copier, déplacer, remplacer, comparer, supprimer (Legacy vs NIO)

Cette section couvre les opérations que vous effectuez sur des entrées du système de fichiers (fichiers/répertoires) : créer, copier, déplacer/renommer, remplacer, comparer et supprimer.

Elle contraste java.io.File legacy (et des helpers legacy associés) avec java.nio.file moderne (NIO.2).

33.2.1 Modèle mental : “Path/Locator” vs “Opérations”

Les deux APIs utilisent des objets qui représentent un path, mais les opérations diffèrent :

  • Legacy : File est à la fois un wrapper de path et une API d’opérations (responsabilité mélangée)
  • NIO : Path est le path ; Files effectue les opérations (séparation des préoccupations)
Responsabilité Legacy NIO
Représentation du path File Path
Opérations sur le système de fichiers File Files
Reporting riche des erreurs Faible (booleans) Fort (exceptions)

Note

Les méthodes legacy retournent souvent boolean (échec silencieux), tandis que NIO lance IOException avec cause.

33.2.2 Créer des fichiers et des répertoires

La création est là où l’ancienne API est la plus maladroite et l’API NIO la plus expressive.

Tâche Approche legacy Approche NIO Notes
Créer fichier vide ouvrir+fermer un stream Files.createFile NIO échoue si existe
Créer un répertoire mkdir Files.createDirectory Le parent doit exister
Créer des répertoires récursivement mkdirs Files.createDirectories Crée les parents

33.2.2.1 Créer un fichier

Legacy n’a pas de méthode “créer fichier vide”, donc typiquement vous créez un fichier en ouvrant un output stream (side effect).

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

File f = new File("created-legacy.txt");
try (FileOutputStream out = new FileOutputStream(f)) {
    // le fichier est créé (ou tronqué) comme side effect
}

NIO fournit une méthode explicite de création.

import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;

Path p = Path.of("created-nio.txt");
Files.createFile(p);

Note

Files.createFile lance FileAlreadyExistsException si l’entrée existe.

33.2.2.2 Créer des répertoires

import java.io.File;

File dir1 = new File("a/b");
boolean ok1 = dir1.mkdir(); // échoue si le parent "a" n’existe pas
boolean ok2 = dir1.mkdirs(); // crée les parents
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;

Path d = Path.of("a/b");
Files.createDirectory(d); // le parent doit exister
Files.createDirectories(d); // crée les parents, ok si déjà existe

Note

Legacy mkdir()/mkdirs() retournent false en cas d’échec sans dire pourquoi. NIO lance IOException.

33.2.3 Copier des fichiers et des répertoires

La copie legacy est généralement une copie manuelle par stream (ou des libs externes). NIO a une opération unique et explicite.

Capacité Legacy NIO
Copier contenu de fichier Streams manuels Files.copy
Copier dans une cible existante Manuel Option REPLACE_EXISTING
Copier arbre de répertoires Récursion manuelle Récursion manuelle (mais meilleurs outils : Files.walk + Files.copy)

33.2.3.1 Copier un fichier (NIO)

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.io.IOException;

Path src = Path.of("src.txt");
Path dst = Path.of("dst.txt");

Files.copy(src, dst); // échoue si dst existe
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);

Note

Files.copy lance FileAlreadyExistsException si la cible existe et que vous n’avez pas utilisé REPLACE_EXISTING.

33.2.3.2 Copie manuelle (Legacy, basée sur stream)

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

try (FileInputStream in = new FileInputStream("src.bin");
FileOutputStream out = new FileOutputStream("dst.bin")) {

    byte[] buf = new byte[8192];
    int n;
    while ((n = in.read(buf)) != -1) {
        out.write(buf, 0, n);
    }
}

Note

Rappelez-vous read(byte[]) retourne le nombre de bytes lus ; vous devez écrire seulement ce compte, pas le buffer entier.

33.2.4 Déplacer / renommer et remplacer

Dans les deux APIs, rename/move est “au niveau metadata” quand possible, mais peut se comporter comme copy+delete entre systèmes de fichiers. NIO rend cela explicite via des options.

Opération Legacy NIO
Renommer/déplacer File.renameTo Files.move
Remplacer existant Peu fiable REPLACE_EXISTING
Déplacement atomique Non supporté ATOMIC_MOVE (si supporté)

33.2.4.1 Renommage legacy (piège commun)

import java.io.File;

File from = new File("old.txt");
File to = new File("new.txt");

boolean ok = from.renameTo(to); // peut échouer silencieusement
System.out.println(ok);

Note

  • renameTo est notoirement platform-dependent et retourne seulement boolean.
  • Il peut échouer parce que la cible existe, le fichier est ouvert, permissions, ou déplacement cross-filesystem.

33.2.4.2 NIO Move (préféré)

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.io.IOException;

Path from = Path.of("old.txt");
Path to = Path.of("new.txt");

Files.move(from, to); // échoue si la cible existe
Files.move(from, to, StandardCopyOption.REPLACE_EXISTING);

Note

Files.move lance FileAlreadyExistsException quand la cible existe et que REPLACE_EXISTING n’est pas spécifié.

33.2.5 Comparer des paths et des fichiers

Comparer des locators peut signifier : égalité de string/path, égalité normalisée/canonique, ou “même fichier sur disque”.

Les APIs diffèrent significativement ici.

Objectif de comparaison Legacy NIO
Même texte de path File.equals Path.equals
Normaliser le path getCanonicalFile normalize
Même fichier/ressource sur disque faible (heuristique canonique) Files.isSameFile

33.2.5.1 Égalité vs même fichier

Deux strings de path différentes peuvent référer au même fichier.

import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;

Path p1 = Path.of("a/../data.txt");
Path p2 = Path.of("data.txt");

System.out.println(p1.equals(p2)); // false (texte de path différent)
System.out.println(p1.normalize().equals(p2.normalize())); // peut encore être false si relatif

try {
    System.out.println(Files.isSameFile(p1, p2)); // peut être true, peut lancer si non accessible
} catch (IOException e) {
    System.out.println("isSameFile failed: " + e.getMessage());
}

Note

Files.isSameFile peut accéder au système de fichiers et peut lancer IOException (problèmes de permissions, fichiers manquants, etc.).

33.2.6 Supprimer des fichiers et des répertoires

La suppression est simple conceptuellement mais a des edge cases importants : répertoires non vides, cibles manquantes, et différences de reporting d’erreurs.

Tâche Legacy NIO Comportement si manquant
Supprimer fichier/dir File.delete Files.delete Legacy false, NIO exception
Supprimer si existe Pas direct (check+delete) Files.deleteIfExists retourne boolean
Supprimer dir non vide Récursion manuelle Récursion manuelle (walk) Les deux exigent récursion

33.2.6.1 Delete legacy

import java.io.File;

File f = new File("x.txt");
boolean ok = f.delete(); // false si non supprimé
System.out.println(ok);

Note

Legacy delete() échoue (retourne false) pour un répertoire non vide et souvent ne fournit aucune raison.

33.2.6.2 NIO Delete et Delete-If-Exists

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.NoSuchFileException;
import java.nio.file.DirectoryNotEmptyException;
import java.io.IOException;

Path p = Path.of("x.txt");

try {
    Files.delete(p);
} catch (NoSuchFileException e) {
    System.out.println("Missing: " + e.getFile());
} catch (DirectoryNotEmptyException e) {
    System.out.println("Directory not empty: " + e.getFile());
} catch (IOException e) {
    System.out.println("Delete failed: " + e.getMessage());
}

boolean deleted = Files.deleteIfExists(p);
System.out.println(deleted);

Note

Certification tip: Files.delete lance NoSuchFileException si manquant, tandis que deleteIfExists retourne false.

33.2.7 Copier / supprimer récursivement des arbres de répertoires (pattern NIO)

NIO ne fournit pas une seule méthode “copyTree/deleteTree”, mais l’approche standard utilise Files.walk ou Files.walkFileTree.

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

Path root = Path.of("dirToDelete");

Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Files.delete(file);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        if (exc != null) throw exc;
        Files.delete(dir);
        return FileVisitResult.CONTINUE;
    }
});

Note

Supprimer un arbre de répertoires exige de supprimer d’abord les fichiers, puis les répertoires (post-order). C’est une question de raisonnement courante.

33.2.8 Checklist de résumé

  • Préférer Files.createFile/createDirectory/createDirectories aux workarounds legacy
  • File.renameTo est peu fiable ; préférer Files.move avec options
  • Files.copy/move lancent FileAlreadyExistsException à moins que REPLACE_EXISTING soit utilisé
  • Files.delete lance ; Files.deleteIfExists retourne boolean
  • Files.isSameFile peut lancer IOException et peut toucher le système de fichiers
  • La suppression de répertoires non vides exige une récursion (les deux APIs)