Varför är Java IO så dåligt?

IO (Input & Output) API är ett Java API som gör det enkelt för utvecklare att arbeta med strömmar. Låt oss säga att vi får en del data (till exempel förnamn, mellannamn, efternamn) och vi behöver skriva det till en fil — det är dags att använda java.io .

Strukturen för java.io-biblioteket

Men Java IO har sina nackdelar, så låt oss prata om var och en av dem i tur och ordning:

  1. Blockerar åtkomst för input/output. Problemet är att när en utvecklare försöker läsa eller skriva något till en fil med Java IO , låser den filen och blockerar åtkomst till den tills jobbet är klart.
  2. Inget stöd för virtuella filsystem.
  3. Inget stöd för länkar.
  4. Massor av kontrollerade undantag.

Att arbeta med filer innebär alltid att man arbetar med undantag: att till exempel försöka skapa en ny fil som redan finns kommer att skapa en IOException . I det här fallet bör applikationen fortsätta att köras och användaren bör meddelas varför filen inte kunde skapas.


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

Här ser vi att createTempFile- metoden kastar ett IOException när filen inte kan skapas. Detta undantag måste hanteras på lämpligt sätt. Om vi ​​försöker anropa den här metoden utanför ett try-catch- block kommer kompilatorn att generera ett fel och föreslå två alternativ för att fixa det: packa in metoden i ett try-catch- block eller få metoden som anropar File.createTempFile att kasta en IOException ( så det kan hanteras på en högre nivå).

Anländer till Java NIO och hur det kan jämföras med Java IO

Java NIO , eller Java Non-Blocking I/O (eller ibland Java New I/O) är designad för högpresterande I/O-operationer.

Låt oss jämföra Java IO- metoder och de som ersätter dem.

Låt oss först prata om att arbeta med Java IO :

InputStream-klass


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

Klassen FileInputStream är till för att läsa data från en fil . Den ärver klassen InputStream och implementerar därför alla dess metoder. Om filen inte kan öppnas, kastas en FileNotFoundException .

OutputStream-klass


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 för att skriva byte till en fil. Den härrör från OutputStream -klassen.

Läsare och författare klasser

Klassen FileReader låter oss läsa teckendata från strömmar, och klassen FileWriter används för att skriva teckenströmmar. Följande kod visar hur man skriver och läser från 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();
        }

Låt oss nu prata om Java NIO :

Kanal

Till skillnad från strömmarna som används i Java IO är Channel tvåvägsgränssnitt, det vill säga den kan både läsa och skriva . En Java NIO- kanal stöder asynkront dataflöde i både blockerande och icke-blockerande lägen.


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

Här använde vi en FileChannel . Vi använder en filkanal för att läsa data från en fil. Ett filkanalobjekt kan bara skapas genom att anropa metoden getChannel() på ett filobjekt — det finns inget sätt att direkt skapa ett filkanalobjekt.

Utöver FileChannel har vi andra kanalimplementeringar:

  • FileChannel — för att arbeta med filer

  • DatagramChannel — en kanal för att arbeta över en UDP-anslutning

  • SocketChannel — en kanal för att arbeta över en TCP-anslutning

  • ServerSocketChannel innehåller en SocketChannel och liknar hur en webbserver fungerar

Observera: FileChannel kan inte växlas till icke-blockerande läge. Java NIO: s icke-blockerande läge låter dig begära läsdata från en kanal och bara ta emot det som är tillgängligt för närvarande (eller ingenting alls om det inte finns någon tillgänglig data ännu). Som sagt, SelectableChannel och dess implementeringar kan sättas i icke-blockerande läge med metoden connect() .

Väljare

Java NIO introducerade möjligheten att skapa en tråd som vet vilken kanal som är redo att skriva och läsa data och kan bearbeta just den kanalen. Denna förmåga implementeras medklassen Selector .

Ansluta kanaler till en väljare


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

Så vi skapar vår Selector och kopplar den till en SelectableChannel .

För att kunna användas med en väljare måste en kanal vara i icke-blockerande läge. Det betyder att du inte kan använda FileChannel med en väljare, eftersom FileChannel inte kan sättas i icke-blockerande läge. Men socket-kanaler kommer att fungera bra.

Låt oss här nämna att i vårt exempel är SelectionKey en uppsättning operationer som kan utföras på en kanal. Väljarknappen låter oss veta statusen för en kanal.

Typer av urvalsnyckel

  • SelectionKey.OP_CONNECT anger en kanal som är redo att ansluta till servern.

  • SelectionKey.OP_ACCEPT är en kanal som är redo att acceptera inkommande anslutningar.

  • SelectionKey.OP_READ betecknar en kanal som är redo att läsa data.

  • SelectionKey.OP_WRITE anger en kanal som är redo att skriva data.

Buffert

