Présentation

Cet article aborde des tâches courantes que les développeurs d’applications peuvent rencontrer, en particulier dans le cadre des applications web, telles que :

  • Lecture et écriture de fichiers texte
  • Récupération de texte, d’images et de JSON depuis le web
  • Exploration des fichiers dans un répertoire
  • Lecture d’un fichier ZIP
  • Création d’un fichier ou d’un répertoire temporaire

La Java API propose de nombreuses autres fonctionnalités, détaillées dans le tutoriel sur l’API Java I/O.

Nous nous concentrerons ici sur les améliorations apportées à l’API depuis Java 8, notamment :

  • UTF-8 est devenu l’encodage par défaut pour les opérations d’entrée/sortie depuis Java 18 (JEP 400 : UTF-8 par défaut)
  • La classe java.nio.file.Files, introduite dans Java 7, a bénéficié de nouvelles méthodes dans les versions 8, 11 et 12
  • La classe java.io.InputStream a également reçu des méthodes utiles dans Java 9, 11 et 12
  • Les classes java.io.File et java.io.BufferedReader sont désormais largement obsolètes, bien qu’elles soient encore fréquemment mentionnées dans les recherches en ligne et les discussions d’IA.

Lecture de Fichiers Texte

Pour lire un fichier texte dans une chaîne, vous pouvez procéder comme suit :

String contenu=Files.readString(chemin);

Dans cet exemple, chemin est une instance de java.nio.Path, obtenue de cette manière :

var chemin=Path.of("/usr/share/dict/words");

Avant Java 18, il était fortement recommandé de spécifier l’encodage des caractères lors des opérations de lecture ou d’écriture de chaînes. Aujourd’hui, l’encodage le plus courant est UTF-8, mais pour des raisons de compatibilité, Java utilisait l' »encodage de la plateforme », qui peut être un encodage hérité sur Windows. Pour garantir la portabilité, les opérations d’entrée/sortie de texte nécessitaient le paramètre StandardCharsets.UTF_8. Cela n’est plus nécessaire.

Si vous souhaitez obtenir le fichier sous forme de lignes, utilisez :

List lignes=Files.readAllLines(chemin);

Pour les fichiers volumineux, traitez les lignes de manière paresseuse en tant que Stream :

try (Stream lignes=Files.lines(chemin)) {
    . . .
}

Utilisez également Files.lines si vous pouvez traiter naturellement les lignes avec des opérations de flux (comme map, filter). Notez que le flux retourné par Files.lines doit être fermé. Pour garantir cela, utilisez une instruction try-with-resources, comme dans l’exemple précédent.

Il n’y a plus de raison valable d’utiliser la méthode readLine de java.io.BufferedReader.

Pour diviser votre entrée en éléments autres que des lignes, utilisez un java.util.Scanner. Par exemple, voici comment lire des mots, séparés par des caractères non alphabétiques :

Stream tokens=new Scanner(chemin).useDelimiter("PL+").tokens();

La classe Scanner propose également des méthodes pour lire des nombres, mais il est généralement plus simple de lire l’entrée sous forme d’une chaîne par ligne, ou d’une seule chaîne, puis de l’analyser.

Faites attention lors de l’analyse des nombres à partir de fichiers texte, car leur format peut dépendre de la locale. Par exemple, l’entrée 100.000 représente 100.0 dans la locale américaine mais 100000.0 dans la locale allemande. Utilisez java.text.NumberFormat pour une analyse spécifique à la locale. Alternativement, vous pouvez utiliser Integer.parseInt/Double.parseDouble.

Écriture de Fichiers Texte

Pour écrire une chaîne dans un fichier texte, il suffit d’un seul appel :

String contenu=. . .;
Files.writeString(chemin, contenu);

Si vous avez une liste de lignes plutôt qu’une seule chaîne, utilisez :

List lignes=. . .;
Files.write(chemin, lignes);

Pour une sortie plus générale, utilisez un PrintWriter si vous souhaitez utiliser la méthode printf :

