Раннее мы с вами познакомились c IO API (Input & Output Application Programming Interface) и пакетом java.io, в классах которого сосредоточен основной функционал работы с потоками в Java. Ключевым здесь является понятие потока (stream).

Сегодня же мы начнем рассматривать NIO API (New Input & Output).

Основное отличие между двумя подходами к организации ввода/вывода заключается в том, что IO API — потоко-ориентированное, а NIO API — буферо-ориентированное. Главные понятия в этом случае — понятия буфера (buffer) и канала (channel).

Что такое буфер и канал?

Канал — это логический портал, через которые осуществляется ввод/вывод данных, а буфер является источником или приёмником этих переданных данных. При организации вывода данные, которые вы хотите отправить, помещаются в буфер, а он передает их в канал. При вводе, данные из канала помещаются в буфер.

Иными словами:

  • буфер — это просто блок памяти, в который мы можем записывать информацию и из которого мы можем читать информацию,
  • канал — это шлюз, который позволяет получить доступ к устройствам ввода/вывода, таким как файл или сокет.

Каналы очень похожи на потоки в пакете java.io. Все данные, которые идут куда угодно (или приходят откуда угодно), должны проходить через объект канала. В общем, чтобы использовать систему NIO, вы получаете канал к устройству ввода/вывода и буфер для хранения данных. Затем вы работаете с буфером, вводя или выводя данные по мере необходимости.

Вы можете двигаться по буферу вперед и назад, то есть, “гулять” по нему, чего не могли делать в потоках. Это дает больше гибкости при обработке данных. В стандартной библиотеке буфер представлен абстрактным классом Buffer и множеством его наследников:

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

Основное отличие наследников — тип данных, который они будут хранить — byte, int, long и другие примитивные типы данных.

Свойства буфера

У буфера есть четыре основных свойства. Это емкость, лимит, позиция и маркер.

Емкость (Capacity) — максимальный объем данных/байт, который может быть сохранен в буфер. Емкость буфера не может быть изменена. Как только буфер заполнен, его следует очистить перед записью в него.

Лимит (Limit) — в режиме записи буфера Лимит равен емкости, что показывает максимальное количество данных, которые могут быть записаны в буфер. В режиме чтения буфера Лимит означает максимальное количество данных, которые можно прочитать из буфера.

Позиция (Position) — указывает на текущую позицию курсора в буфере. Первоначально устанавливается на 0 в момент создания буфера. Иными словами, это индекс элемента, который должен быть прочитан или записан.

Маркер (Mark) — используется для маркировки текущей позиции курсора. В процессе манипуляций с буфером позиция курсора постоянно изменяется, но мы всегда можем вернуть его в маркированную раннее позицию.

Методы для работы с буфером

Теперь давайте рассмотрим основной набор методов, которые позволяют работать с нашим буфером (блоком памяти) для чтения и записи данных в каналы и из каналов.

  1. allocate (int capacity) — метод используется для выделения нового буфера с емкостью в качестве параметра. Метод allocate() выдает исключение IllegalArgumentException в случае, если переданная емкость является отрицательным целым числом.

  2. capacity() — возвращает емкость (capacity) текущего буфера.

  3. position() — возвращает текущую позицию курсора. Как операции чтения, так и записи перемещают курсор в конец буфера. Возвращаемое значение всегда меньше или равно limit.

  4. limit() — возвращает лимит текущего буфера.

  5. mark() — используется для обозначения (маркировки) текущей позиции курсора.

  6. reset() — вернет курсор в ранее отмеченную (маркированную) позицию.

  7. clear() — устанавливает позицию в ноль и ограничивает ее до емкости. В этом методе данные в буфере не очищаются, только маркеры инициализируются повторно.

  8. flip() — переключает режим буфера с режима записи на режим чтения. Он также устанавливает позицию обратно в ноль и устанавливает лимит, в котором позиция была во время записи.

  9. read() — метод чтения канала используется для записи данных из канала в буфер, а put() — метод буфера, который используется для записи данных в буфер.

  10. write() — метод записи канала используется для записи данных из буфера в канал, в то время как get() является методом буфера, который используется для чтения данных из буфера.

  11. rewind() — метод перемотки. Используется, когда требуется перечитывание, так как он устанавливает позицию в ноль и не изменяет значение лимита.

А теперь — немного о канале.

Наиболее важными реализациями канала в Java NIO выступают следующие классы:

  1. FileChannel — канал для чтения и записи данных в файл.

  2. DatagramChannel — считывает и записывает данные по сети через UDP (User Datagram Protocol).

  3. SocketChannel — канал для считывания и записи данныx по сети через TCP (Transmission Control Protocol).

  4. ServerSocketChannel — канал для чтения и записи данных через TCP-соединения, так же, как это делает веб-сервер. Для каждого входящего соединения создается SocketChannel.

Практика

Пришло время написать пару строчек кода. Для начала давайте прочитаем файл и выведем его содержимое в консоли, а потом запишем в файл любую созданную нами строку.

В коде содержится много комментариев, надеюсь, они помогут вам понять как же все работает:


// инициализируем класс RandomAccessFile, в параметры передаем путь к файлу
// и модификатор, который говорит, что файл откроется для чтения и записи
try (RandomAccessFile randomAccessFile = new RandomAccessFile("text.txt", "rw");
    // получаем экземпляр класса FileChannel
    FileChannel channel = randomAccessFile.getChannel();
) {
// наш файл имеет небольшой размер, поэтому считывать мы его будем за один раз   
// создаем буфер необходимого размера на основании размера нашего канала
   ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size());
   // прочитанные данные будем помещать в StringBuilder
   StringBuilder builder = new StringBuilder();
   // записываем данные из канала в буфер
   channel.read(byteBuffer);
   // переключаем буфер с режима записи на режим чтения
   byteBuffer.flip();
   // в цикле записываем данные из буфера в StringBuilder
   while (byteBuffer.hasRemaining()) {
       builder.append((char) byteBuffer.get());
   }
   // выводим содержимое StringBuilder в консоли
   System.out.println(builder);
 
   // теперь продолжим нашу программу и запишем данные из строки в файл
   // создаем строку с произвольным текстом
   String someText = "Hello, Amigo!!!!!";
   // создаем для работы с записью новый буфер,
   // а канал пусть остается прежним, т.к. мы будем писать в тот же файл
   // т.е., один канал мы можем использовать как для чтения, так и для записи в файл
   // создаем буфер конкретно под нашу строку — строку переводим в массив и берем его длину
   ByteBuffer byteBuffer2 = ByteBuffer.allocate(someText.getBytes().length);
   // записываем нашу строку в буфер
   byteBuffer2.put(someText.getBytes());
   // переключаем буфер с режима записи на режим чтения,
   // чтобы канал смог прочитать из буфера и записать нашу строку в файл
   byteBuffer2.flip();
   // канал читает информацию из буфера и записывает ее в наш файл
   channel.write(byteBuffer2);
} catch (FileNotFoundException e) {
   e.printStackTrace();
} catch (IOException e) {
   e.printStackTrace();
}

Попробуйте NIO API и он вам понравится!