Data läses in i en buffert för vidare bearbetning. En utvecklare kan flytta fram och tillbaka på bufferten, vilket ger oss lite mer flexibilitet när vi behandlar data. Samtidigt måste vi kontrollera om bufferten innehåller den mängd data som krävs för korrekt behandling. När du läser data i en buffert ska du också vara säker på att du inte förstör befintliga data som ännu inte har bearbetats.


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äggande egenskaper för en buffert:

Grundläggande attribut
kapacitet Buffertstorleken, som är längden på arrayen.
placera Utgångspositionen för att arbeta med data.
begränsa Driftgränsen. För läsoperationer är gränsen mängden data som kan läsas, men för skrivoperationer är det kapaciteten eller kvoten som är tillgänglig för skrivning.
märke Indexet för värdet till vilket positionsparametern kommer att återställas när metoden reset() anropas.

Låt oss nu prata lite om vad som är nytt i Java NIO.2 .

Väg

Sökväg representerar en sökväg i filsystemet. Den innehåller namnet på en fil och en lista över kataloger som definierar sökvägen till den.


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

Paths är en mycket enkel klass med en enda statisk metod: get() . Det skapades enbart för att hämta ett sökvägsobjekt från den passerade strängen eller URI.


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

Filer

Filer är en verktygsklass som låter oss direkt få storleken på en fil, kopiera filer och mer.


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

Filsystem

FileSystem tillhandahåller ett gränssnitt till filsystemet. FileSystem fungerar som en fabrik för att skapa olika objekt (Väg,PathMatcher,Filer). Det hjälper oss att komma åt filer och andra objekt i filsystemet.


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

Utvärderingsprov

För detta test, låt oss ta två filer. Den första är en liten textfil och den andra är en stor video.

Vi skapar en fil och lägger till några ord och tecken:

% tryck på text.txt

Vår fil upptar totalt 42 byte i minnet:

Låt oss nu skriva kod som kopierar vår fil från en mapp till en annan. Låt oss testa det på små och stora filer för att jämföra hastigheten på IO och NIO och NIO.2 .

Kod för kopiering, skriven 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();
        }
    }

Och här är koden 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();
        }
    }

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

Låt oss börja med den lilla filen.

Exekveringstiden för Java IO var i genomsnitt 1 millisekund. Genom att köra testet flera gånger får vi resultat från 0 till 2 millisekunder.

Utförandetid i millisekunder: 1

Exekveringstiden för Java NIO är mycket längre. Medeltiden är 11 millisekunder. Resultaten varierade från 9 till 16. Detta beror på att Java IO fungerar annorlunda än vårt operativsystem. IO flyttar och bearbetar filer en efter en, men operativsystemet skickar data i en stor bit. NIO fungerade dåligt eftersom det är buffertorienterat, inte strömorienterat som IO .

Utförandetid i millisekunder: 12

Och låt oss också köra vårt test för Java NIO.2 . NIO.2 har förbättrad filhantering jämfört med Java NIO . Det är därför det uppdaterade biblioteket ger så olika resultat:

Utförandetid i millisekunder: 3

Låt oss nu testa vår stora fil, en 521 MB video. Uppgiften kommer att vara exakt densamma: kopiera filen till en annan mapp. Se!

Resultat för Java IO :

Exekveringstid i millisekunder: 1866

Och här är resultatet för Java NIO :

Utförandetid i millisekunder: 205

Java NIO hanterade filen 9 gånger snabbare i det första testet. Upprepade tester visade ungefär samma resultat.

Och vi kommer också att prova vårt test på Java NIO.2 :

Utförandetid i millisekunder: 360

Varför detta resultat? Helt enkelt för att det inte är så meningsfullt för oss att jämföra prestanda mellan dem, eftersom de tjänar olika syften. NIO är mer abstrakt lågnivå I/O, medan NIO.2 är inriktat på filhantering.

Sammanfattning

Vi kan lugnt säga att Java NIO är betydligt effektivare när man arbetar med filer tack vare användning inuti block. Ett annat plus är att NIO- biblioteket är uppdelat i två delar: en för att arbeta med filer, en annan för att arbeta med nätverket.

Java NIO.2: s nya API för att arbeta med filer erbjuder många användbara funktioner:

  • mycket mer användbar filsystemsadressering med hjälp av Path ,

  • avsevärt förbättrad hantering av ZIP-filer med hjälp av en anpassad filsystemleverantör,

  • tillgång till speciella filattribut,

  • många bekväma metoder, som att läsa en hel fil med ett enda uttalande, kopiera en fil med ett enda uttalande, etc.

Det handlar om filer och filsystem, och allt är på en ganska hög nivå.

Verkligheten idag är att Java NIO står för ungefär 80-90% av arbetet med filer, även om Java IO :s andel fortfarande är betydande.

💡 PS Dessa tester kördes på en MacBook Pro 14" 16/512. Testresultaten kan skilja sig beroende på operativsystem och arbetsstationsspecifikationer.