Раннее мы с вами познакомились 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) — используется для маркировки текущей позиции курсора. В процессе манипуляций с буфером позиция курсора постоянно изменяется, но мы всегда можем вернуть его в маркированную раннее позицию.
Методы для работы с буфером
Теперь давайте рассмотрим основной набор методов, которые позволяют работать с нашим буфером (блоком памяти) для чтения и записи данных в каналы и из каналов.
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 — канал для считывания и записи данныx по сети через 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 и он вам понравится!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