Waarom is Java IO zo slecht?

De IO (Input & Output) API is een Java API die het voor ontwikkelaars gemakkelijk maakt om met streams te werken. Laten we zeggen dat we wat gegevens ontvangen (bijvoorbeeld voornaam, middelste naam, achternaam) en we moeten deze naar een bestand schrijven - het is tijd om java.io te gebruiken .

Structuur van de java.io-bibliotheek

Maar Java IO heeft zijn nadelen, dus laten we ze allemaal achtereenvolgens bespreken:

  1. Toegang blokkeren voor invoer/uitvoer. Het probleem is dat wanneer een ontwikkelaar iets probeert te lezen of schrijven naar een bestand met behulp van Java IO , het bestand wordt vergrendeld en de toegang tot het bestand wordt geblokkeerd totdat de klus is geklaard.
  2. Geen ondersteuning voor virtuele bestandssystemen.
  3. Geen ondersteuning voor koppelingen.
  4. Heel veel gecontroleerde uitzonderingen.

Werken met bestanden betekent altijd werken met uitzonderingen: als u bijvoorbeeld probeert een nieuw bestand te maken dat al bestaat, wordt er een IOException gegenereerd . In dit geval moet de toepassing blijven draaien en moet de gebruiker worden geïnformeerd waarom het bestand niet kan worden gemaakt.


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 zien we dat de methode createTempFile een IOException genereert wanneer het bestand niet kan worden gemaakt. Met deze uitzondering moet op de juiste manier worden omgegaan. Als we deze methode buiten een try-catch- blok proberen aan te roepen, genereert de compiler een fout en stelt twee opties voor om het probleem op te lossen: verpak de methode in een try-catch- blok of laat de methode die File.createTempFile aanroept een IOException ( zodat het op een hoger niveau kan worden afgehandeld).

Aangekomen bij Java NIO en hoe het zich verhoudt tot Java IO

Java NIO of Java Non-Blocking I/O (of soms Java New I/O) is ontworpen voor hoogwaardige I/O-bewerkingen.

Laten we Java IO- methoden vergelijken met de methoden die deze vervangen.

Laten we het eerst hebben over het werken met Java IO :

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

De klasse FileInputStream is bedoeld voor het lezen van gegevens uit een bestand. Het erft de klasse InputStream en implementeert daarom al zijn methoden. Als het bestand niet kan worden geopend, wordt een FileNotFoundException gegenereerd.

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

De klasse FileOutputStream voor het schrijven van bytes naar een bestand. Het is afgeleid van de klasse OutputStream .

Lezers- en Schrijversklassen

Met de klasse FileReader kunnen we tekengegevens uit streams lezen en de klasse FileWriter wordt gebruikt om tekenstreams te schrijven. De volgende code laat zien hoe te schrijven en lezen van een bestand:


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

Laten we het nu hebben over Java NIO :

Kanaal

In tegenstelling tot de streams die in Java IO worden gebruikt , is Channel een tweerichtingsinterface, dat wil zeggen dat het zowel kan lezen als schrijven. Een Java NIO- kanaal ondersteunt asynchrone gegevensstroom in zowel blokkerende als niet-blokkerende modi.


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 hebben we een FileChannel gebruikt . We gebruiken een bestandskanaal om gegevens uit een bestand te lezen. Een bestandskanaalobject kan alleen worden gemaakt door de methode getChannel() op een bestandsobject aan te roepen — er is geen manier om rechtstreeks een bestandskanaalobject te maken.

Naast FileChannel hebben we nog andere kanaalimplementaties:

  • FileChannel — voor het werken met bestanden

  • DatagramChannel — een kanaal om via een UDP-verbinding te werken

  • SocketChannel — een kanaal om via een TCP-verbinding te werken

  • ServerSocketChannel bevat een SocketChannel en is vergelijkbaar met hoe een webserver werkt

Let op: FileChannel kan niet worden overgeschakeld naar de niet-blokkerende modus. Met de niet-blokkerende modus van Java NIO kunt u leesgegevens van een kanaal opvragen en alleen ontvangen wat momenteel beschikbaar is (of helemaal niets als er nog geen gegevens beschikbaar zijn). Dat gezegd hebbende, kunnen SelectableChannel en zijn implementaties in niet-blokkerende modus worden gezet met behulp van de methode connect() .

kiezer

Java NIO introduceerde de mogelijkheid om een ​​thread te maken die weet welk kanaal klaar is om gegevens te schrijven en te lezen en dat specifieke kanaal kan verwerken. Deze mogelijkheid wordt geïmplementeerd met behulp van de klasse Selector .

Kanalen verbinden met een selector


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

Dus we maken onze Selector en verbinden deze met een SelectableChannel .

Om te worden gebruikt met een selector, moet een kanaal in de niet-blokkerende modus staan. Dit betekent dat u FileChannel niet kunt gebruiken met een selector, omdat FileChannel niet in de niet-blokkerende modus kan worden gezet. Maar socket-kanalen zullen prima werken.

Laten we hier vermelden dat in ons voorbeeld SelectionKey een reeks bewerkingen is die op een kanaal kunnen worden uitgevoerd. De selectietoets laat ons de status van een kanaal weten.

Soorten selectiesleutel

  • SelectionKey.OP_CONNECT geeft een kanaal aan dat klaar is om verbinding te maken met de server.

  • SelectionKey.OP_ACCEPT is een kanaal dat klaar is om inkomende verbindingen te accepteren.

  • SelectionKey.OP_READ geeft een kanaal aan dat klaar is om gegevens te lezen.

  • SelectionKey.OP_WRITE geeft een kanaal aan dat klaar is om gegevens te schrijven.

Buffer

