In precedenza, abbiamo conosciuto l' API IO (Input/Output Application Programming Interface) e il pacchetto java.io , le cui classi servono principalmente per lavorare con i flussi in Java. La chiave qui è il concetto di un flusso .

Oggi inizieremo a considerare l' API NIO (New Input/Output).

La differenza principale tra i due approcci all'I/O è che l'API IO è orientata al flusso mentre l'API NIO è orientata al buffer. Quindi i concetti principali da comprendere sono buffer e canali .

Cos'è un buffer e cos'è un canale?

Un canale è un portale logico attraverso il quale i dati entrano ed escono, mentre un buffer è l'origine o la destinazione di questi dati trasmessi. Durante l'output, i dati che si desidera inviare vengono inseriti in un buffer e il buffer passa i dati al canale. Durante l'input, i dati del canale vengono inseriti nel buffer.

In altre parole:

  • un buffer è semplicemente un blocco di memoria in cui possiamo scrivere informazioni e da cui possiamo leggere informazioni,
  • un canale è un gateway che fornisce l'accesso a dispositivi I/O come file o socket.

I canali sono molto simili agli stream nel pacchetto java.io. Tutti i dati che vanno ovunque (o provengono da qualsiasi luogo) devono passare attraverso un oggetto canale. In generale, per utilizzare il sistema NIO, si ottiene un canale per un'entità I/O e un buffer per l'archiviazione dei dati. Quindi lavori con il buffer, inserendo o emettendo i dati secondo necessità.

Puoi andare avanti e indietro in un buffer, cioè "percorrere" il buffer, cosa che non puoi fare nei flussi. Ciò offre una maggiore flessibilità durante l'elaborazione dei dati. Nella libreria standard, i buffer sono rappresentati dalla classe Buffer astratta e da molti dei suoi discendenti:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • FloatBuffer
  • DoubleBuffer
  • Buffer lungo

La principale differenza tra le sottoclassi è il tipo di dati che memorizzano: bytes , ints , longs e altri tipi di dati primitivi.

Proprietà tampone

Un buffer ha quattro proprietà principali. Questi sono capacità, limite, posizione e segno.

La capacità è la quantità massima di dati/byte che possono essere memorizzati nel buffer. La capacità di un buffer non può essere modificata . Una volta che un buffer è pieno, deve essere cancellato prima di scrivervi altro.

In modalità di scrittura, il limite di un buffer è uguale alla sua capacità, indicando la quantità massima di dati che possono essere scritti nel buffer. In modalità di lettura, il limite di un buffer si riferisce alla quantità massima di dati che possono essere letti dal buffer.

La posizione indica la posizione corrente del cursore nel buffer. Inizialmente, è impostato su 0 quando viene creato il buffer. In altre parole, è l'indice dell'elemento successivo da leggere o scrivere.

Il contrassegno viene utilizzato per salvare la posizione del cursore. Man mano che manipoliamo un buffer, la posizione del cursore cambia costantemente, ma possiamo sempre riportarlo alla posizione contrassegnata in precedenza.

Metodi per lavorare con un buffer

Ora diamo un'occhiata al set principale di metodi che ci permettono di lavorare con il nostro buffer (blocco di memoria) per leggere e scrivere dati da e verso i canali.

  1. allocate(int capacity) — questo metodo viene utilizzato per allocare un nuovo buffer con la capacità specificata. Il metodo allocate() genera un'eccezione IllegalArgumentException se la capacità passata è un numero intero negativo.

  2. capacity() restituisce la capacità del buffer corrente .

  3. position() restituisce la posizione corrente del cursore. Le operazioni di lettura e scrittura spostano il cursore alla fine del buffer. Il valore restituito è sempre minore o uguale al limite.

  4. limit() restituisce il limite del buffer corrente.

  5. mark() viene utilizzato per contrassegnare (salvare) la posizione corrente del cursore.

  6. reset() riporta il cursore alla posizione contrassegnata in precedenza (salvata).

  7. clear() imposta la posizione a zero e imposta il limite alla capacità. Questo metodo non cancella i dati nel buffer. Reinizializza solo la posizione, il limite e il contrassegno.

  8. flip() commuta il buffer dalla modalità di scrittura alla modalità di lettura. Imposta anche il limite alla posizione corrente e quindi riporta la posizione a zero.

  9. read() — Il metodo read del canale viene utilizzato per scrivere i dati dal canale al buffer, mentre il metodo put() del buffer viene utilizzato per scrivere i dati nel buffer.

  10. write() — Il metodo write del canale viene utilizzato per scrivere i dati dal buffer al canale, mentre il metodo get() del buffer viene utilizzato per leggere i dati dal buffer.

  11. rewind() riavvolge il buffer. Questo metodo viene utilizzato quando è necessario rileggere il buffer: imposta la posizione su zero e non modifica il limite.

E ora qualche parola sui canali.

Le implementazioni di canale più importanti in Java NIO sono le seguenti classi:

  1. FileChannel — Un canale per leggere e scrivere dati da/su un file.

  2. DatagramChannel — Questa classe legge e scrive i dati sulla rete tramite UDP (User Datagram Protocol).

  3. SocketChannel — Un canale per la lettura e la scrittura di dati sulla rete tramite TCP (Transmission Control Protocol).

  4. ServerSocketChannel — Un canale per leggere e scrivere dati su connessioni TCP, proprio come fa un server web. Viene creatoun SocketChannel per ogni connessione in entrata.

Pratica

È ora di scrivere un paio di righe di codice. Per prima cosa, leggiamo il file e visualizziamo il suo contenuto sulla console, quindi scriviamo una stringa nel file.

Il codice contiene molti commenti — spero che ti aiutino a capire come funziona tutto:


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

Prova l' API NIO : lo adorerai!