De ce este Java IO atât de rău?

API-ul IO (Input & Output) este un API Java care facilitează lucrul dezvoltatorilor cu fluxurile. Să presupunem că primim câteva date (de exemplu, prenume, al doilea nume, prenume) și trebuie să le scriem într-un fișier — a sosit momentul să folosim java.io .

Structura bibliotecii java.io

Dar Java IO are dezavantajele sale, așa că haideți să vorbim despre fiecare dintre ele pe rând:

  1. Blocarea accesului pentru intrare/ieșire. Problema este că atunci când un dezvoltator încearcă să citească sau să scrie ceva într-un fișier folosind Java IO , acesta blochează fișierul și blochează accesul la el până când lucrarea este terminată.
  2. Nu există suport pentru sistemele de fișiere virtuale.
  3. Nu există suport pentru link-uri.
  4. O mulțime și o mulțime de excepții verificate.

Lucrul cu fișiere implică întotdeauna lucrul cu excepții: de exemplu, încercarea de a crea un fișier nou care există deja va genera o IOException . În acest caz, aplicația ar trebui să continue să ruleze și utilizatorul ar trebui să fie notificat de ce fișierul nu a putut fi creat.


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

Aici vedem că metoda createTempFile aruncă o IOException atunci când fișierul nu poate fi creat. Această excepție trebuie tratată în mod corespunzător. Dacă încercăm să apelăm această metodă în afara unui bloc try-catch , compilatorul va genera o eroare și va sugera două opțiuni pentru remedierea acesteia: înfășurați metoda într-un bloc try-catch sau face ca metoda care apelează File.createTempFile să arunce o IOException ( deci poate fi manevrat la un nivel superior).

Sosirea la Java NIO și cum se compară cu Java IO

Java NIO sau Java Non-Blocking I/O (sau uneori Java New I/O) este proiectat pentru operațiuni I/O de înaltă performanță.

Să comparăm metodele Java IO și cele care le înlocuiesc.

Mai întâi, să vorbim despre lucrul cu Java IO :

Clasa InputStream


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

Clasa FileInputStream este pentru citirea datelor dintr-un fișier. Moștenește clasa InputStream și, prin urmare, implementează toate metodele acesteia. Dacă fișierul nu poate fi deschis, este lansată o excepție FileNotFoundException .

Clasa OutputStream


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

Clasa FileOutputStream pentru scrierea de octeți într - un fișier. Derivă din clasa OutputStream .

Cursuri de cititor și scriitor

Clasa FileReader ne permite să citim date de caractere din fluxuri, iar clasa FileWriter este folosită pentru a scrie fluxuri de caractere. Următorul cod arată cum să scrieți și să citiți dintr-un fișier:


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

Acum să vorbim despre Java NIO :

Canal

Spre deosebire de fluxurile utilizate în Java IO , Channel are o interfață bidirecțională, adică poate atât să citească, cât și să scrie. Un canal Java NIO acceptă fluxul de date asincron în ambele moduri de blocare și neblocare.


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

Aici am folosit un FileChannel . Folosim un canal de fișiere pentru a citi datele dintr-un fișier. Un obiect de canal de fișiere poate fi creat doar apelând metoda getChannel() pe un obiect de fișier - nu există nicio modalitate de a crea direct un obiect de canal de fișier.

Pe lângă FileChannel , avem și alte implementări de canal:

  • FileChannel — pentru lucrul cu fișiere

  • DatagramChannel — un canal pentru lucrul printr-o conexiune UDP

  • SocketChannel — un canal pentru lucrul printr-o conexiune TCP

  • ServerSocketChannel conține un SocketChannel și este similar cu modul în care funcționează un server web

Vă rugăm să rețineți: FileChannel nu poate fi comutat în modul fără blocare. Modul de non-blocare al Java NIO vă permite să solicitați citirea datelor de la un canal și să primiți doar ceea ce este disponibil în prezent (sau nimic dacă nu există încă date disponibile). Acestea fiind spuse, SelectableChannel și implementările sale pot fi puse în modul neblocant folosind metoda connect() .

Selector

Java NIO a introdus capacitatea de a crea un fir care știe care canal este gata să scrie și să citească date și poate procesa acel canal anume. Această abilitate este implementată folosind clasa Selector .

Conectarea canalelor la un selector


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

Așa că ne creăm Selectorul și îl conectăm la un SelectableChannel .

Pentru a fi utilizat cu un selector, un canal trebuie să fie în modul neblocant. Aceasta înseamnă că nu puteți utiliza FileChannel cu un selector, deoarece FileChannel nu poate fi pus în modul neblocant. Dar canalele prize vor funcționa bine.

Aici să menționăm că în exemplul nostru SelectionKey este un set de operații care pot fi efectuate pe un canal. Tasta de selecție ne informează despre starea unui canal.

Tipuri de SelectionKey

  • SelectionKey.OP_CONNECT înseamnă un canal care este gata să se conecteze la server.

  • SelectionKey.OP_ACCEPT este un canal care este gata să accepte conexiuni de intrare.

  • SelectionKey.OP_READ înseamnă un canal care este gata să citească date.

  • SelectionKey.OP_WRITE înseamnă un canal care este gata să scrie date.

Tampon

Datele sunt citite într-un buffer pentru procesare ulterioară. Un dezvoltator se poate mișca înainte și înapoi pe buffer, ceea ce ne oferă puțin mai multă flexibilitate atunci când procesăm datele. În același timp, trebuie să verificăm dacă bufferul conține cantitatea de date necesară procesării corecte. De asemenea, atunci când citiți date într-un buffer, asigurați-vă că nu distrugeți datele existente care nu au fost încă procesate.


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

Proprietățile de bază ale unui tampon:

Atribute de bază
capacitate Dimensiunea bufferului, care este lungimea matricei.
poziţie Poziția de pornire pentru lucrul cu date.
limită Limita de operare. Pentru operațiunile de citire, limita este cantitatea de date care poate fi citită, dar pentru operațiunile de scriere, este capacitatea sau cota disponibilă pentru scriere.
marcă Indicele valorii la care va fi resetat parametrul de poziție atunci când este apelată metoda reset() .

Acum să vorbim puțin despre ce este nou în Java NIO.2 .

cale

Cale reprezintă o cale în sistemul de fișiere. Conține numele unui fișier și o listă de directoare care definesc calea către acesta.


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

Paths este o clasă foarte simplă cu o singură metodă statică: get() . A fost creat doar pentru a obține un obiect Path din șirul sau URI-ul transmis.


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

Fișiere

Files este o clasă de utilitate care ne permite să obținem direct dimensiunea unui fișier, să copiem fișiere și multe altele.


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

Sistemul de fișiere

FileSystem oferă o interfață cu sistemul de fișiere. FileSystem funcționează ca o fabrică pentru crearea diferitelor obiecte (cale,PathMatcher,Fișiere). Ne ajută să accesăm fișiere și alte obiecte din sistemul de fișiere.


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

Test de performanță

Pentru acest test, să luăm două fișiere. Primul este un fișier text mic, iar al doilea este un videoclip mare.

Vom crea un fișier și vom adăuga câteva cuvinte și caractere:

% atinge text.txt

Fișierul nostru ocupă un total de 42 de octeți în memorie:

Acum să scriem cod care va copia fișierul nostru dintr-un folder în altul. Să-l testăm pe fișierele mici și mari pentru a compara viteza IO și NIO și NIO.2 .

Cod pentru copiere, scris folosind 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();
        }
    }

Și iată codul pentru 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();
        }
    }

Cod pentru 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));
}

Să începem cu fișierul mic.

Timpul de execuție pentru Java IO a fost în medie de 1 milisecundă. Prin rularea testului de mai multe ori, obținem rezultate de la 0 la 2 milisecunde.

Timp de execuție în milisecunde: 1

Timpul de execuție pentru Java NIO este mult mai lung. Timpul mediu este de 11 milisecunde. Rezultatele au variat de la 9 la 16. Acest lucru se datorează faptului că Java IO funcționează diferit față de sistemul nostru de operare. IO mută și procesează fișierele unul câte unul, dar sistemul de operare trimite datele într-o bucată mare. NIO a avut rezultate slabe, deoarece este orientat pe buffer, nu pe flux, cum ar fi IO .

Timp de execuție în milisecunde: 12

Și să rulăm și testul nostru pentru Java NIO.2 . NIO.2 a îmbunătățit gestionarea fișierelor în comparație cu Java NIO . Acesta este motivul pentru care biblioteca actualizată produce rezultate atât de diferite:

Timp de execuție în milisecunde: 3

Acum să încercăm să testăm fișierul nostru mare, un videoclip de 521 MB. Sarcina va fi exact aceeași: copiați fișierul într-un alt folder. Uite!

Rezultate pentru Java IO :

Timp de execuție în milisecunde: 1866

Și iată rezultatul pentru Java NIO :

Timp de execuție în milisecunde: 205

Java NIO a gestionat fișierul de 9 ori mai rapid la primul test. Testele repetate au arătat aproximativ aceleași rezultate.

Și vom încerca și testul nostru pe Java NIO.2 :

Timp de execuție în milisecunde: 360

De ce acest rezultat? Pur și simplu pentru că nu are prea mult sens să comparăm performanța între ele, deoarece servesc unor scopuri diferite. NIO este mai abstract I/O de nivel scăzut, în timp ce NIO.2 este orientat spre managementul fișierelor.

rezumat

Putem spune cu siguranță că Java NIO este semnificativ mai eficient atunci când lucrați cu fișiere datorită utilizării în interiorul blocurilor. Un alt plus este că biblioteca NIO este împărțită în două părți: una pentru lucrul cu fișiere, alta pentru lucrul cu rețeaua.

Noul API Java NIO.2 pentru lucrul cu fișiere oferă multe caracteristici utile:

  • Adresarea sistemului de fișiere mult mai utilă folosind Path ,

  • gestionarea îmbunătățită semnificativ a fișierelor ZIP folosind un furnizor de sistem de fișiere personalizat,

  • acces la atributele speciale ale fișierului,

  • multe metode convenabile, cum ar fi citirea unui întreg fișier cu o singură instrucțiune, copierea unui fișier cu o singură instrucțiune etc.

Totul este despre fișiere și sisteme de fișiere și totul este la un nivel destul de înalt.

Realitatea de astăzi este că Java NIO reprezintă aproximativ 80-90% din munca cu fișiere, deși cota Java IO este încă semnificativă.

💡 PS Aceste teste au fost efectuate pe un MacBook Pro 14" 16/512. Rezultatele testelor pot diferi în funcție de sistemul de operare și specificațiile stației de lucru.