Warum ist Java IO so schlecht?

Die IO-API (Input & Output) ist eine Java-API, die es Entwicklern erleichtert, mit Streams zu arbeiten. Nehmen wir an, wir erhalten einige Daten (z. B. Vorname, zweiter Vorname, Nachname) und müssen diese in eine Datei schreiben – jetzt ist die Zeit gekommen, java.io zu verwenden .

Struktur der java.io-Bibliothek

Aber Java IO hat seine Nachteile, also lassen Sie uns der Reihe nach über jeden von ihnen sprechen:

  1. Zugriffssperre für Ein-/Ausgabe. Das Problem besteht darin, dass ein Entwickler, wenn er versucht, mit Java IO etwas in eine Datei zu lesen oder zu schreiben , die Datei sperrt und den Zugriff darauf blockiert, bis die Aufgabe erledigt ist.
  2. Keine Unterstützung für virtuelle Dateisysteme.
  3. Keine Unterstützung für Links.
  4. Viele, viele geprüfte Ausnahmen.

Das Arbeiten mit Dateien erfordert immer das Arbeiten mit Ausnahmen: Wenn Sie beispielsweise versuchen, eine neue Datei zu erstellen, die bereits vorhanden ist, wird eine IOException ausgelöst . In diesem Fall sollte die Anwendung weiterlaufen und der Benutzer sollte benachrichtigt werden, warum die Datei nicht erstellt werden konnte.


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 {
...
}

Hier sehen wir, dass die Methode „createTempFile“ eine IOException auslöst, wenn die Datei nicht erstellt werden kann. Diese Ausnahme muss entsprechend behandelt werden. Wenn wir versuchen, diese Methode außerhalb eines Try-Catch- Blocks aufzurufen , generiert der Compiler einen Fehler und schlägt zwei Optionen zur Behebung vor: Wickeln Sie die Methode in einen Try-Catch- Block ein oder lassen Sie die Methode, die File.createTempFile aufruft, eine IOException auslösen ( damit es auf einer höheren Ebene gehandhabt werden kann).

Ankunft bei Java NIO und wie es im Vergleich zu Java IO abschneidet

Java NIO oder Java Non-Blocking I/O (manchmal auch Java New I/O) ist für Hochleistungs-I/O-Vorgänge konzipiert.

Vergleichen wir Java IO- Methoden und diejenigen, die sie ersetzen.

Lassen Sie uns zunächst über die Arbeit mit Java IO sprechen :

InputStream-Klasse


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());
}

Die FileInputStream- Klasse dient zum Lesen von Daten aus einer Datei. Es erbt die InputStream- Klasse und implementiert daher alle ihre Methoden. Wenn die Datei nicht geöffnet werden kann, wird eine FileNotFoundException ausgelöst.

OutputStream-Klasse


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());
}

Die FileOutputStream- Klasse zum Schreiben von Bytes in eine Datei. Es ist von der OutputStream- Klasse abgeleitet.

Lese- und Schreibkurse

Mit der FileReader- Klasse können wir Zeichendaten aus Streams lesen, und die FileWriter- Klasse wird zum Schreiben von Zeichenstreams verwendet. Der folgende Code zeigt, wie man aus einer Datei schreibt und liest:


        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();
        }

Lassen Sie uns nun über Java NIO sprechen :

Kanal

Im Gegensatz zu den in Java IO verwendeten Streams ist Channel eine bidirektionale Schnittstelle, das heißt, er kann sowohl lesen als auch schreiben . Ein Java-NIO- Kanal unterstützt den asynchronen Datenfluss sowohl im blockierenden als auch im nicht blockierenden Modus.


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();

Hier haben wir einen FileChannel verwendet . Wir verwenden einen Dateikanal, um Daten aus einer Datei zu lesen. Ein Dateikanalobjekt kann nur durch Aufrufen der getChannel() -Methode für ein Dateiobjekt erstellt werden – es gibt keine Möglichkeit, ein Dateikanalobjekt direkt zu erstellen.

Zusätzlich zu FileChannel haben wir weitere Kanalimplementierungen:

  • FileChannel – zum Arbeiten mit Dateien

  • DatagramChannel – ein Kanal zum Arbeiten über eine UDP-Verbindung

  • SocketChannel – ein Kanal zum Arbeiten über eine TCP-Verbindung

  • ServerSocketChannel enthält einen SocketChannel und ähnelt der Funktionsweise eines Webservers