De gegevens worden ingelezen in een buffer voor verdere verwerking. Een ontwikkelaar kan heen en weer bewegen op de buffer, wat ons iets meer flexibiliteit geeft bij het verwerken van gegevens. Tegelijkertijd moeten we controleren of de buffer de hoeveelheid gegevens bevat die nodig is voor een correcte verwerking. Zorg er bij het inlezen van gegevens in een buffer ook voor dat u de bestaande gegevens die nog niet zijn verwerkt niet vernietigt.


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

Basiseigenschappen van een buffer:

Basisattributen
capaciteit De buffergrootte, de lengte van de array.
positie De startpositie voor het werken met data.
begrenzing De operationele limiet. Voor leesbewerkingen is de limiet de hoeveelheid gegevens die kan worden gelezen, maar voor schrijfbewerkingen is dit de capaciteit of het beschikbare quotum voor schrijven.
markering De index van de waarde waarnaar de parameter position wordt gereset wanneer de methode reset() wordt aangeroepen.

Laten we het nu eens hebben over wat er nieuw is in Java NIO.2 .

Pad

Pad vertegenwoordigt een pad in het bestandssysteem. Het bevat de naam van een bestand en een lijst met mappen die het pad ernaartoe definiëren.


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

Paths is een zeer eenvoudige klasse met een enkele statische methode: get() . Het is uitsluitend gemaakt om een ​​Path- object op te halen uit de doorgegeven tekenreeks of URI.


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

Bestanden

Bestanden is een hulpprogrammaklasse waarmee we direct de grootte van een bestand kunnen bepalen, bestanden kunnen kopiëren en meer.


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

Bestandssysteem

FileSystem biedt een interface naar het bestandssysteem. FileSystem werkt als een fabriek voor het maken van verschillende objecten (Pad,PathMatcher,Bestanden). Het helpt ons toegang te krijgen tot bestanden en andere objecten in het bestandssysteem.


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

Prestatie test

Laten we voor deze test twee bestanden nemen. De eerste is een klein tekstbestand en de tweede is een grote video.

We maken een bestand en voegen een paar woorden en tekens toe:

% raakt tekst.txt aan

Ons bestand neemt in totaal 42 bytes geheugen in beslag:

Laten we nu code schrijven die ons bestand van de ene map naar de andere zal kopiëren. Laten we het testen op de kleine en grote bestanden om de snelheid van IO en NIO en NIO.2 te vergelijken .

Code voor kopiëren, geschreven met 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();
        }
    }

En hier is de code voor 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 voor 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));
}

Laten we beginnen met het kleine bestand.

De uitvoeringstijd voor Java IO was gemiddeld 1 milliseconde. Door de test meerdere keren uit te voeren, krijgen we resultaten van 0 tot 2 milliseconden.

Uitvoeringstijd in milliseconden: 1

De uitvoeringstijd voor Java NIO is veel langer. De gemiddelde tijd is 11 milliseconden. De resultaten varieerden van 9 tot 16. Dit komt doordat Java IO anders werkt dan ons besturingssysteem. IO verplaatst en verwerkt bestanden één voor één, maar het besturingssysteem verzendt de gegevens in één groot stuk. NIO presteerde slecht omdat het buffergericht is, niet stroomgericht zoals IO .

Uitvoeringstijd in milliseconden: 12

En laten we ook onze test uitvoeren voor Java NIO.2 . NIO.2 heeft verbeterd bestandsbeheer in vergelijking met Java NIO . Dit is de reden waarom de bijgewerkte bibliotheek zulke verschillende resultaten oplevert:

Uitvoeringstijd in milliseconden: 3

Laten we nu proberen ons grote bestand, een video van 521 MB, te testen. De taak is precies hetzelfde: kopieer het bestand naar een andere map. Kijk!

Resultaten voor Java IO :

Uitvoeringstijd in milliseconden: 1866

En hier is het resultaat voor Java NIO :

Uitvoeringstijd in milliseconden: 205

Java NIO behandelde het bestand 9 keer sneller in de eerste test. Herhaalde tests lieten ongeveer dezelfde resultaten zien.

En we zullen onze test ook proberen op Java NIO.2 :

Uitvoeringstijd in milliseconden: 360

Waarom dit resultaat? Simpelweg omdat het voor ons weinig zin heeft om de prestaties tussen beide te vergelijken, omdat ze verschillende doelen dienen. NIO is meer abstracte low-level I/O, terwijl NIO.2 gericht is op bestandsbeheer.

Samenvatting

We kunnen gerust zeggen dat Java NIO aanzienlijk efficiënter is bij het werken met bestanden dankzij het gebruik in blokken. Een ander pluspunt is dat de NIO- bibliotheek in twee delen is verdeeld: een voor het werken met bestanden, een ander voor het werken met het netwerk.

De nieuwe API van Java NIO.2 voor het werken met bestanden biedt veel handige functies:

  • veel nuttiger bestandssysteemadressering met behulp van Path ,

  • aanzienlijk verbeterde verwerking van ZIP-bestanden met behulp van een aangepaste bestandssysteemprovider,

  • toegang tot speciale bestandskenmerken,

  • vele handige methodes, zoals het lezen van een heel bestand met een enkel statement, het kopiëren van een bestand met een enkel statement, etc.

Het draait allemaal om bestanden en bestandssystemen, en het is allemaal van behoorlijk hoog niveau.

De realiteit van vandaag is dat Java NIO ongeveer 80-90% van het werk met bestanden voor zijn rekening neemt, hoewel het aandeel van Java IO nog steeds aanzienlijk is.

💡 PS Deze tests zijn uitgevoerd op een MacBook Pro 14" 16/512. Testresultaten kunnen verschillen op basis van het besturingssysteem en de specificaties van het werkstation.