Maintenant que nous savons comment créer des objets Fichier et Path, nous pouvons commencer à les utiliser pour faire des choses utiles. Dans cette section, nous explorons les fonctionnalités qui impliquent les répertoires.
Utilisation des Fonctionnalités Partagées
De nombreuses opérations peuvent être effectuées à l’aide des bibliothèques I/O et NIO.2. Nous présentons de nombreuses API communes dans les Tableaux 14.3 et 14.4. Bien que ces tableaux puissent sembler contenir beaucoup de méthodes à apprendre, beaucoup d’entre elles sont explicites. Vous pouvez ignorer les paramètres vararg pour l’instant. Nous les expliquons plus tard dans le chapitre.
Description | Méthode d’instance I/O file | Méthode d’instance NIO.2 path |
---|---|---|
Obtient le nom du fichier/répertoire | getName() | getFileName() |
Récupère le répertoire parent ou null s’il n’y en a pas | getParent() | getParent() |
Vérifie si le fichier/répertoire est un chemin absolu | isAbsolute() | isAbsolute() |
Description | Méthode d’instance I/O file | Méthode statique NIO.2 files |
---|---|---|
Supprime le fichier/répertoire | delete() | deleteIfExists(Path p) throws IOException |
Vérifie si le fichier/répertoire existe | exists() | exists(Path p, LinkOption… o) |
Récupère le chemin absolu du fichier/répertoire | getAbsolutePath() | toAbsolutePath() |
Vérifie si la ressource est un répertoire | isDirectory() | isDirectory(Path p, LinkOption… o) |
Vérifie si la ressource est un fichier | isFile() | isRegularFile(Path p, LinkOption… o) |
Renvoie l’heure de la dernière modification du fichier | lastModified() | getLastModifiedTime(Path p, LinkOption… o) throws IOException |
Récupère le nombre d’octets dans le fichier | length() | size(Path p) throws IOException |
Liste le contenu du répertoire | listFiles() | list(Path p) throws IOException |
Crée un répertoire | mkdir() | createDirectory(Path p, FileAttribute… a) throws IOException |
Crée un répertoire incluant tous les répertoires parents non existants | mkdirs() | createDirectories(Path p, FileAttribute… a) throws IOException |
Renomme le fichier/répertoire désigné | renameTo(File dest) | move(Path src, Path dest, CopyOption… o) throws IOException |
Essayons maintenant d’utiliser certaines de ces API. Voici un exemple de programme utilisant uniquement les API I/O héritées. Étant donné un chemin de fichier, il affiche des informations sur le fichier ou le répertoire, comme s’il existe, quels fichiers sont contenus dedans, etc. :
public static void io() {
var fichier = new File("C:\\donnees\\zoo.txt");
if (fichier.exists()) {
System.out.println("Chemin Absolu: " + fichier.getAbsolutePath());
System.out.println("Est un Répertoire: " + fichier.isDirectory());
System.out.println("Chemin Parent: " + fichier.getParent());
if (fichier.isFile()) {
System.out.println("Taille: " + fichier.length());
System.out.println("Dernière Modification: " + fichier.lastModified());
} else {
for (File sousFichier : fichier.listFiles()) {
System.out.println(" " + sousFichier.getName());
}
}
}
}
Si le chemin fourni pointe vers un fichier valide, le programme affiche quelque chose de similaire à ce qui suit en raison de l’instruction if :
Chemin Absolu: C:\donnees\zoo.txt Est un Répertoire: false Chemin Parent: C:\donnees Taille: 12382 Dernière Modification: 1650610000000
Enfin, si le chemin fourni pointe vers un répertoire valide, comme C:\donnees, le programme affiche quelque chose de similaire à ce qui suit, grâce au bloc else :
Chemin Absolu: C:\donnees Est un Répertoire: true Chemin Parent: C:\ employes.txt zoo.txt zoo-backup.txt
Dans ces exemples, vous voyez que la sortie d’un programme basé sur I/O dépend entièrement des répertoires et fichiers disponibles au moment de l’exécution dans le système de fichiers sous-jacent.
Dans l’exemple précédent, nous avons utilisé deux barres obliques inverses (\\) dans le chemin String, comme C:\\donnees\\zoo.txt. Lorsque le compilateur voit un \\ à l’intérieur d’une expression String, il l’interprète comme une seule valeur \.
Maintenant, écrivons ce même programme en utilisant uniquement NIO.2 et voyons en quoi il diffère :
public static void nio() throws IOException {
var chemin = Path.of("C:\\donnees\\zoo.txt");
if (Files.exists(chemin)) {
System.out.println("Chemin Absolu: " + chemin.toAbsolutePath());
System.out.println("Est un Répertoire: " + Files.isDirectory(chemin));
System.out.println("Chemin Parent: " + chemin.getParent());
if (Files.isRegularFile(chemin)) {
System.out.println("Taille: " + Files.size(chemin));
System.out.println("Dernière Modification: "
+ Files.getLastModifiedTime(chemin));
} else {
try (Stream flux = Files.list(chemin)) {
flux.forEach(p ->
System.out.println(" " + p.getFileName()));
}
}
}
}
La plupart de cet exemple est équivalent et remplace les appels de méthode I/O des tableaux précédents par les versions NIO.2. Cependant, il y a des différences clés. Premièrement, la ligne 25 déclare une exception vérifiée. Plus d’API dans NIO.2 lancent IOException que les API I/O ne le faisaient. Dans ce cas, Files.size(), Files.getLastModifiedTime(), et Files.list() lancent une IOException.
Deuxièmement, les lignes 36-39 utilisent un Stream et une lambda au lieu d’une boucle. Étant donné que les flux utilisent une évaluation paresseuse, cela signifie que la méthode chargera chaque élément de chemin au besoin, plutôt que le répertoire entier d’un coup.
Fermeture du Stream
Avez-vous remarqué que dans le dernier exemple de code, nous avons mis notre objet Stream dans un try-with-resources ? Les méthodes basées sur les flux de NIO.2 ouvrent une connexion au système de fichiers qui doit être correctement fermée ; sinon, une fuite de ressources pourrait survenir. Une fuite de ressources dans le système de fichiers signifie que le chemin peut être verrouillé contre toute modification bien après que le processus qui l’a utilisé soit terminé.
Si vous supposiez qu’une opération terminale d’un flux fermerait automatiquement les ressources de fichier sous-jacentes, vous auriez tort. Il y a eu beaucoup de débat sur ce comportement lorsqu’il a été présenté pour la première fois ; en bref, l’obligation pour les développeurs de fermer le flux l’a emporté.
Du bon côté, tous les flux n’ont pas besoin d’être fermés : seulement ceux qui ouvrent des ressources comme ceux trouvés dans NIO.2. Par exemple, vous n’aviez pas besoin de fermer les flux avec lesquels vous avez travaillé dans le Chapitre 10, “Streams”.
Utilisez toujours des instructions try-with-resources avec ces méthodes NIO.2 dans votre propre code.
Pour le reste de cette section, nous ne discutons que des méthodes NIO.2, car elles sont plus importantes. Il y a aussi plus à savoir à leur sujet, et elles sont plus susceptibles d’être utilisées dans le monde réel.
Gestion des Méthodes Qui Déclarent IOException
Beaucoup des méthodes présentées dans ce chapitre déclarent IOException. Les causes courantes d’une méthode lançant cette exception incluent les suivantes :
- Perte de communication avec le système de fichiers sous-jacent.
- Le fichier ou le répertoire existe mais ne peut pas être accédé ou modifié.
- Le fichier existe mais ne peut pas être écrasé.
- Le fichier ou le répertoire est requis mais n’existe pas.
Les méthodes qui accèdent ou modifient des fichiers et des répertoires, comme celles de la classe Files, déclarent souvent IOException. Il y a des exceptions à cette règle, comme nous le verrons. Par exemple, la méthode Files.exists() ne déclare pas IOException. Si elle lançait une exception lorsque le fichier n’existe pas, elle ne serait jamais capable de renvoyer false ! En règle générale, si une méthode NIO.2 déclare une IOException, elle nécessite généralement que les chemins sur lesquels elle opère existent.
Fournir des Paramètres Optionnels NIO.2
Beaucoup des méthodes NIO.2 dans ce chapitre incluent un varargs qui prend une liste optionnelle de valeurs. Le Tableau 14.5 présente les arguments que vous devriez connaître.
Type d’énumération | Interface héritée | Valeur d’énumération | Détails |
---|---|---|---|
LinkOption | CopyOption OpenOption | NOFOLLOW_LINKS | Ne pas suivre les liens symboliques. |
StandardCopyOption | CopyOption | ATOMIC_MOVE | Déplacer le fichier comme une opération atomique du système de fichiers. |
COPY_ATTRIBUTES | Copier les attributs existants vers le nouveau fichier. | ||
REPLACE_EXISTING | Écraser le fichier s’il existe déjà. | ||
StandardOpenOption | OpenOption | APPEND | Si le fichier est déjà ouvert pour écriture, ajouter à la fin. |
CREATE | Créer un nouveau fichier s’il n’existe pas. | ||
CREATE_NEW | Créer un nouveau fichier uniquement s’il n’existe pas ; échouer sinon. | ||
READ | Ouvrir pour l’accès en lecture. | ||
TRUNCATE_EXISTING | Si le fichier est déjà ouvert pour écriture, effacer le fichier et ajouter au début. | ||
WRITE | Ouvrir pour l’accès en écriture. | ||
FileVisitOption | N/A | FOLLOW_LINKS | Suivre les liens symboliques. |
Avec les exceptions de Files.copy() et Files.move(), nous ne discuterons pas de ces paramètres varargs chaque fois que nous présenterons une méthode. Leur comportement devrait être assez simple cependant. Par exemple, pouvez-vous déterminer ce que fait l’appel suivant à Files.exists() avec le LinkOption dans l’extrait de code suivant ?
Path chemin = Paths.get("horaire.xml");
boolean existe = Files.exists(chemin, LinkOption.NOFOLLOW_LINKS);
La méthode Files.exists() vérifie simplement si un fichier existe. Mais si le paramètre est un lien symbolique, la méthode vérifie si la cible du lien symbolique existe, au lieu. Fournir LinkOption.NOFOLLOW_LINKS signifie que le comportement par défaut sera remplacé, et la méthode vérifiera si le lien symbolique lui-même existe.
Notez que certaines des énumérations dans le Tableau 14.5 héritent d’une interface. Cela signifie que certaines méthodes acceptent une variété de types d’énumération. Par exemple, la méthode Files.move() prend un vararg CopyOption, elle peut donc prendre des énumérations de différents types, et plus d’options peuvent être ajoutées au fil du temps.
void copier(Path source, Path cible) throws IOException {
Files.move(source, cible,
LinkOption.NOFOLLOW_LINKS,
StandardCopyOption.ATOMIC_MOVE);
}
Interagir avec les Chemins NIO.2
Tout comme les valeurs String, les instances Path sont immuables. Dans l’exemple suivant, l’opération Path sur la deuxième ligne est perdue car p est immuable :
Path p = Path.of("baleine");
p.resolve("krill");
System.out.println(p); // baleine
Beaucoup des méthodes disponibles dans l’interface Path transforment la valeur du chemin d’une manière ou d’une autre et renvoient un nouvel objet Path, permettant aux méthodes d’être chaînées. Nous démontrons le chaînage dans l’exemple suivant, dont nous discutons des détails dans cette section du chapitre :
Path.of("/zoo/../maison").getParent().normalize().toAbsolutePath();
Visualisation du Chemin
L’interface Path contient trois méthodes pour récupérer des informations de base sur la représentation du chemin. La méthode toString() renvoie une représentation String du chemin entier. En fait, c’est la seule méthode de l’interface Path qui renvoie une String. Beaucoup des autres méthodes de l’interface Path renvoient des instances de Path.
Les méthodes getNameCount() et getName() sont souvent utilisées ensemble pour récupérer le nombre d’éléments dans le chemin et une référence à chaque élément, respectivement. Ces deux méthodes n’incluent pas le répertoire racine comme partie du chemin.
Path chemin = Paths.get("/terre/hippo/harry.heureux");
System.out.println("Le Nom du Chemin est: " + chemin);
for(int i=0; i<chemin.getNameCount(); i++)
System.out.println(" Élément " + i + " est: " + chemin.getName(i));
Remarquez que nous n’avons pas appelé toString() explicitement sur la deuxième ligne. Souvenez-vous, Java appelle toString() sur n’importe quel Objet dans le cadre de la concaténation de chaînes. Nous utilisons cette fonctionnalité dans les exemples de ce chapitre.
Le code imprime ce qui suit :
Le Nom du Chemin est: /terre/hippo/harry.heureux Élément 0 est: terre Élément 1 est: hippo Élément 2 est: harry.heureux
Même si c’est un chemin absolu, l’élément racine n’est pas inclus dans la liste des noms. Comme nous l’avons dit, ces méthodes ne considèrent pas la racine comme faisant partie du chemin.
var p = Path.of("/");
System.out.print(p.getNameCount()); // 0
System.out.print(p.getName(0)); // IllegalArgumentException
Notez que si vous essayez d’appeler getName() avec un index invalide, une exception sera lancée à l’exécution.
Nos exemples impriment / comme caractère séparateur de fichier en raison du système que nous utilisons. Votre sortie réelle peut varier tout au long de ce chapitre.
Création d’une Partie du Chemin
L’interface Path inclut la méthode subpath() pour sélectionner des portions d’un chemin. Elle prend deux paramètres : un indice de début inclusif et un indice de fin exclusif. Cela devrait vous sembler familier car c’est ainsi que fonctionne la méthode substring() de String, comme vous l’avez vu au Chapitre 4, “API de base”.
L’extrait de code suivant montre comment fonctionne subpath(). Nous imprimons également les éléments du Path en utilisant getName() pour que vous puissiez voir comment les indices sont utilisés.
var p = Paths.get("/mammifere/omnivore/raton-laveur.image");
System.out.println("Chemin est: " + p);
for (int i = 0; i < p.getNameCount(); i++) {
System.out.println(" Élément " + i + " est: " + p.getName(i));
}
System.out.println();
System.out.println("subpath(0,3): " + p.subpath(0, 3));
System.out.println("subpath(1,2): " + p.subpath(1, 2));
System.out.println("subpath(1,3): " + p.subpath(1, 3));
La sortie de cet extrait de code est la suivante :
Chemin est: /mammifere/omnivore/raton-laveur.image Élément 0 est: mammifere Élément 1 est: omnivore Élément 2 est: raton-laveur.image subpath(0,3): mammifere/omnivore/raton-laveur.image subpath(1,2): omnivore subpath(1,3): omnivore/raton-laveur.image
Comme getNameCount() et getName(), subpath() est indexé à zéro et n’inclut pas la racine. Aussi comme getName(), subpath() lance une exception si des indices invalides sont fournis.
var q = p.subpath(0, 4); // IllegalArgumentException
var x = p.subpath(1, 1); // IllegalArgumentException
Le premier exemple lance une exception à l’exécution, car la valeur d’indice maximale autorisée est 3. Le deuxième exemple lance une exception car les indices de début et de fin sont les mêmes, ce qui conduit à une valeur de chemin vide.
Accès aux Éléments du Chemin
L’interface Path contient de nombreuses méthodes pour récupérer des éléments particuliers d’un Path, renvoyés sous forme d’objets Path eux-mêmes. La méthode getFileName() renvoie l’élément Path du fichier ou répertoire actuel, tandis que getParent() renvoie le chemin complet du répertoire contenant. La méthode getParent() renvoie null si elle est utilisée sur le chemin racine ou au sommet d’un chemin relatif. La méthode getRoot() renvoie l’élément racine du fichier dans le système de fichiers, ou null si le chemin est un chemin relatif.
Considérez la méthode suivante, qui imprime divers éléments de Path :
public void afficherInformationsChemin(Path chemin) {
System.out.println("Nom de fichier est: " + chemin.getFileName());
System.out.println(" Racine est: " + chemin.getRoot());
Path parentActuel = chemin;
while((parentActuel = parentActuel.getParent()) != null)
System.out.println(" Parent actuel est: " + parentActuel);
System.out.println();
}
La boucle while dans la méthode afficherInformationsChemin() continue jusqu’à ce que getParent() retourne null. Nous appliquons cette méthode aux trois chemins suivants :
afficherInformationsChemin(Path.of("zoo"));
afficherInformationsChemin(Path.of("/zoo/armadillo/coquilles.txt"));
afficherInformationsChemin(Path.of("./armadillo/../coquilles.txt"));
Cette application de l’exemple produit la sortie suivante :
Nom de fichier est: zoo Racine est: null Nom de fichier est: coquilles.txt Racine est: / Parent actuel est: /zoo/armadillo Parent actuel est: /zoo Parent actuel est: / Nom de fichier est: coquilles.txt Racine est: null Parent actuel est: ./armadillo/.. Parent actuel est: ./armadillo Parent actuel est: .
En examinant la sortie, vous pouvez voir la différence dans le comportement de getRoot() sur les chemins absolus et relatifs. Comme vous pouvez le voir dans le premier et le dernier exemples, la méthode getParent() ne traverse pas les chemins relatifs en dehors du répertoire de travail actuel.
Vous voyez également que ces méthodes ne résolvent pas les symboles de chemin et les traitent comme une partie distincte du chemin. Bien que la plupart des méthodes dans cette partie du chapitre traitent les symboles de chemin comme partie du chemin, nous en présentons une bientôt qui nettoie les symboles de chemin.
Résolution des Chemins
Supposons que vous vouliez concaténer des chemins d’une manière similaire à la façon dont nous concaténons des chaînes. La méthode resolve() fournit des versions surchargées qui vous permettent de passer soit un paramètre Path, soit un paramètre String. L’objet sur lequel la méthode resolve() est invoquée devient la base du nouvel objet Path, avec l’argument d’entrée étant ajouté au Path. Voyons ce qui se passe si nous appliquons resolve() à un chemin absolu et un chemin relatif :
Path chemin1 = Path.of("/chats/../panthère");
Path chemin2 = Path.of("nourriture");
System.out.println(chemin1.resolve(chemin2));
L’extrait de code génère la sortie suivante :
/chats/../panthère/nourriture
Comme les autres méthodes que nous avons vues, resolve() ne nettoie pas les symboles de chemin. Dans cet exemple, l’argument d’entrée de la méthode resolve() était un chemin relatif, mais qu’en serait-il s’il s’agissait d’un chemin absolu ?
Path chemin3 = Path.of("/dinde/nourriture");
System.out.println(chemin3.resolve("/tigre/cage"));
Puisque le paramètre d’entrée est un chemin absolu, la sortie serait la suivante :
/tigre/cage
Pour l’examen, vous devriez être conscient du mélange de chemins absolus et relatifs avec la méthode resolve(). Si un chemin absolu est fourni comme entrée de la méthode, c’est la valeur retournée. En gros, vous ne pouvez pas combiner deux chemins absolus en utilisant resolve().
Lors de l’examen, quand vous voyez resolve(), pensez à la concaténation.
Relativisation d’un Chemin
L’interface Path inclut une méthode relativize() pour construire le chemin relatif d’un Path à un autre, souvent en utilisant des symboles de chemin. Que pensez-vous que les exemples suivants imprimeront ?
var chemin1 = Path.of("poisson.txt");
var chemin2 = Path.of("amical/oiseaux.txt");
System.out.println(chemin1.relativize(chemin2));
System.out.println(chemin2.relativize(chemin1));
Les exemples impriment ce qui suit :
../amical/oiseaux.txt ../../poisson.txt
L’idée est la suivante : si vous pointez vers un chemin dans le système de fichiers, quelles étapes devriez-vous prendre pour atteindre l’autre chemin ? Par exemple, pour atteindre poisson.txt depuis amical/oiseaux.txt, vous devez monter de deux niveaux (le fichier lui-même compte pour un niveau) puis sélectionner poisson.txt.
Si les deux valeurs de chemin sont relatives, la méthode relativize() calcule les chemins comme s’ils sont dans le même répertoire de travail actuel. Alternativement, si les deux valeurs de chemin sont absolues, la méthode calcule le chemin relatif d’un emplacement absolu à un autre, indépendamment du répertoire de travail actuel. L’exemple suivant démontre cette propriété lorsqu’il est exécuté sur un ordinateur Windows :
Path chemin3 = Paths.get("E:\\habitat");
Path chemin4 = Paths.get("E:\\sanctuaire\\corbeau\\poe.txt");
System.out.println(chemin3.relativize(chemin4));
System.out.println(chemin4.relativize(chemin3));
Cet extrait de code produit la sortie suivante :
..\sanctuaire\corbeau\poe.txt ..\..\..\habitat
La méthode relativize() nécessite que les deux chemins soient absolus ou relatifs et lance une exception si les types sont mélangés.
Path chemin1 = Paths.get("/primate/chimpanze");
Path chemin2 = Paths.get("bananes.txt");
chemin1.relativize(chemin2); // IllegalArgumentException
Sur les systèmes basés sur Windows, elle exige également que si des chemins absolus sont utilisés, les deux chemins doivent avoir le même répertoire racine ou lettre de lecteur. Par exemple, ce qui suit lancerait également une IllegalArgumentException sur un système Windows :
Path chemin3 = Paths.get("C:\\primate\\chimpanze");
Path chemin4 = Paths.get("D:\\stockage\\bananes.txt");
chemin3.relativize(chemin4); // IllegalArgumentException
Normalisation d’un Chemin
Jusqu’à présent, nous avons présenté un certain nombre d’exemples qui incluaient des symboles de chemin inutiles. Heureusement, Java fournit la méthode normalize() pour éliminer les redondances inutiles dans un chemin.
Rappelez-vous, le symbole de chemin .. fait référence au répertoire parent, tandis que le symbole de chemin . fait référence au répertoire actuel. Nous pouvons appliquer normalize() à certains de nos chemins précédents.
var p1 = Path.of("./armadillo/../coquilles.txt");
System.out.println(p1.normalize()); // coquilles.txt
var p2 = Path.of("/chats/../panthere/nourriture");
System.out.println(p2.normalize()); // /panthere/nourriture
var p3 = Path.of("../../poisson.txt");
System.out.println(p3.normalize()); // ../../poisson.txt
Les deux premiers exemples appliquent les symboles de chemin pour éliminer les redondances, mais qu’en est-il du dernier ? C’est aussi simplifié que possible. La méthode normalize() n’élimine pas tous les symboles de chemin, seulement ceux qui peuvent être réduits.
La méthode normalize() nous permet également de comparer des chemins équivalents. Considérez l’exemple suivant :
var p1 = Paths.get("/poney/../meteo.txt");
var p2 = Paths.get("/meteo.txt");
System.out.println(p1.equals(p2)); // false
System.out.println(p1.normalize().equals(p2.normalize())); // true
La méthode equals() renvoie true si deux chemins représentent la même valeur. Dans la première comparaison, les valeurs de chemin sont différentes. Dans la seconde comparaison, les valeurs de chemin ont toutes deux été réduites à la même valeur normalisée, /meteo.txt. C’est la fonction principale de la méthode normalize() : nous permettre de mieux comparer différents chemins.
Récupération du Chemin Réel du Système de Fichiers
Bien que travailler avec des chemins théoriques soit utile, parfois vous voulez vérifier que le chemin existe dans le système de fichiers en utilisant toRealPath(). Cette méthode est similaire à normalize() en ce qu’elle élimine tous les symboles de chemin redondants. Elle est également similaire à toAbsolutePath(), en ce qu’elle joindra le chemin avec le répertoire de travail actuel si le chemin est relatif.
Contrairement à ces deux méthodes, cependant, toRealPath() lancera une exception si le chemin n’existe pas. De plus, elle suivra les liens symboliques, avec un paramètre vararg LinkOption optionnel pour les ignorer.
Disons que nous avons un système de fichiers dans lequel nous avons un lien symbolique de /zebre à /cheval. Que pensez-vous que ce qui suit imprimera, étant donné un répertoire de travail actuel de /cheval/horaire ?
System.out.println(Paths.get("/zebre/nourriture.txt").toRealPath());
System.out.println(Paths.get(".././nourriture.txt").toRealPath());
La sortie des deux lignes est la suivante :
/cheval/nourriture.txt
Dans cet exemple, les chemins absolus et relatifs se résolvent tous deux au même fichier absolu, car le lien symbolique pointe vers un fichier réel dans le système de fichiers. Nous pouvons également utiliser la méthode toRealPath() pour accéder au répertoire de travail actuel en tant qu’objet Path.
System.out.println(Paths.get(".").toRealPath());
Révision des API Path NIO.2
Nous avons couvert beaucoup de méthodes d’instance sur Path dans cette section. Le Tableau 14.6 les répertorie pour révision.
Description | Méthode ou constructeur |
---|---|
Chemin de fichier en tant que chaîne | public String toString() |
Segment unique | public Path getName(int index) |
Nombre de segments | public int getNameCount() |
Segments dans une plage | public Path subpath(int beginIndex, int endIndex) |
Segment final | public Path getFileName() |
Parent immédiat | public Path getParent() |
Segment de niveau supérieur | public Path getRoot() |
Concaténer des chemins | public Path resolve(String p) public Path resolve(Path p) |
Construire un chemin vers celui fourni | public Path relativize(Path p) |
Supprimer les parties redondantes du chemin | public Path normalize() |
Suivre les liens symboliques pour trouver le chemin dans le système de fichiers | public Path toRealPath() |
Création, Déplacement et Suppression de Fichiers et Répertoires
Puisque la création, le déplacement et la suppression ont quelques nuances, nous les détaillons dans cette section.
Création de Répertoires
Pour créer un répertoire, nous utilisons ces méthodes Files :
public static Path createDirectory(Path dir,
FileAttribute<?>... attrs) throws IOException
public static Path createDirectories(Path dir,
FileAttribute<?>... attrs) throws IOException
La méthode createDirectory() créera un répertoire et lancera une exception s’il existe déjà ou si les chemins menant au répertoire n’existent pas. La méthode createDirectories() crée le répertoire cible ainsi que tous les répertoires parents non existants menant au chemin. Si tous les répertoires existent déjà, createDirectories() se terminera simplement sans rien faire. Ceci est utile dans les situations où vous voulez vous assurer qu’un répertoire existe et le créer s’il n’existe pas.
Ces deux méthodes acceptent également une liste optionnelle de valeurs FileAttribute<?> à appliquer au répertoire ou aux répertoires nouvellement créés. Nous discutons des attributs de fichier vers la fin du chapitre.
Voici comment créer des répertoires :
Files.createDirectory(Path.of("/bison/champ"));
Files.createDirectories(Path.of("/bison/champ/paturage/vert"));
Le premier exemple crée un nouveau répertoire, champ, dans le répertoire /bison, en supposant que /bison existe ; sinon, une exception est lancée. Contrastez cela avec le second exemple, qui crée le répertoire vert ainsi que tous les répertoires parents suivants s’ils n’existent pas déjà, incluant bison, champ, et paturage.
Copie de Fichiers
La classe Files fournit une méthode pour copier des fichiers et des répertoires dans le système de fichiers.
public static Path copy(Path source, Path target,
CopyOption... options) throws IOException
La méthode copie un fichier ou un répertoire d’un emplacement à un autre en utilisant des objets Path. Les exemples suivants montrent comment copier un fichier et un répertoire :
Files.copy(Paths.get("/panda/bambou.txt"),
Paths.get("/panda-sauvegarde/bambou.txt"));
Files.copy(Paths.get("/tortue"), Paths.get("/tortuecopie"));
Lorsque des répertoires sont copiés, la copie est superficielle. Une copie superficielle signifie que les fichiers et sous-répertoires dans le répertoire ne sont pas copiés. Une copie profonde signifie que l’arbre entier est copié, y compris tout son contenu et ses sous-répertoires. Une copie profonde nécessite généralement la récursion, où une méthode s’appelle elle-même.
public void copierChemin(Path source, Path cible) {
try {
Files.copy(source, cible);
if(Files.isDirectory(source))
try (Stream s = Files.list(source)) {
s.forEach(p -> copierChemin(p,
cible.resolve(p.getFileName())));
}
} catch(IOException e) {
// Gérer l'exception
}
}
La méthode copie d’abord le chemin, qu’il s’agisse d’un fichier ou d’un répertoire. Si c’est un répertoire, seule une copie superficielle est effectuée. Ensuite, elle vérifie si le chemin est un répertoire et, si c’est le cas, effectue une copie récursive de chacun de ses éléments. Que se passe-t-il si la méthode rencontre un lien symbolique ? Ne vous inquiétez pas : la JVM ne suivra pas les liens symboliques lors de l’utilisation de la méthode list().
Copie et Remplacement de Fichiers
Par défaut, si la cible existe déjà, la méthode copy() lancera une exception. Vous pouvez modifier ce comportement en fournissant la valeur d’énumération StandardCopyOption REPLACE_EXISTING à la méthode. L’appel de méthode suivant écrasera le fichier film.txt s’il existe déjà :
Files.copy(Paths.get("livre.txt"), Paths.get("film.txt"),
StandardCopyOption.REPLACE_EXISTING);
Pour l’examen, vous devez savoir que sans l’option REPLACE_EXISTING, cette méthode lancera une exception si le fichier existe déjà.
Copie de Fichiers avec des Flux I/O
La classe Files inclut deux méthodes copy() qui fonctionnent avec des flux I/O.
public static long copy(InputStream in, Path target,
CopyOption... options) throws IOException
public static long copy(Path source, OutputStream out)
throws IOException
La première méthode lit le contenu d’un flux I/O et écrit la sortie dans un fichier. La seconde méthode lit le contenu d’un fichier et écrit la sortie dans un flux I/O. Ces méthodes sont très pratiques si vous avez besoin de lire/écrire rapidement des données depuis/vers le disque.
Voici des exemples de chaque méthode copy() :
try (var is = new FileInputStream("donnees-source.txt")) {
// Écrire les données du flux I/O dans un fichier
Files.copy(is, Paths.get("/mammiferes/loup.txt"));
}
Files.copy(Paths.get("/poisson/clown.xsl"), System.out);
Bien que nous ayons utilisé FileInputStream dans le premier exemple, le flux I/O aurait pu être n’importe quel flux I/O valide, y compris des connexions de site web, des ressources de flux en mémoire, etc. Le second exemple imprime le contenu d’un fichier directement dans le flux System.out.
Copie de Fichiers dans un Répertoire
Pour l’examen, il est important que vous compreniez comment la méthode copy() opère sur les fichiers et les répertoires. Par exemple, disons que nous avons un fichier, nourriture.txt, et un répertoire, /enclos. Le fichier et le répertoire existent tous les deux. Selon vous, quel est le résultat de l’exécution du processus suivant ?
var fichier = Paths.get("nourriture.txt");
var repertoire = Paths.get("/enclos");
Files.copy(fichier, repertoire);
Si vous avez dit qu’il créerait un nouveau fichier à /enclos/nourriture.txt, vous êtes très loin. Cela lance une exception. La commande essaie de créer un nouveau fichier nommé /enclos. Puisque le chemin /enclos existe déjà, une exception est lancée à l’exécution.
D’autre part, si le répertoire n’existait pas, le processus créerait un nouveau fichier avec le contenu de nourriture.txt, mais le fichier s’appellerait /enclos. Rappelez-vous, nous avons dit que les fichiers n’ont pas besoin d’avoir d’extensions, et dans cet exemple, cela importe.
Ce comportement s’applique à la fois aux méthodes copy() et move(), cette dernière que nous couvrons ensuite. Au cas où vous seriez curieux, la façon correcte de copier le fichier dans le répertoire est de faire ce qui suit :
var fichier = Paths.get("nourriture.txt");
var repertoire = Paths.get("/enclos/nourriture.txt");
Files.copy(fichier, repertoire);
Déplacement ou Renommage de Chemins avec move()
La classe Files fournit une méthode utile pour déplacer ou renommer des fichiers et des répertoires.
public static Path move(Path source, Path target,
CopyOption... options) throws IOException
L’exemple de code suivant utilise la méthode move() :
Files.move(Path.of("C:\\zoo"), Path.of("C:\\zoo-nouveau"));
Files.move(Path.of("C:\\utilisateur\\adresses.txt"),
Path.of("C:\\zoo-nouveau\\adresses2.txt"));
Le premier exemple renomme le répertoire zoo en répertoire zoo-nouveau, en conservant tout le contenu original du répertoire source. Le second exemple déplace le fichier adresses.txt du répertoire utilisateur au répertoire zoo-nouveau et le renomme adresses2.txt.
Similitudes entre move() et copy()
Comme copy(), move() nécessite REPLACE_EXISTING pour écraser la cible si elle existe ; sinon, il lancera une exception. Aussi comme copy(), move() ne mettra pas un fichier dans un répertoire si la source est un fichier et la cible est un répertoire. Au lieu de cela, il créera un nouveau fichier avec le nom du répertoire.
Exécution d’un Déplacement Atomique
Une autre valeur d’énumération que vous devez connaître pour l’examen lorsque vous travaillez avec la méthode move() est la valeur StandardCopyOption ATOMIC_MOVE.
Files.move(Path.of("souris.txt"), Path.of("gerbille.txt"),
StandardCopyOption.ATOMIC_MOVE);
Vous vous souvenez peut-être de la propriété atomique du Chapitre 13, “Concurrence”, et le principe d’un déplacement atomique est similaire. Un déplacement atomique est un déplacement dans lequel un fichier est déplacé dans le système de fichiers comme une seule opération indivisible. Autrement dit, tout processus surveillant le système de fichiers ne voit jamais un fichier incomplet ou partiellement écrit. Si le système de fichiers ne prend pas en charge cette fonctionnalité, une AtomicMoveNotSupportedException sera lancée.
Notez que bien qu’ATOMIC_MOVE soit disponible en tant que membre du type StandardCopyOption, il lancera probablement une exception s’il est passé à une méthode copy().
Suppression d’un Fichier avec delete() et deleteIfExists()
La classe Files inclut deux méthodes qui suppriment un fichier ou un répertoire vide dans le système de fichiers.
public static void delete(Path path) throws IOException
public static boolean deleteIfExists(Path path) throws IOException
Pour supprimer un répertoire, il doit être vide. Ces deux méthodes lancent une exception si elles sont utilisées sur un répertoire non vide. De plus, si le chemin est un lien symbolique, le lien symbolique sera supprimé, pas le chemin vers lequel le lien symbolique pointe.
Les méthodes diffèrent sur la façon dont elles gèrent un chemin qui n’existe pas. La méthode delete() lance une exception si le chemin n’existe pas, tandis que la méthode deleteIfExists() renvoie true si la suppression a réussi ou false autrement. Similaire à createDirectories(), deleteIfExists() est utile dans les situations où vous voulez vous assurer qu’un chemin n’existe pas et le supprimer s’il existe.
Voici un exemple de code qui effectue des opérations delete() :
Files.delete(Paths.get("/vautour/plumes.txt"));
Files.deleteIfExists(Paths.get("/pigeon"));
Le premier exemple supprime le fichier plumes.txt dans le répertoire vautour, et il lance une NoSuchFileException si le fichier ou le répertoire n’existe pas. Le second exemple supprime le répertoire pigeon, en supposant qu’il est vide. Si le répertoire pigeon n’existe pas, la seconde ligne ne lancera pas d’exception.
Comparaison de Fichiers avec isSameFile() et mismatch()
Puisqu’un chemin peut inclure des symboles de chemin et des liens symboliques dans un système de fichiers, la méthode equals() ne peut pas être utilisée pour savoir si deux instances Path font référence au même fichier. Heureusement, il existe la méthode isSameFile(). Cette méthode prend deux objets Path en entrée, résout tous les symboles de chemin et suit les liens symboliques. Malgré le nom, la méthode peut également être utilisée pour déterminer si deux objets Path font référence au même répertoire.
Bien que la plupart des utilisations de isSameFile() déclencheront une exception si les chemins n’existent pas, il y a un cas spécial où ce n’est pas le cas. Si les deux objets path sont égaux en termes de equals(), la méthode renverra simplement true sans vérifier si le fichier existe.
Supposons que le système de fichiers existe, comme montré dans la Figure 14.4, avec un lien symbolique de /animaux/serpent à /animaux/cobra.
Étant donné la structure définie, qu’est-ce que ce qui suit produirait ?
System.out.println(Files.isSameFile(
Path.of("/animaux/cobra"),
Path.of("/animaux/serpent")));
System.out.println(Files.isSameFile(
Path.of("/animaux/singe/oreilles.png"),
Path.of("/animaux/loup/oreilles.png")));
Puisque serpent est un lien symbolique vers cobra, le premier exemple affiche true. Dans le second exemple, les chemins font référence à différents fichiers, donc false est imprimé.
Parfois, vous voulez comparer le contenu du fichier plutôt que de savoir s’il s’agit physiquement du même fichier. Par exemple, nous pourrions avoir deux fichiers avec le texte bonjour. La méthode mismatch() a été introduite en Java 12 pour nous aider ici. Elle prend deux objets Path en entrée. La méthode renvoie -1 si les fichiers sont les mêmes ; sinon, elle renvoie l’index de la première position dans le fichier qui diffère.
System.out.println(Files.mismatch(
Path.of("/animaux/singe.txt"),
Path.of("/animaux/loup.txt")));
Supposons que singe.txt contient le nom Harold et loup.txt contient le nom Howler. Le code précédent imprime 1 dans ce cas car la deuxième position est différente, et nous utilisons l’indexation à base zéro en Java. Étant donné ces valeurs, que pensez-vous que ce code imprime ?
System.out.println(Files.mismatch(
Path.of("/animaux/loup.txt"),
Path.of("/animaux/singe.txt")));
La réponse est la même que l’exemple précédent. Le code imprime 1 à nouveau. La méthode mismatch() est symétrique et renvoie le même résultat quel que soit l’ordre des paramètres.