Anteriormente, conhecemos a API IO (Input/Output Application Programming Interface) e o pacote java.io , cujas classes são principalmente para trabalhar com streams em Java. A chave aqui é o conceito de um fluxo .

Hoje começaremos a considerar a API NIO (New Input/Output).

A principal diferença entre as duas abordagens de E/S é que a API IO é orientada a fluxo, enquanto a API NIO é orientada a buffer. Portanto, os principais conceitos a serem compreendidos são buffers e canais .

O que é um buffer e o que é um canal?

Um canal é um portal lógico através do qual os dados entram e saem, enquanto um buffer é a fonte ou o destino desses dados transmitidos. Durante a saída, os dados que você deseja enviar são colocados em um buffer e o buffer passa os dados para o canal. Durante a entrada, os dados do canal são colocados no buffer.

Em outras palavras:

  • um buffer é simplesmente um bloco de memória no qual podemos escrever informações e do qual podemos ler informações,
  • um canal é um gateway que fornece acesso a dispositivos de E/S, como arquivos ou soquetes.

Os canais são muito semelhantes aos fluxos no pacote java.io. Todos os dados que vão para qualquer lugar (ou vêm de qualquer lugar) devem passar por um objeto de canal. Em geral, para usar o sistema NIO, você obtém um canal para uma entidade de I/O e um buffer para armazenamento de dados. Em seguida, você trabalha com o buffer, inserindo ou produzindo dados conforme necessário.

Você pode mover para frente e para trás em um buffer, ou seja, "andar" no buffer, algo que você não poderia fazer em streams. Isso dá mais flexibilidade ao processar dados. Na biblioteca padrão, os buffers são representados pela classe abstrata Buffer e vários de seus descendentes:

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

A principal diferença entre as subclasses é o tipo de dados que armazenam — bytes , ints , longs e outros tipos de dados primitivos.

Propriedades do buffer

Um buffer tem quatro propriedades principais. Estes são capacidade, limite, posição e marca.

Capacidade é a quantidade máxima de dados/bytes que podem ser armazenados no buffer. A capacidade de um buffer não pode ser alterada . Quando um buffer estiver cheio, ele deve ser limpo antes de gravar mais nele.

No modo de gravação, o limite de um buffer é igual à sua capacidade, indicando a quantidade máxima de dados que podem ser gravados no buffer. No modo de leitura, o limite de um buffer refere-se à quantidade máxima de dados que podem ser lidos do buffer.

A posição indica a posição atual do cursor no buffer. Inicialmente, é definido como 0 quando o buffer é criado. Em outras palavras, é o índice do próximo elemento a ser lido ou escrito.

A marca é usada para salvar uma posição do cursor. À medida que manipulamos um buffer, a posição do cursor muda constantemente, mas sempre podemos retorná-lo à posição marcada anteriormente.

Métodos para trabalhar com um buffer

Agora vamos ver o conjunto principal de métodos que nos permite trabalhar com nosso buffer (bloco de memória) para ler e escrever dados de e para os canais.

  1. allocate(int capacidade) — este método é usado para alocar um novo buffer com a capacidade especificada. O método allocate() lançará uma IllegalArgumentException se a capacidade passada for um número inteiro negativo.

  2. capacidade() retorna a capacidade do buffer atual .

  3. position() retorna a posição atual do cursor. As operações de leitura e gravação movem o cursor para o final do buffer. O valor de retorno é sempre menor ou igual ao limite.

  4. limit() retorna o limite do buffer atual.

  5. mark() é usado para marcar (salvar) a posição atual do cursor.

  6. reset() retorna o cursor para a posição previamente marcada (salvo).

  7. clear() define a posição como zero e define o limite para a capacidade. Este método não limpa os dados no buffer. Apenas reinicializa a posição, limite e marca.

  8. flip() muda o buffer do modo de gravação para o modo de leitura. Ele também define o limite para a posição atual e, em seguida, coloca a posição de volta em zero.

  9. read() — O método read do canal é usado para gravar dados do canal no buffer, enquanto o método put() do buffer é usado para gravar dados no buffer.

  10. write() — O método write do canal é usado para gravar dados do buffer no canal, enquanto o método get() do buffer é usado para ler dados do buffer.

  11. rewind() rebobina o buffer. Este método é usado quando você precisa reler o buffer - ele define a posição como zero e não altera o limite.

E agora algumas palavras sobre canais.

As implementações de canal mais importantes em Java NIO são as seguintes classes:

  1. FileChannel — Um canal para ler e gravar dados de/para um arquivo.

  2. DatagramChannel — Esta classe lê e grava dados na rede via UDP (User Datagram Protocol).

  3. SocketChannel — Um canal para leitura e gravação de dados na rede via TCP (Transmission Control Protocol).

  4. ServerSocketChannel — Um canal para leitura e gravação de dados em conexões TCP, assim como um servidor web. Um SocketChannel é criado para cada conexão de entrada.

Prática

É hora de escrever algumas linhas de código. Primeiro, vamos ler o arquivo e exibir seu conteúdo no console e, em seguida, gravar alguma string no arquivo.

O código contém muitos comentários — espero que ajudem você a entender como tudo funciona:


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

Experimente a API NIO — você vai adorar!