var writer=new PrintWriter(chemin.toFile());
writer.printf(locale, "Bonjour, %s, l'année prochaine vous aurez %d ans !%n", nom, age + 1);

Notez que printf est spécifique à la locale. Lors de l’écriture de nombres, assurez-vous de les formater correctement. Au lieu d’utiliser printf, envisagez d’utiliser java.text.NumberFormat ou Integer.toString/Double.toString.

Étrangement, à partir de Java 21, il n’existe pas de constructeur PrintWriter avec un paramètre Path.

Si vous n’utilisez pas printf, vous pouvez recourir à la classe BufferedWriter et écrire des chaînes avec la méthode write.

var writer=Files.newBufferedWriter(chemin);
writer.write(ligne); // N'écrit pas de séparateur de ligne
writer.newLine(); 

N’oubliez pas de fermer le writer une fois que vous avez terminé.

Lecture Depuis un Flux Entrant

Une des raisons les plus courantes d’utiliser un flux est de lire des données depuis un site web.

Si vous devez définir des en-têtes de requête ou lire des en-têtes de réponse, utilisez le HttpClient :

HttpClient client=HttpClient.newBuilder().build();
HttpRequest request=HttpRequest.newBuilder()
    .uri(URI.create("https://horstmann.com/index.html"))
    .GET()
    .build();
HttpResponse response=client.send(request, HttpResponse.BodyHandlers.ofString());
String resultat=response.body();

Cela peut sembler excessif si vous souhaitez simplement obtenir les données. Dans ce cas, utilisez :

InputStream in=new URI("https://horstmann.com/index.html").toURL().openStream();

Ensuite, lisez les données dans un tableau d’octets et, si nécessaire, transformez-les en chaîne :

byte[] octets=in.readAllBytes();
String resultat=new String(octets);

Ou transférez les données vers un flux de sortie :

OutputStream out=Files.newOutputStream(chemin);
in.transferTo(out);

Notez qu’aucune boucle n’est nécessaire si vous souhaitez simplement lire tous les octets d’un flux entrant.

Mais avez-vous vraiment besoin d’un flux entrant ? De nombreuses API vous offrent la possibilité de lire à partir d’un fichier ou d’une URL.

Votre bibliothèque JSON préférée dispose probablement de méthodes pour lire à partir d’un fichier ou d’une URL. Par exemple, avec Jackson jr :

URL url=new URI("https://dog.ceo/api/breeds/image/random").toURL();
Map resultat=JSON.std.mapFrom(url);

Voici comment lire l’image d’un chien à partir de l’appel précédent :

URL url=new URI(result.get("message").toString()).toURL();
BufferedImage img=javax.imageio.ImageIO.read(url);

Cela est préférable à l’utilisation d’un flux d’entrée pour la méthode read, car la bibliothèque peut tirer parti d’informations supplémentaires provenant de l’URL pour déterminer le type d’image.

L’API des Fichiers

La classe java.nio.file.Files offre un ensemble complet d’opérations sur les fichiers, telles que la création, la copie, le déplacement et la suppression de fichiers et de répertoires. Le tutoriel sur les bases du système de fichiers fournit une description détaillée. Dans cette section, je vais mettre en avant quelques tâches courantes.

Parcourir les Entrées dans les Répertoires et Sous-Répertoires

Dans la plupart des cas, vous pouvez utiliser l’une des deux méthodes. La méthode Files.list permet de visiter toutes les entrées (fichiers, sous-répertoires, liens symboliques) d’un répertoire.

try (Stream entries=Files.list(pathToDirectory)) {
    . . .
}

Utilisez une déclaration try-with-resources pour garantir que l’objet de flux, qui suit l’itération, sera fermé.

Si vous souhaitez également explorer les entrées des répertoires descendants, utilisez plutôt la méthode Files.walk.

Stream entries=Files.walk(pathToDirectory);

Vous pouvez ensuite utiliser les méthodes de flux pour cibler les entrées qui vous intéressent et collecter les résultats :

try (Stream entries=Files.walk(pathToDirectory)) {
    List htmlFiles=entries.filter(p -> p.toString().endsWith("html")).toList();
    . . .
}

