Hvorfor er Java IO så dårlig?

IO (Input & Output) API er et Java API som gjør det enkelt for utviklere å jobbe med strømmer. La oss si at vi mottar noen data (for eksempel fornavn, mellomnavn, etternavn) og vi må skrive det til en fil — tiden er inne for å bruke java.io .

Strukturen til java.io-biblioteket

Men Java IO har sine ulemper, så la oss snakke om hver av dem etter tur:

  1. Blokkering av tilgang for input/output. Problemet er at når en utvikler prøver å lese eller skrive noe til en fil ved hjelp av Java IO , låser den filen og blokkerer tilgangen til den til jobben er gjort.
  2. Ingen støtte for virtuelle filsystemer.
  3. Ingen støtte for lenker.
  4. Massevis av sjekkede unntak.

Å jobbe med filer innebærer alltid å jobbe med unntak: for eksempel å prøve å lage en ny fil som allerede eksisterer vil gi en IOException . I dette tilfellet skal applikasjonen fortsette å kjøre og brukeren skal få beskjed om hvorfor filen ikke kunne opprettes.


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

Her ser vi at createTempFile -metoden kaster et IOException når filen ikke kan opprettes. Dette unntaket må håndteres hensiktsmessig. Hvis vi prøver å kalle denne metoden utenfor en try-catch- blokk, vil kompilatoren generere en feil og foreslå to alternativer for å fikse den: pakk metoden inn i en try-catch- blokk eller få metoden som kaller File.createTempFile til å kaste en IOException ( slik at det kan håndteres på et høyere nivå).

Ankomst til Java NIO og hvordan det kan sammenlignes med Java IO

Java NIO , eller Java Non-Blocking I/O (eller noen ganger Java New I/O) er designet for I/O-operasjoner med høy ytelse.

La oss sammenligne Java IO- metoder og de som erstatter dem.

La oss først snakke om å jobbe med Java IO :

InputStream-klassen


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

FileInputStream - klassen er for å lese data fra en fil. Den arver InputStream -klassen og implementerer derfor alle metodene. Hvis filen ikke kan åpnes, blir en FileNotFoundException kastet.

OutputStream-klassen


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

FileOutputStream - klassen for å skrive byte til en fil. Den stammer fra OutputStream -klassen.

Leser- og forfatterkurs

FileReader - klassen lar oss lese tegndata fra strømmer, og FileWriter- klassen brukes til å skrive karakterstrømmer. Følgende kode viser hvordan du skriver og leser fra en fil:


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

La oss nå snakke om Java NIO :

Kanal

I motsetning til strømmene som brukes i Java IO , er Channel toveis grensesnitt, det vil si at den kan både lese og skrive. En Java NIO- kanal støtter asynkron dataflyt i både blokkerende og ikke-blokkerende 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();

Her brukte vi en FileChannel . Vi bruker en filkanal for å lese data fra en fil. Et filkanalobjekt kan bare opprettes ved å kalle getChannel()- metoden på et filobjekt - det er ingen måte å opprette et filkanalobjekt direkte.

I tillegg til FileChannel har vi andre kanalimplementeringer:

  • FileChannel — for arbeid med filer

  • DatagramChannel — en kanal for å jobbe over en UDP-tilkobling

  • SocketChannel — en kanal for arbeid over en TCP-tilkobling

  • ServerSocketChannel inneholder en SocketChannel og ligner på hvordan en webserver fungerer

Vennligst merk: FileChannel kan ikke byttes til ikke-blokkerende modus. Java NIOs ikke-blokkerende modus lar deg be om lesedata fra en kanal og motta bare det som er tilgjengelig for øyeblikket (eller ingenting i det hele tatt hvis det ikke er tilgjengelige data ennå). Når det er sagt, kan SelectableChannel og dens implementeringer settes i ikke-blokkerende modus ved å bruke connect()- metoden.

Velger

Java NIO introduserte muligheten til å lage en tråd som vet hvilken kanal som er klar til å skrive og lese data og kan behandle den aktuelle kanalen. Denne evnen implementeres ved hjelp av Selector -klassen.

Koble kanaler til en velger


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

Så vi lager vår Selector og kobler den til en SelectableChannel .

For å kunne brukes med en velger, må en kanal være i ikke-blokkerende modus. Dette betyr at du ikke kan bruke FileChannel med en velger, siden FileChannel ikke kan settes i ikke-blokkerende modus. Men socket-kanaler vil fungere fint.

La oss her nevne at i vårt eksempel er SelectionKey et sett med operasjoner som kan utføres på en kanal. Valgtasten forteller oss statusen til en kanal.

Typer utvalgsnøkler

  • SelectionKey.OP_CONNECT betyr en kanal som er klar til å koble til serveren.

  • SelectionKey.OP_ACCEPT er en kanal som er klar til å akseptere innkommende tilkoblinger.

  • SelectionKey.OP_READ betyr en kanal som er klar til å lese data.

  • SelectionKey.OP_WRITE betyr en kanal som er klar til å skrive data.

Buffer

