Раніше ми з вами познайомилися з 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) — використовується для маркування поточної позиції курсору. У процесі маніпуляцій із буфером позиція курсору постійно змінюється, але ми завжди можемо повернути його в марковану раніше позицію.
Методи для роботи з буфером
Тепер давайте розглянемо основний набір методів, які дозволяють працювати з нашим буфером (блоком пам’яті) для читання та запису даних у канали та із каналів.
allocate (int capacity) — метод використовується для виділення нового буфера з ємністю як параметр. Метод allocate() видає виняток IllegalArgumentException у разі, якщо передана ємність є від’ємним цілим числом.
capacity() — повертає ємність (
capacity
) поточного буфера.position() — повертає поточну позицію курсору. Як операції читання, так і запису переміщають курсор у кінець буфера. Значення, що повертається, завжди менше або дорівнює limit.
limit() — повертає ліміт поточного буфера.
mark() — використовується для позначення (маркування) поточної позиції курсору.
reset() — поверне курсор у у раніше позначену (марковану) позицію.
clear() — встановлює позицію у нуль і обмежує її до ємності. У цьому методі дані в буфері не очищаются, тільки маркери ініціалізуються повторно.
flip() — перемикає режим буфера з режиму запису на режим читання. Він також встановлює позицію назад у нуль і встановлює ліміт, у якому позиція була під час запису.
read() — метод читання каналу використовується для запису даних із каналу в буфер, а put() — метод буфера, який використовується для запису даних у буфер.
write() — метод запису каналу використовується для запису даних із буфера в канал, тоді як get() є методом буфера, який використовується для читання даних із буфера.
rewind() — Використовується, коли потрібно перечитування, оскільки він встановлює позицію в нуль і змінює значення ліміту.
А тепер — трохи про канал.
Найбільш важливими реалізаціями каналу в Java NIO виступають такі класи:
FileChannel — канал для читання та запису даних у файл.
DatagramChannel — зчитує та записує дані за допомогою мережі через UDP (User Datagram Protocol).
SocketChannel — канал для зчитування та запису даних за допомогою мережі через TCP (Transmission Control Protocol).
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, і він вам сподобається!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