Hvorfor er Java IO så dårlig?

IO (Input & Output) API er en Java API, der gør det nemt for udviklere at arbejde med streams. Lad os sige, at vi modtager nogle data (for eksempel fornavn, mellemnavn, efternavn), og vi skal skrive det til en fil — tiden er inde til at bruge java.io .

Strukturen af ​​java.io-biblioteket

Men Java IO har sine ulemper, så lad os tale om hver af dem på skift:

  1. Blokering af adgang til input/output. Problemet er, at når en udvikler forsøger at læse eller skrive noget til en fil ved hjælp af Java IO , låser den filen og blokerer adgangen til den, indtil jobbet er udført.
  2. Ingen understøttelse af virtuelle filsystemer.
  3. Ingen understøttelse af links.
  4. Masser og masser af kontrollerede undtagelser.

Arbejde med filer indebærer altid arbejde med undtagelser: For eksempel vil forsøg på at oprette en ny fil, der allerede eksisterer, kaste en IOException . I dette tilfælde skal applikationen fortsætte med at køre, og brugeren skal have besked om, hvorfor filen ikke kunne oprettes.


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 en IOException , når filen ikke kan oprettes. Denne undtagelse skal håndteres korrekt. Hvis vi forsøger at kalde denne metode uden for en try-catch- blok, vil compileren generere en fejl og foreslå to muligheder for at rette den: pak metoden ind i en try-catch- blok eller få metoden, der kalder File.createTempFile , til at kaste en IOException ( så det kan håndteres på et højere niveau).

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

Java NIO eller Java Non-Blocking I/O (eller nogle gange Java New I/O) er designet til højtydende I/O-operationer.

Lad os sammenligne Java IO -metoder og dem, der erstatter dem.

Lad os først tale om at arbejde med 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());
}

FileInputStream - klassen er til læsning af data fra en fil. Den arver InputStream -klassen og implementerer derfor alle dens metoder. Hvis filen ikke kan åbnes, vises en FileNotFoundException .

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

FileOutputStream - klassen til at skrive bytes til en fil. Det stammer fra OutputStream -klassen.

Læser- og forfatterhold

FileReader - klassen lader os læse karakterdata fra streams, og FileWriter- klassen bruges til at skrive karakterstrømme. Følgende kode viser, hvordan man skriver og læser 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();
        }

Lad os nu tale om Java NIO :

Kanal

I modsætning til de streams, der bruges i Java IO , er Channel to-vejs grænseflade, det vil sige, den kan både læse og skrive. En Java NIO- kanal understøtter asynkront dataflow i både blokerende og ikke-blokerende tilstande.


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 brugte vi en FileChannel . Vi bruger en filkanal til at læse data fra en fil. Et filkanalobjekt kan kun oprettes ved at kalde metoden getChannel() på et filobjekt - der er ingen måde at oprette et filkanalobjekt direkte på.

Ud over FileChannel har vi andre kanalimplementeringer:

  • FileChannel — til at arbejde med filer

  • DatagramChannel — en kanal til at arbejde over en UDP-forbindelse

  • SocketChannel — en kanal til at arbejde over en TCP-forbindelse

  • ServerSocketChannel indeholder en SocketChannel og ligner, hvordan en webserver fungerer

Bemærk venligst: FileChannel kan ikke skiftes til ikke-blokerende tilstand. Java NIO 's ikke-blokerende tilstand giver dig mulighed for at anmode om læsedata fra en kanal og kun modtage det, der er tilgængeligt i øjeblikket (eller slet intet, hvis der endnu ikke er tilgængelige data). Når det er sagt, kan SelectableChannel og dens implementeringer sættes i ikke-blokerende tilstand ved hjælp af connect() metoden.

Vælger

Java NIO introducerede muligheden for at skabe en tråd, der ved, hvilken kanal der er klar til at skrive og læse data og kan behandle den pågældende kanal. Denne evne implementeres ved hjælp af Selector -klassen.

Tilslutning af kanaler til en vælger


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

Så vi opretter vores Selector og forbinder den med en SelectableChannel .

For at blive brugt med en vælger, skal en kanal være i ikke-blokerende tilstand. Det betyder, at du ikke kan bruge FileChannel med en vælger, da FileChannel ikke kan sættes i ikke-blokerende tilstand. Men socket-kanaler vil fungere fint.

Lad os her nævne, at i vores eksempel er SelectionKey et sæt handlinger, der kan udføres på en kanal. Valgtasten fortæller os status for en kanal.

Typer af Selection Key

  • SelectionKey.OP_CONNECT angiver en kanal, der er klar til at oprette forbindelse til serveren.

  • SelectionKey.OP_ACCEPT er en kanal, der er klar til at acceptere indgående forbindelser.

  • SelectionKey.OP_READ angiver en kanal, der er klar til at læse data.

  • SelectionKey.OP_WRITE angiver en kanal, der er klar til at skrive data.

Buffer

