Раніше ми з вами познайомилися з 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 — канал для зчитування та запису даних за допомогою мережі через 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, і він вам сподобається!