Bitte beachten Sie: FileChannel kann nicht in den nicht blockierenden Modus geschaltet werden. Mit dem nicht blockierenden Modus von Java NIO können Sie Lesedaten von einem Kanal anfordern und nur das empfangen, was aktuell verfügbar ist (oder gar nichts, wenn noch keine Daten verfügbar sind). Allerdings können SelectableChannel und seine Implementierungen mithilfe der Methode connect() in den nicht blockierenden Modus versetzt werden .

Wähler

Mit Java NIO wurde die Möglichkeit eingeführt, einen Thread zu erstellen, der weiß, welcher Kanal zum Schreiben und Lesen von Daten bereit ist und diesen bestimmten Kanal verarbeiten kann. Diese Fähigkeit wird mithilfe der Selector -Klasse implementiert.

Kanäle mit einem Selektor verbinden


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

Also erstellen wir unseren Selector und verbinden ihn mit einem SelectableChannel .

Um mit einem Selektor verwendet zu werden, muss sich ein Kanal im nicht blockierenden Modus befinden. Das bedeutet, dass Sie FileChannel nicht mit einem Selektor verwenden können , da FileChannel nicht in den nicht blockierenden Modus versetzt werden kann. Aber Socket-Kanäle werden gut funktionieren.

Hier erwähnen wir, dass SelectionKey in unserem Beispiel eine Reihe von Operationen ist, die auf einem Kanal ausgeführt werden können. Die Auswahltaste informiert uns über den Status eines Kanals.

Arten von SelectionKey

  • SelectionKey.OP_CONNECT gibt einen Kanal an, der bereit ist, eine Verbindung zum Server herzustellen.

  • SelectionKey.OP_ACCEPT ist ein Kanal, der bereit ist, eingehende Verbindungen zu akzeptieren.

  • SelectionKey.OP_READ gibt einen Kanal an, der zum Lesen von Daten bereit ist.

  • SelectionKey.OP_WRITE gibt einen Kanal an, der zum Schreiben von Daten bereit ist.

Puffer

Die Daten werden zur weiteren Verarbeitung in einen Puffer eingelesen. Ein Entwickler kann sich im Puffer hin und her bewegen, was uns etwas mehr Flexibilität bei der Datenverarbeitung gibt. Gleichzeitig müssen wir prüfen, ob der Puffer die für eine korrekte Verarbeitung erforderliche Datenmenge enthält. Achten Sie außerdem beim Einlesen von Daten in einen Puffer darauf, dass Sie die vorhandenen Daten, die noch nicht verarbeitet wurden, nicht zerstören.


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

Grundlegende Eigenschaften eines Puffers:

Grundlegende Attribute
Kapazität Die Puffergröße, also die Länge des Arrays.
Position Die Ausgangslage für die Arbeit mit Daten.
Grenze Die Betriebsgrenze. Bei Lesevorgängen ist die Grenze die Datenmenge, die gelesen werden kann, bei Schreibvorgängen hingegen die Kapazität oder Quote, die zum Schreiben zur Verfügung steht.
markieren Der Index des Werts, auf den der Positionsparameter zurückgesetzt wird, wenn die Methode reset() aufgerufen wird.

Lassen Sie uns nun ein wenig über die Neuerungen in Java NIO.2 sprechen .

Weg

Pfad stellt einen Pfad im Dateisystem dar. Es enthält den Namen einer Datei und eine Liste von Verzeichnissen, die den Pfad zu dieser Datei definieren.


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

Paths ist eine sehr einfache Klasse mit einer einzigen statischen Methode: get() . Es wurde ausschließlich erstellt, um ein Path- Objekt aus der übergebenen Zeichenfolge oder URI abzurufen.


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

Dateien

Files ist eine Dienstprogrammklasse, mit der wir direkt die Größe einer Datei ermitteln, Dateien kopieren und vieles mehr können.


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

Dateisystem

FileSystem stellt eine Schnittstelle zum Dateisystem bereit. FileSystem funktioniert wie eine Fabrik zum Erstellen verschiedener Objekte (Weg,PathMatcher,Dateien). Es hilft uns, auf Dateien und andere Objekte im Dateisystem zuzugreifen.


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

Leistungstest