Dataene læses ind i en buffer til videre behandling. En udvikler kan bevæge sig frem og tilbage på bufferen, hvilket giver os lidt mere fleksibilitet, når vi behandler data. Samtidig skal vi kontrollere, om bufferen indeholder den mængde data, der kræves for korrekt behandling. Når du læser data ind i en buffer, skal du også være sikker på, at du ikke ødelægger de eksisterende data, der endnu ikke er blevet 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

Grundlæggende egenskaber for en buffer:

Grundlæggende egenskaber
kapacitet Bufferstørrelsen, som er længden af ​​arrayet.
position Udgangspositionen for at arbejde med data.
begrænse Driftsgrænsen. For læseoperationer er grænsen mængden af ​​data, der kan læses, men for skriveoperationer er det den kapacitet eller kvote, der er tilgængelig for skrivning.
mærke Indekset for den værdi, som positionsparameteren nulstilles til, når reset() -metoden kaldes.

Lad os nu tale lidt om, hvad der er nyt i Java NIO.2 .

Sti

Sti repræsenterer en sti i filsystemet. Den indeholder navnet på en fil og en liste over mapper, der definerer stien til den.


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

Stier er en meget simpel klasse med en enkelt statisk metode: get() . Det blev udelukkende oprettet for at hente et stiobjekt fra den beståede streng eller URI.


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

Filer

Filer er en hjælpeklasse, der lader os direkte få størrelsen på en fil, kopiere filer og mere.


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

Filsystem

FileSystem giver en grænseflade til filsystemet. FileSystem fungerer som en fabrik til at skabe forskellige objekter (Sti,PathMatcher,Filer). Det hjælper os med at få adgang 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();
  }

Præstationstest

Til denne test, lad os tage to filer. Den første er en lille tekstfil, og den anden er en stor video.

Vi opretter en fil og tilføjer et par ord og tegn:

% tryk på text.txt

Vores fil optager i alt 42 bytes i hukommelsen:

Lad os nu skrive kode, der kopierer vores fil fra en mappe til en anden. Lad os teste det på de små og store filer for at sammenligne hastigheden af ​​IO og NIO og NIO.2 .

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

Lad os starte med den lille fil.

Udførelsestiden for Java IO var 1 millisekund i gennemsnit. Ved at køre testen flere gange får vi resultater fra 0 til 2 millisekunder.

Udførelsestid i millisekunder: 1

Udførelsestiden for Java NIO er meget længere. Den gennemsnitlige tid er 11 millisekunder. Resultaterne varierede fra 9 til 16. Dette skyldes, at Java IO fungerer anderledes end vores operativsystem. IO flytter og behandler filer én efter én, men operativsystemet sender dataene i én stor del. NIO klarede sig dårligt, fordi det er buffer-orienteret, ikke stream-orienteret som IO .

Udførelsestid i millisekunder: 12

Og lad os også køre vores test for Java NIO.2 . NIO.2 har forbedret filhåndtering sammenlignet med Java NIO . Det er derfor, det opdaterede bibliotek producerer så forskellige resultater:

Udførelsestid i millisekunder: 3

Lad os nu prøve at teste vores store fil, en 521 MB video. Opgaven vil være nøjagtig den samme: Kopier filen til en anden mappe. Se!

Resultater for Java IO :

Udførelsestid i millisekunder: 1866

Og her er resultatet for Java NIO :

Udførelsestid i millisekunder: 205

Java NIO håndterede filen 9 gange hurtigere i den første test. Gentagne test viste omtrent de samme resultater.

Og vi vil også prøve vores test på Java NIO.2 :

Udførelsestid i millisekunder: 360

Hvorfor dette resultat? Simpelthen fordi det ikke giver meget mening for os at sammenligne ydeevne mellem dem, da de tjener forskellige formål. NIO er mere abstrakt lav-niveau I/O, mens NIO.2 er orienteret mod filhåndtering.

Resumé

Vi kan roligt sige, at Java NIO er betydeligt mere effektiv, når man arbejder med filer takket være brugen inde i blokke. Et andet plus er, at NIO- biblioteket er opdelt i to dele: en til at arbejde med filer, en anden til at arbejde med netværket.

Java NIO.2s nye API til at arbejde med filer tilbyder mange nyttige funktioner:

  • langt mere nyttig filsystemadressering ved hjælp af Path ,

  • markant forbedret håndtering af ZIP-filer ved hjælp af en brugerdefineret filsystemudbyder,

  • adgang til særlige filattributter,

  • mange praktiske metoder, såsom at læse en hel fil med en enkelt sætning, kopiere en fil med en enkelt sætning osv.

Det handler om filer og filsystemer, og det hele er på et ret højt niveau.

Virkeligheden i dag er, at Java NIO står for omkring 80-90% af arbejdet med filer, selvom Java IO 's andel stadig er betydelig.

💡 PS Disse test blev kørt på en MacBook Pro 14" 16/512. Testresultaterne kan variere afhængigt af operativsystemet og arbejdsstationens specifikationer.