Pourquoi Java IO est-il si mauvais ?

L'API IO (Input & Output) est une API Java qui permet aux développeurs de travailler facilement avec les flux. Disons que nous recevons des données (par exemple, prénom, deuxième prénom, nom de famille) et que nous devons les écrire dans un fichier - le moment est venu d'utiliser java.io .

Structure de la bibliothèque java.io

Mais Java IO a ses inconvénients, alors parlons de chacun d'eux tour à tour :

  1. Blocage d'accès pour entrée/sortie. Le problème est que lorsqu'un développeur essaie de lire ou d'écrire quelque chose dans un fichier à l'aide de Java IO , il verrouille le fichier et en bloque l'accès jusqu'à ce que le travail soit terminé.
  2. Pas de prise en charge des systèmes de fichiers virtuels.
  3. Pas de support pour les liens.
  4. Beaucoup, beaucoup d'exceptions vérifiées.

Travailler avec des fichiers implique toujours de travailler avec des exceptions : par exemple, essayer de créer un nouveau fichier qui existe déjà lèvera une IOException . Dans ce cas, l'application doit continuer à s'exécuter et l'utilisateur doit être informé de la raison pour laquelle le fichier n'a pas pu être créé.


try {
	File.createTempFile("prefix", "");
} catch (IOException e) {
	// Handle the IOException
}

/**
 * Creates an empty file in the default temporary-file directory 
 * any exceptions will be ignored. This is typically used in finally blocks. 
 * @param prefix 
 * @param suffix 
 * @throws IOException - If a file could not be created
 */
public static File createTempFile(String prefix, String suffix) 
throws IOException {
...
}

Ici, nous voyons que la méthode createTempFile lève une IOException lorsque le fichier ne peut pas être créé. Cette exception doit être gérée de manière appropriée. Si nous essayons d'appeler cette méthode en dehors d'un bloc try-catch , le compilateur générera une erreur et proposera deux options pour la corriger : envelopper la méthode dans un bloc try-catch ou faire en sorte que la méthode qui appelle File.createTempFile lance une IOException ( afin qu'il puisse être traité à un niveau supérieur).

Arriver à Java NIO et comment il se compare à Java IO

Java NIO , ou Java Non-Blocking I/O (ou parfois Java New I/O) est conçu pour les opérations d'E/S hautes performances.

Comparons les méthodes Java IO et celles qui les remplacent.

Parlons d'abord du travail avec Java IO :

Classe InputStreamInputStream class


try(FileInputStream fin = new FileInputStream("C:/codegym/file.txt")){
    System.out.printf("File size: %d bytes \n", fin.available());
    int i=-1;
    while((i=fin.read())!=-1) {
        System.out.print((char)i);
    }   
} catch(IOException ex) {
    System.out.println(ex.getMessage());
}

La classe FileInputStream sert à lire les données d'un fichier. Il hérite de la classe InputStream et implémente donc toutes ses méthodes. Si le fichier ne peut pas être ouvert, une FileNotFoundException est levée.

Classe OutputStream


String text = "Hello world!"; // String to write
try(FileOutputStream fos = new FileOutputStream("C:/codegym/file.txt")){
    // Convert our string into bytes
    byte[] buffer = text.getBytes();
    fos.write(buffer, 0, buffer.length);
    System.out.println("The file has been written");
} catch(IOException ex) {
    System.out.println(ex.getMessage());
}

La classe FileOutputStream pour écrire des octets dans un fichier. Il dérive de la classe OutputStream .

Cours de lecture et d'écriture

La classe FileReader nous permet de lire des données de caractères à partir de flux, et la classe FileWriter est utilisée pour écrire des flux de caractères. Le code suivant montre comment écrire et lire à partir d'un fichier :


        String fileName = "c:/codegym/Example.txt";

        // Create a FileWriter object
        try (FileWriter writer = new FileWriter(fileName)) {

            // Write content to file
            writer.write("This is a simple example\nin which we\nwrite to a file\nand read from a file\n");
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Create a FileReader object
        try (FileReader fr = new FileReader(fileName)) {
            char[] a = new char[200]; // Number of characters to read
            fr.read(a); // Read content into an array
            for (char c : a) {
                System.out.print(c); // Display characters one by one
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

Parlons maintenant de Java NIO :

Canaliser

Contrairement aux flux utilisés dans Java IO , Channel est une interface bidirectionnelle, c'est-à-dire qu'il peut à la fois lire et écrire. Un canal Java NIO prend en charge le flux de données asynchrone en mode bloquant et non bloquant.


RandomAccessFile aFile = new RandomAccessFile("C:/codegym/file.txt", "rw");
FileChannel inChannel = aFile.getChannel();

ByteBuffer buf = ByteBuffer.allocate(100);
int bytesRead = inChannel.read(buf);

while (bytesRead != -1) {
  System.out.println("Read: " + bytesRead);
  buf.flip();
	  while(buf.hasRemaining()) {
	      System.out.print((char) buf.get());
	  }
  buf.clear();
  bytesRead = inChannel.read(buf);
}
aFile.close();

Ici, nous avons utilisé un FileChannel . Nous utilisons un canal de fichier pour lire les données d'un fichier. Un objet de canal de fichier ne peut être créé qu'en appelant la méthode getChannel() sur un objet de fichier — il n'y a aucun moyen de créer directement un objet de canal de fichier.

En plus de FileChannel , nous avons d'autres implémentations de canaux :

  • FileChannel — pour travailler avec des fichiers

  • DatagramChannel — un canal pour travailler sur une connexion UDP

  • SocketChannel — un canal pour travailler sur une connexion TCP

  • ServerSocketChannel contient un SocketChannel et est similaire au fonctionnement d'un serveur Web

Remarque : FileChannel ne peut pas être basculé en mode non bloquant. Le mode non bloquant de Java NIO vous permet de demander la lecture de données à partir d'un canal et de ne recevoir que ce qui est actuellement disponible (ou rien du tout s'il n'y a pas encore de données disponibles) . Cela dit, SelectableChannel et ses implémentations peuvent être mis en mode non bloquant à l'aide de la méthode connect() .

Sélecteur

Java NIO a introduit la possibilité de créer un thread qui sait quel canal est prêt à écrire et à lire des données et peut traiter ce canal particulier. Cette capacité est implémentée à l'aide de la classe Selector .

Connecter des canaux à un sélecteur


Selector selector = Selector.open();
channel.configureBlocking(false); // Non-blocking mode
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Nous créons donc notre Selector et le connectons à un SelectableChannel .

Pour être utilisé avec un sélecteur, un canal doit être en mode non bloquant. Cela signifie que vous ne pouvez pas utiliser FileChannel avec un sélecteur, car FileChannel ne peut pas être mis en mode non bloquant. Mais les canaux de socket fonctionneront bien.

Mentionnons ici que dans notre exemple SelectionKey est un ensemble d'opérations pouvant être effectuées sur un canal. La touche de sélection permet de connaître l'état d'un canal.

Types de clé de sélection

  • SelectionKey.OP_CONNECT signifie un canal prêt à se connecter au serveur.

  • SelectionKey.OP_ACCEPT est un canal prêt à accepter les connexions entrantes.

  • SelectionKey.OP_READ signifie un canal qui est prêt à lire des données.

  • SelectionKey.OP_WRITE signifie un canal qui est prêt à écrire des données.

Amortir

Les données sont lues dans une mémoire tampon pour un traitement ultérieur. Un développeur peut faire des allers-retours sur le buffer, ce qui nous donne un peu plus de flexibilité lors du traitement des données. Dans le même temps, nous devons vérifier si le tampon contient la quantité de données requise pour un traitement correct. De plus, lors de la lecture de données dans un tampon, assurez-vous de ne pas détruire les données existantes qui n'ont pas encore été traitées.


ByteBuffer buf = ByteBuffer.allocate (2048); 
int bytesRead = channel.read(buf);
buf.flip(); // Change to read mode
while (buf.hasRemaining()) { 
	byte data = buf.get(); // There are methods for primitives 
}

buf.clear(); // Clear the buffer - now it can be reused

Propriétés de base d'un tampon :

Attributs de base
capacité La taille de la mémoire tampon, qui correspond à la longueur du tableau.
position La position de départ pour travailler avec des données.
limite La limite de fonctionnement. Pour les opérations de lecture, la limite correspond à la quantité de données pouvant être lues, mais pour les opérations d'écriture, il s'agit de la capacité ou du quota disponible pour l'écriture.
marquer L'indice de la valeur à laquelle le paramètre de position sera réinitialisé lorsque la méthode reset() est appelée.

Parlons maintenant un peu des nouveautés de Java NIO.2 .

Chemin

Path représente un chemin dans le système de fichiers. Il contient le nom d'un fichier et une liste de répertoires qui définissent le chemin d'accès à celui-ci.


Path relative = Paths.get("Main.java");
System.out.println("File: " + relative);
// Get the file system
System.out.println(relative.getFileSystem());

Paths est une classe très simple avec une seule méthode statique : get() . Il a été créé uniquement pour obtenir un objet Path à partir de la chaîne ou de l'URI transmis.


Path path = Paths.get("c:\\data\\file.txt");

Des dossiers

Files est une classe utilitaire qui nous permet d'obtenir directement la taille d'un fichier, de copier des fichiers, etc.


Path path = Paths.get("files/file.txt");
boolean pathExists = Files.exists(path);

Système de fichiers

FileSystem fournit une interface au système de fichiers. FileSystem fonctionne comme une usine pour créer divers objets (Chemin,PathMatter,Des dossiers). Il nous aide à accéder aux fichiers et autres objets du système de fichiers.


try {
      FileSystem filesystem = FileSystems.getDefault();
      for (Path rootdir : filesystem.getRootDirectories()) {
          System.out.println(rootdir.toString());
      }
  } catch (Exception e) {
      e.printStackTrace();
  }

Test de performance

Pour ce test, prenons deux fichiers. Le premier est un petit fichier texte et le second est une grande vidéo.

Nous allons créer un fichier et ajouter quelques mots et caractères :

% toucher text.txt

Notre fichier occupe au total 42 octets en mémoire :

Écrivons maintenant du code qui copiera notre fichier d'un dossier à un autre. Testons-le sur les petits et gros fichiers afin de comparer la vitesse de IO et NIO et NIO.2 .

Code pour copier, écrit en Java IO :


public static void main(String[] args) {
        long currentMills = System.currentTimeMillis();
        long startMills = currentMills;
        File src = new File("/Users/IdeaProjects/testFolder/text.txt");
        File dst = new File("/Users/IdeaProjects/testFolder/text1.txt");
        copyFileByIO(src, dst);
        currentMills = System.currentTimeMillis();
        System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
    }

    public static void copyFileByIO(File src, File dst) {
        try(InputStream inputStream = new FileInputStream(src);
            OutputStream outputStream = new FileOutputStream(dst)){

            byte[] buffer = new byte[1024];
            int length;
            // Read data into a byte array and then output to an OutputStream
            while((length = inputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Et voici le code pour Java NIO :


public static void main(String[] args) {
        long currentMills = System.currentTimeMillis();
        long startMills = currentMills;

        File src = new File("/Users/IdeaProjects/testFolder/text.txt");
        File dst = new File("/Users/IdeaProjects/testFolder/text2.txt");
        // Code for copying using NIO
        copyFileByChannel(src, dst);
        currentMills = System.currentTimeMillis();
        System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
    }

    public static void copyFileByChannel(File src, File dst) {
        // 1. Get a FileChannel for the source file and the target file
        try(FileChannel srcFileChannel  = new FileInputStream(src).getChannel();
            FileChannel dstFileChannel = new FileOutputStream(dst).getChannel()){
            // 2. Size of the current FileChannel
            long count = srcFileChannel.size();
            while(count > 0) {
                /**=============================================================
                 * 3. Write bytes from the source file's FileChannel to the target FileChannel
                 * 1. srcFileChannel.position(): the starting position in the source file, cannot be negative
                 * 2. count: the maximum number of bytes transferred, cannot be negative
                 * 3. dstFileChannel: the target file
                 *==============================================================*/
                long transferred = srcFileChannel.transferTo(srcFileChannel.position(),
                        count, dstFileChannel);
                // 4. After the transfer is complete, change the position of the original file to the new one
                srcFileChannel.position(srcFileChannel.position() + transferred);
                // 5. Calculate how many bytes are left to transfer
                count -= transferred;
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Code pour Java NIO.2 :


public static void main(String[] args) {
  long currentMills = System.currentTimeMillis();
  long startMills = currentMills;

  Path sourceDirectory = Paths.get("/Users/IdeaProjects/testFolder/test.txt");
  Path targetDirectory = Paths.get("/Users/IdeaProjects/testFolder/test3.txt");
  Files.copy(sourceDirectory, targetDirectory);

  currentMills = System.currentTimeMillis();
  System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
}

Commençons par le petit fichier.

Le temps d'exécution pour Java IO était de 1 milliseconde en moyenne. En exécutant le test plusieurs fois, nous obtenons des résultats de 0 à 2 millisecondes.

Temps d'exécution en millisecondes : 1

Le temps d'exécution de Java NIO est beaucoup plus long. Le temps moyen est de 11 millisecondes. Les résultats variaient de 9 à 16. En effet, Java IO fonctionne différemment de notre système d'exploitation. IO déplace et traite les fichiers un par un, mais le système d'exploitation envoie les données en un seul gros morceau. NIO a mal fonctionné car il est orienté tampon et non orienté flux comme IO .

Temps d'exécution en millisecondes : 12

Et exécutons également notre test pour Java NIO.2 . NIO.2 a amélioré la gestion des fichiers par rapport à Java NIO . C'est pourquoi la bibliothèque mise à jour produit des résultats si différents :

Temps d'exécution en millisecondes : 3

Essayons maintenant de tester notre gros fichier, une vidéo de 521 Mo. La tâche sera exactement la même : copier le fichier dans un autre dossier. Regarder!

Résultats pour Java IO :

Temps d'exécution en millisecondes : 1866

Et voici le résultat pour Java NIO :

Temps d'exécution en millisecondes : 205

Java NIO a traité le fichier 9 fois plus vite lors du premier test. Des tests répétés ont montré approximativement les mêmes résultats.

Et nous allons également tenter notre test sur Java NIO.2 :

Temps d'exécution en millisecondes : 360

Pourquoi ce résultat ? Tout simplement parce que cela n'a pas beaucoup de sens pour nous de comparer les performances entre eux, car ils servent à des fins différentes. NIO est une E/S de bas niveau plus abstraite, tandis que NIO.2 est orienté vers la gestion de fichiers.

Résumé

Nous pouvons dire en toute sécurité que Java NIO est nettement plus efficace lorsque vous travaillez avec des fichiers grâce à l'utilisation à l'intérieur des blocs. Un autre avantage est que la bibliothèque NIO est divisée en deux parties : une pour travailler avec des fichiers, une autre pour travailler avec le réseau.

La nouvelle API de Java NIO.2 pour travailler avec des fichiers offre de nombreuses fonctionnalités utiles :

  • adressage de système de fichiers beaucoup plus utile en utilisant Path ,

  • amélioration significative de la gestion des fichiers ZIP à l'aide d'un fournisseur de système de fichiers personnalisé,

  • accès aux attributs de fichiers spéciaux,

  • de nombreuses méthodes pratiques, telles que la lecture d'un fichier entier avec une seule instruction, la copie d'un fichier avec une seule instruction, etc.

Il s'agit de fichiers et de systèmes de fichiers, et tout est de très haut niveau.

La réalité aujourd'hui est que Java NIO représente environ 80 à 90 % du travail sur les fichiers, bien que la part de Java IO soit toujours importante.

💡 PS Ces tests ont été exécutés sur un MacBook Pro 14" 16/512. Les résultats des tests peuvent différer en fonction du système d'exploitation et des spécifications de la station de travail.