Dataene leses inn i en buffer for videre behandling. En utvikler kan bevege seg frem og tilbake på bufferen, noe som gir oss litt mer fleksibilitet når vi behandler data. Samtidig må vi sjekke om bufferen inneholder mengden data som kreves for korrekt behandling. Når du leser data inn i en buffer, må du også passe på at du ikke ødelegger eksisterende data som ennå ikke er behandlet.


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

Grunnleggende egenskaper for en buffer:

Grunnleggende attributter
kapasitet Bufferstørrelsen, som er lengden på matrisen.
posisjon Utgangsposisjonen for arbeid med data.
grense Driftsgrensen. For leseoperasjoner er grensen mengden data som kan leses, men for skriveoperasjoner er det kapasiteten eller kvoten som er tilgjengelig for skriving.
merke Indeksen for verdien som posisjonsparameteren vil bli tilbakestilt til når reset() -metoden kalles.

La oss nå snakke litt om hva som er nytt i Java NIO.2 .

Sti

Bane representerer en bane i filsystemet. Den inneholder navnet på en fil og en liste over kataloger som definerer banen til den.


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

Paths er en veldig enkel klasse med en enkelt statisk metode: get() . Den ble opprettet utelukkende for å hente et Path- objekt fra den passerte strengen eller URI.


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

Filer

Files er en verktøyklasse som lar oss direkte få størrelsen på en fil, kopiere filer og mer.


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

Filsystem

FileSystem gir et grensesnitt til filsystemet. FileSystem fungerer som en fabrikk for å lage forskjellige objekter (Sti,PathMatcher,Filer). Det hjelper oss med å få tilgang til filer og andre objekter i filsystemet.


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

Ytelsestest

For denne testen, la oss ta to filer. Den første er en liten tekstfil, og den andre er en stor video.

Vi lager en fil og legger til noen få ord og tegn:

% trykk på text.txt

Filen vår opptar totalt 42 byte i minnet:

La oss nå skrive kode som vil kopiere filen vår fra en mappe til en annen. La oss teste det på små og store filer for å sammenligne hastigheten til IO og NIO og NIO.2 .

Kode for kopiering, skrevet med 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();
        }
    }

Og her er koden for 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();
        }
    }

Kode for 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));
}

La oss starte med den lille filen.

Utførelsestiden for Java IO var 1 millisekund i gjennomsnitt. Ved å kjøre testen flere ganger får vi resultater fra 0 til 2 millisekunder.

Utførelsestid i millisekunder: 1

Utførelsestiden for Java NIO er mye lengre. Gjennomsnittlig tid er 11 millisekunder. Resultatene varierte fra 9 til 16. Dette er fordi Java IO fungerer annerledes enn vårt operativsystem. IO flytter og behandler filer én etter én, men operativsystemet sender dataene i én stor del. NIO presterte dårlig fordi den er bufferorientert, ikke strømorientert som IO .

Utførelsestid i millisekunder: 12

Og la oss også kjøre testen vår for Java NIO.2 . NIO.2 har forbedret filbehandling sammenlignet med Java NIO . Dette er grunnen til at det oppdaterte biblioteket gir så forskjellige resultater:

Utførelsestid i millisekunder: 3

La oss nå prøve å teste den store filen vår, en 521 MB video. Oppgaven vil være nøyaktig den samme: kopier filen til en annen mappe. Se!

Resultater for Java IO :

Utførelsestid i millisekunder: 1866

Og her er resultatet for Java NIO :

Utførelsestid i millisekunder: 205

Java NIO håndterte filen 9 ganger raskere i den første testen. Gjentatte tester viste omtrent de samme resultatene.

Og vi vil også prøve testen vår på Java NIO.2 :

Utførelsestid i millisekunder: 360

Hvorfor dette resultatet? Rett og slett fordi det ikke gir mye mening for oss å sammenligne ytelsen mellom dem, siden de tjener forskjellige formål. NIO er mer abstrakt lav-nivå I/O, mens NIO.2 er orientert mot filbehandling.

Sammendrag

Vi kan trygt si at Java NIO er betydelig mer effektiv når du arbeider med filer takket være bruk inne i blokker. Et annet pluss er at NIO- biblioteket er delt i to deler: en for arbeid med filer, en annen for arbeid med nettverket.

Java NIO.2s nye API for arbeid med filer tilbyr mange nyttige funksjoner:

  • langt mer nyttig filsystemadressering ved å bruke Path ,

  • betydelig forbedret håndtering av ZIP-filer ved å bruke en tilpasset filsystemleverandør,

  • tilgang til spesielle filattributter,

  • mange praktiske metoder, som å lese en hel fil med en enkelt setning, kopiere en fil med en enkelt setning, etc.

Det handler om filer og filsystemer, og det hele er på et ganske høyt nivå.

Realiteten i dag er at Java NIO står for omtrent 80-90 % av arbeidet med filer, selv om Java IO sin andel fortsatt er betydelig.

💡 PS Disse testene ble kjørt på en MacBook Pro 14" 16/512. Testresultatene kan variere basert på operativsystemet og arbeidsstasjonsspesifikasjonene.