Für diesen Test nehmen wir zwei Dateien. Die erste ist eine kleine Textdatei und die zweite ist ein großes Video.

Wir erstellen eine Datei und fügen ein paar Wörter und Zeichen hinzu:

% touch text.txt

Unsere Datei belegt insgesamt 42 Bytes im Speicher:

Schreiben wir nun Code, der unsere Datei von einem Ordner in einen anderen kopiert. Testen wir es an kleinen und großen Dateien, um die Geschwindigkeit von IO und NIO und NIO.2 zu vergleichen .

Code zum Kopieren, geschrieben mit 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();
        }
    }

Und hier ist der Code für 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 für 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));
}

Beginnen wir mit der kleinen Datei.

Die Ausführungszeit für Java IO betrug durchschnittlich 1 Millisekunde. Durch mehrmaliges Durchführen des Tests erhalten wir Ergebnisse von 0 bis 2 Millisekunden.

Ausführungszeit in Millisekunden: 1

Die Ausführungszeit für Java NIO ist viel länger. Die durchschnittliche Zeit beträgt 11 Millisekunden. Die Ergebnisse lagen zwischen 9 und 16. Dies liegt daran, dass Java IO anders funktioniert als unser Betriebssystem. IO verschiebt und verarbeitet Dateien einzeln, aber das Betriebssystem sendet die Daten in einem großen Block. NIO hat eine schlechte Leistung erbracht, da es pufferorientiert und nicht streamorientiert wie IO ist .

Ausführungszeit in Millisekunden: 12

Und lassen Sie uns auch unseren Test für Java NIO.2 durchführen . NIO.2 hat im Vergleich zu Java NIO eine verbesserte Dateiverwaltung . Aus diesem Grund liefert die aktualisierte Bibliothek so unterschiedliche Ergebnisse:

Ausführungszeit in Millisekunden: 3

Versuchen wir nun, unsere große Datei, ein 521 MB großes Video, zu testen. Die Aufgabe wird genau dieselbe sein: Kopieren Sie die Datei in einen anderen Ordner. Sehen!

Ergebnisse für Java IO :

Ausführungszeit in Millisekunden: 1866

Und hier ist das Ergebnis für Java NIO :

Ausführungszeit in Millisekunden: 205

Java NIO verarbeitete die Datei im ersten Test neunmal schneller. Wiederholte Tests zeigten ungefähr die gleichen Ergebnisse.

Und wir werden unseren Test auch auf Java NIO.2 ausprobieren :

Ausführungszeit in Millisekunden: 360

Warum dieses Ergebnis? Ganz einfach, weil es für uns wenig Sinn macht, die Leistung zwischen ihnen zu vergleichen, da sie unterschiedlichen Zwecken dienen. NIO ist eine abstraktere Low-Level-I/O, während NIO.2 auf Dateiverwaltung ausgerichtet ist.

Zusammenfassung

Wir können mit Sicherheit sagen, dass Java NIO dank der Verwendung innerhalb von Blöcken bei der Arbeit mit Dateien deutlich effizienter ist. Ein weiterer Pluspunkt ist, dass die NIO- Bibliothek in zwei Teile unterteilt ist: einen für die Arbeit mit Dateien, einen anderen für die Arbeit mit dem Netzwerk.

Die neue API von Java NIO.2 für die Arbeit mit Dateien bietet viele nützliche Funktionen:

  • weitaus nützlichere Dateisystemadressierung mit Path ,

  • deutlich verbesserte Handhabung von ZIP-Dateien mithilfe eines benutzerdefinierten Dateisystemanbieters,

  • Zugriff auf spezielle Dateiattribute,

  • viele praktische Methoden, z. B. das Lesen einer gesamten Datei mit einer einzigen Anweisung, das Kopieren einer Datei mit einer einzigen Anweisung usw.

Es dreht sich alles um Dateien und Dateisysteme, und das alles auf einem ziemlich hohen Niveau.

Die Realität sieht heute so aus, dass Java NIO etwa 80–90 % der Arbeit mit Dateien ausmacht, obwohl der Anteil von Java IO immer noch erheblich ist.

💡 PS: Diese Tests wurden auf einem MacBook Pro 14" 16/512 durchgeführt. Die Testergebnisse können je nach Betriebssystem und Workstation-Spezifikationen abweichen.