Voici d’autres méthodes pour parcourir les entrées de répertoire :

  • Une version surchargée de Files.walk vous permet de limiter la profondeur de l’arborescence parcourue.
  • Deux méthodes Files.walkFileTree offrent un contrôle accru sur le processus d’itération, en notifiant un FileVisitor lors de la première et de la dernière visite d’un répertoire. Cela peut être utile, notamment pour vider et supprimer un arbre de répertoires. Consultez le tutoriel sur le parcours de l’arborescence des fichiers pour plus de détails. À moins que vous n’ayez besoin de ce contrôle, utilisez la méthode plus simple Files.walk.
  • La méthode Files.find fonctionne comme Files.walk, mais vous fournissez un filtre qui inspecte chaque chemin et ses BasicFileAttributes. Cela est légèrement plus efficace que de lire les attributs séparément pour chaque fichier.
  • Deux méthodes Files.newDirectoryStream(Path) renvoient des instances de DirectoryStream, qui peuvent être utilisées dans des boucles for améliorées. Il n’y a pas d’avantage par rapport à l’utilisation de Files.list.
  • Les méthodes héritées File.list ou File.listFiles renvoient des noms de fichiers ou des objets File. Celles-ci sont désormais obsolètes.

Travailler avec des Fichiers ZIP

Depuis Java 1.1, les classes ZipInputStream et ZipOutputStream fournissent une API pour le traitement des fichiers ZIP. Cependant, l’API est un peu maladroite. Java 8 a introduit un système de fichiers ZIP beaucoup plus agréable :

try (FileSystem fs=FileSystems.newFileSystem(pathToZipFile)) {
    . . .
}

La déclaration try-with-resources garantit que la méthode close est appelée après les opérations sur le fichier ZIP. Cette méthode met à jour le fichier ZIP pour refléter les modifications dans le système de fichiers.

Vous pouvez ensuite utiliser les méthodes de la classe Files. Ici, nous obtenons une liste de tous les fichiers dans le fichier ZIP :

try (Stream entries=Files.walk(fs.getPath("/"))) {
    List filesInZip=entries.filter(Files::isRegularFile).toList();
}

Pour lire le contenu d’un fichier, utilisez simplement Files.readString ou Files.readAllBytes :

String contents=Files.readString(fs.getPath("/LICENSE"));

Vous pouvez supprimer des fichiers avec Files.delete. Pour ajouter ou remplacer des fichiers, utilisez simplement Files.writeString ou Files.write.

Créer des Fichiers et Répertoires Temporaires

Il m’arrive souvent de devoir collecter des entrées utilisateur, de produire des fichiers et d’exécuter un processus externe. Dans ce cas, j’utilise des fichiers temporaires, qui disparaissent après le prochain redémarrage, ou un répertoire temporaire que je supprime après l’achèvement du processus.

J’utilise les deux méthodes Files.createTempFile et Files.createTempDirectory pour cela.

Path filePath=Files.createTempFile("myapp", ".txt");
Path dirPath=Files.createTempDirectory("myapp");

Cela crée un fichier ou un répertoire temporaire à un emplacement approprié (/tmp sous Linux) avec le préfixe donné et, pour un fichier, un suffixe.

Synthèse

Les recherches sur le web et les discussions avec l’IA peuvent suggérer des codes inutilement complexes pour des opérations d’E/S courantes. Il existe souvent de meilleures alternatives :

  1. Vous n’avez pas besoin d’une boucle pour lire ou écrire des chaînes ou des tableaux d’octets.
  2. Vous n’avez même pas besoin d’un flux, d’un lecteur ou d’un écrivain.
  3. Familiarisez-vous avec les méthodes Files pour créer, copier, déplacer et supprimer des fichiers et des répertoires.
  4. Utilisez Files.list ou Files.walk pour parcourir les entrées de répertoire.
  5. Utilisez un système de fichiers ZIP pour traiter les fichiers ZIP.
  6. Évitez la classe héritée File.
Show Comments (0)
Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *