Mai devreme, am făcut cunoștință cu IO API (Input/Output Application Programming Interface) și pachetul java.io , ale căror clase sunt în principal pentru lucrul cu fluxuri în Java. Cheia aici este conceptul de flux .

Astăzi vom începe să luăm în considerare API-ul NIO (New Input/Output).

Principala diferență dintre cele două abordări ale I/O este că API-ul IO este orientat spre flux, în timp ce API-ul NIO este orientat către buffer. Deci, principalele concepte de înțeles sunt bufferele și canalele .

Ce este un buffer și ce este un canal?

Un canal este un portal logic prin care datele se deplasează înăuntru și în afara, în timp ce un buffer este sursa sau destinația acestor date transmise. În timpul ieșirii, datele pe care doriți să le trimiteți sunt introduse într-un buffer, iar tamponul transmite datele către canal. În timpul introducerii, datele de pe canal sunt introduse în buffer.

Cu alte cuvinte:

  • un buffer este pur și simplu un bloc de memorie în care putem scrie informații și din care putem citi informații,
  • un canal este un gateway care oferă acces la dispozitive I/O, cum ar fi fișiere sau socket-uri.

Canalele sunt foarte asemănătoare cu fluxurile din pachetul java.io. Toate datele care merg oriunde (sau vin de oriunde) trebuie să treacă printr-un obiect canal. În general, pentru a utiliza sistemul NIO, obțineți un canal către o entitate I/O și un buffer pentru stocarea datelor. Apoi lucrați cu tamponul, introducând sau scoțând date după cum este necesar.

Vă puteți deplasa înainte și înapoi într-un buffer, adică „plimbați” în buffer, ceea ce nu ați putea face în fluxuri. Acest lucru oferă mai multă flexibilitate la procesarea datelor. În biblioteca standard, bufferele sunt reprezentate de clasa abstractă Buffer și câțiva dintre descendenții acesteia:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • FloatBuffer
  • DoubleBuffer
  • LongBuffer

Principala diferență dintre subclase este tipul de date pe care le stochează - octeți , int , longs și alte tipuri de date primitive.

Proprietăți tampon

Un tampon are patru proprietăți principale. Acestea sunt capacitatea, limita, poziția și marcajul.

Capacitatea este cantitatea maximă de date/octeți care poate fi stocată în buffer. Capacitatea unui buffer nu poate fi modificată . Odată ce un buffer este plin, acesta trebuie șters înainte de a scrie mai multe în el.

În modul de scriere, limita unui buffer este aceeași cu capacitatea sa, indicând cantitatea maximă de date care poate fi scrisă în buffer. În modul de citire, limita unui buffer se referă la cantitatea maximă de date care poate fi citită din buffer.

Poziția indică poziția curentă a cursorului în buffer. Inițial, este setat la 0 când este creat tamponul. Cu alte cuvinte, este indexul următorului element care trebuie citit sau scris.

Marcajul este folosit pentru a salva poziția cursorului. Pe măsură ce manipulăm un buffer, poziția cursorului se schimbă constant, dar îl putem întoarce oricând la poziția marcată anterior.

Metode de lucru cu un buffer

Acum să ne uităm la setul principal de metode care ne permit să lucrăm cu tamponul nostru (blocul de memorie) pentru citirea și scrierea datelor către și de la canale.

  1. allocate(int capacity) — această metodă este utilizată pentru a aloca un nou buffer cu capacitatea specificată. Metoda allocate() aruncă o excepție IllegalArgumentException dacă capacitatea transmisă este un număr întreg negativ.

  2. Capacitate() returnează capacitatea bufferului curent .

  3. position() returnează poziţia curentă a cursorului. Operațiile de citire și scriere mută cursorul la sfârșitul bufferului. Valoarea returnată este întotdeauna mai mică sau egală cu limita.

  4. limit() returnează limita actuală a tamponului.

  5. mark() este folosit pentru a marca (salva) poziția curentă a cursorului.

  6. reset() readuce cursorul la poziția marcată anterior (salvată).

  7. clear() setează poziția la zero și stabilește limita capacității. Această metodă nu șterge datele din buffer. Reinițializează doar poziția, limita și marcajul.

  8. flip() comută buffer-ul din modul scriere în modul citire. De asemenea, stabilește limita pentru poziția curentă și apoi pune poziția înapoi la zero.

  9. read() — Metoda de citire a canalului este folosită pentru a scrie date de pe canal în buffer, în timp ce metoda put() a bufferului este folosită pentru a scrie date în buffer.

  10. write() — Metoda de scriere a canalului este folosită pentru a scrie date din buffer pe canal, în timp ce metoda get() a bufferului este folosită pentru a citi datele din buffer.

  11. rewind() derulează înapoi tamponul. Această metodă este utilizată atunci când trebuie să recitiți memoria tampon - setează poziția la zero și nu modifică limita.

Și acum câteva cuvinte despre canale.

Cele mai importante implementări de canal în Java NIO sunt următoarele clase:

  1. FileChannel — Un canal pentru citirea și scrierea datelor dintr-un fișier.

  2. DatagramChannel — Această clasă citește și scrie date în rețea prin UDP (User Datagram Protocol).

  3. SocketChannel — Un canal pentru citirea și scrierea datelor în rețea prin TCP (Transmission Control Protocol).

  4. ServerSocketChannel — Un canal pentru citirea și scrierea datelor prin conexiuni TCP, la fel ca un server web. Un SocketChannel este creat pentru fiecare conexiune de intrare.

Practică

Este timpul să scrieți câteva rânduri de cod. Mai întâi, să citim fișierul și să îi afișăm conținutul pe consolă, apoi să scriem un șir în fișier.

Codul conține o mulțime de comentarii - sper că vă vor ajuta să înțelegeți cum funcționează totul:


// Create a RandomAccessFile object, passing in the file path
// and a string that says the file will be opened for reading and writing
try (RandomAccessFile randomAccessFile = new RandomAccessFile("text.txt", "rw");
    // Get an instance of the FileChannel class
    FileChannel channel = randomAccessFile.getChannel();
) {
// Our file is small, so we'll read it in one go   
// Create a buffer of the required size based on the size of our channel
   ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size());
   // Read data will be put into a StringBuilder
   StringBuilder builder = new StringBuilder();
   // Write data from the channel to the buffer
   channel.read(byteBuffer);
   // Switch the buffer from write mode to read mode
   byteBuffer.flip();
   // In a loop, write data from the buffer to the StringBuilder
   while (byteBuffer.hasRemaining()) {
       builder.append((char) byteBuffer.get());
   }
   // Display the contents of the StringBuilder on the console
   System.out.println(builder);
 
   // Now let's continue our program and write data from a string to the file
   // Create a string with arbitrary text
   String someText = "Hello, Amigo!!!!!";
   // Create a new buffer for writing,
   // but let the channel remain the same, because we're going to the same file
   // In other words, we can use one channel for both reading and writing to a file
   // Create a buffer specifically for our string — convert the string into an array and get its length
   ByteBuffer byteBuffer2 = ByteBuffer.allocate(someText.getBytes().length);
   // Write our string to the buffer
   byteBuffer2.put(someText.getBytes());
   // Switch the buffer from write mode to read mode
   // so that the channel can read from the buffer and write our string to the file
   byteBuffer2.flip();
   // The channel reads the information from the buffer and writes it to our file
   channel.write(byteBuffer2);
} catch (FileNotFoundException e) {
   e.printStackTrace();
} catch (IOException e) {
   e.printStackTrace();
}

Încercați API-ul NIO - vă va plăcea!