Buffer, Channel

Модуль 1. Java Syntax
Рівень 26 , Лекція 2
Відкрита

Раніше ми з вами познайомилися з 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, і він вам сподобається!

Коментарі (4)
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ
Aleksey Golovin Рівень 50
28 вересня 2025
Щось дивне. Ось вам нуль теорії по FileChannel.Ось вам задачка по класу.
Кирило Рівень 43
16 серпня 2024
навіщо давати перше завдання, не даючи при цьому в лекції метод truncate?
Arthur Рівень 70
11 січня 2024
Цікаві задачки. На перший погляд складні , але якщо уважно читати завдання і полистати docs.oracle.com то все досить просто.
20 грудня 2023
Ну тут не медіум явно...