Привет, Хабр!
Парадигма «неблокируемого ввода/вывода» заинтересовала меня с того момента, как я о ней услышал. Идея возможности вызвать операцию чтения без блокировки вызывающего потока довольно привлекающая сама по себе.
Неблокируемый ввод/вывод был реализован в пакете java.nio
Java SE 1.4. К сожалению, в ежедневной практике нечасто приходится иметь дело с низкоуровневым I/O, и намного чаще при необходимости используются стримы из java.io
. В этой статье будет описано содержание Java NIO, несколько примеров и принцип работы неблокируемого I/O.
Прим.: данная статья не является руководством по использованию или собранием best-practices. Она направлена в первую очередь на обзор существующих в Java NIO каналов и принцип работы неблокируемого I/O.
В начале статьи приведу цитату, описывающую разницу между подходами в Java IO и Java NIO:
“Основное отличие между двумя подходами к организации ввода/вывода в том, что Java IO является потокоориентированным, а Java NIO – буфер-ориентированным. Разберем подробней.
Потокоориентированный ввод/вывод подразумевает чтение/запись из потока/в поток одного или нескольких байт в единицу времени поочередно. Данная информация нигде не кэшируются. Таким образом, невозможно произвольно двигаться по потоку данных вперед или назад. Если вы хотите произвести подобные манипуляции, вам придется сначала кэшировать данные в буфере.
Подход, на котором основан Java NIO немного отличается. Данные считываются в буфер для последующей обработки. Вы можете двигаться по буферу вперед и назад. Это дает немного больше гибкости при обработке данных. В то же время, вам необходимо проверять содержит ли буфер необходимый для корректной обработки объем данных. Также необходимо следить, чтобы при чтении данных в буфер вы не уничтожили ещё не обработанные данные, находящиеся в буфере.”
В Java NIO присутствуют три важные для его понимания сущности: Buffer
, Channel
и Selector
. Рассмотрим их по порядку.
Buffer – это контейнер для данных примитивного типа. Является более функциональной и удобной заменой массивам примитивов. В Java NIO используется как объект, который хранит фиксированный объем данных, подлежащих отправке или получению из службы ввода-вывода. Он находится между приложением и каналом, который записывает данные в буффер или считывает из него данные.
Channel – связующее звено для операций ввода/вывода. Представляет собой открытое соединение с объектом, таким как аппаратное устройство, файл, сетевой сокет или программный компонент, который способен выполнять одну или несколько различных операций ввода-вывода, например чтение или запись. Остановимся на них поподробнее.
Java NIO имеет множество реализаций каналов. Ниже представлена иерархия интерфейсов.
Краткое описание особенностей интерфейсов семейства Channel.
Channel
– родительский класс для всего семействаReadableByteChannel
– канал, читающий байты из источника данных.ScatteringByteChannel
– канал, читающий байты из источника данных в массив буфферов.WritableByteChannel
– канал, записывающий байты в приемник данных.GatheringByteChannel
– канал, записывающий байты в приемник данных из массива буфферов.ByteChannel
– канал, который может как считывать, так и записывать данные. Интерфейс, объединяющийReadableByteChannel
иWritableByteChannel
.SeekableByteChannel
– канал, запоминающий текущую позицию чтения и имеющий возможность ее изменять. Иными словами, позволяет перемещаться по источнику данных.AsynchronousChannel
– канал, поддерживающий асинхронные операции ввода/вывода.AsynchronousByteChannel
– канал, поддерживающий асинхронные операции чтения/записи байт.NetworkChannel
– канал, использующий сетевой сокет.MulticastChannel
– канал, поддерживающий многоадресную рассылку по IP протоколу.InterruptibleChannel
– канал, который возможно асинхронно закрыть и прервать операцию.
Прим.: потоки из Java OI всегда являются однонаправленными: InputStream/OutputStream. Каналы из Java NIO могут быть двунаправленными.
На следующей диаграмме представлена иерархия классов с абстрактными реализациями.
На диаграмме интерфейсы (реализации) разделены на 3 группы:
красные – блокирующие каналы;
пурпурные – асинхронные каналы;
зеленые – неблокирующие каналы.
Да-да, оказывается, далеко не все каналы из Java NIO являются неблокирующими! Возможно, именно поэтому NIO - не Non-blockable I/O, а именно New I/O. А еще можно заметить, что асинхронные и неблокирующие каналы имеют разные реализации (и концепции). Рассмотрим все три группы по порядку.
Блокирующие каналы
Данная группа каналов работает в стандартной парадигме – после вызова функции чтения/записи, вызывающий поток блокируется до выполнения операции. Главным отличием от стандартного IO тут является буфер-ориентированный подход.
Листинг 1: чтение файла с помощью канала из Java NIO
void nio_blocking_readFile() throws IOException, URISyntaxException {
URL fileUrl = NioTest.class.getResource(testFilePath);
var filepath = Path.of(fileUrl.toURI());
try (ReadableByteChannel inputChannel = FileChannel.open(filepath)) {
var buffer = ByteBuffer.allocate(300_000);
int readByteCount = inputChannel.read(buffer);
var resultBytes = new byte[readByteCount];
//Записываем считанные данные в resultBytes
//Если просто вызвать buffer.array(), то если массив больше считываемого файла,
//в конце он будет заполнен нулями
buffer.get(0, resultBytes);
var fileString = new String(resultBytes, StandardCharsets.UTF_8);
System.out.println(fileString);
}
}
Для сравнения, чтение файла с помощью потоков будет выглядеть следующим образом:
Листинг 2: чтение файла с помощью потока из Java IO
void io_readFile() throws IOException {
try (
InputStream fileStream = NioTest.class.getResourceAsStream(testFilePath);
var inputStream = new BufferedInputStream(fileStream)
) {
byte[] fileBytes = inputStream.readAllBytes();
var fileString = new String(fileBytes, StandardCharsets.UTF_8);
System.out.println(fileString);
}
}
Стоит отметить, что в листинге 1 мы не сможем полностью прочесть файл, если он больше размером, чем выделенный буффер. А так же, нам приходится "обрезать" буффер, что бы убрать лишние нули в нем.
Чтение файла в Java NIO
Из вышесказанного может показаться, что чтение файлов в Java NIO реализовано довольно неудобно. Однако в нем появился утилитный класс Files
, который делает взаимодействие с файлами очень удобным. Вот пример, как с помощью него можно прочитать файл:
Листинг 2.1: чтение файла с помощью java.nio.file.Files
void nio_readFile_files() throws IOException, URISyntaxException {
URL fileUrl = NioTest.class.getResource(testFilePath);
var filePath = Path.of(fileUrl.toURI());
var fileString = Files.readString(path);
System.out.println(fileString);
}
Асинхронные каналы
Данная группа каналов имеет возможность асинхронных операций чтения/записи. Есть возможность выполнить чтение/запись с каллбэком, или просто получить объект Future
, а сама операция чтения/записи будет проходить в фоновом режиме.
Листинг 3: асинхронное чтение файла с Future
void nio_async_readFile() throws URISyntaxException, IOException {
URL fileUrl = NioTest.class.getResource(testFilePath);
var path = Path.of(fileUrl.toURI());
try (var inputChannel = AsynchronousFileChannel.open(path)) {
var buffer = ByteBuffer.allocate(300_000);
Future<Integer> futureResult = inputChannel.read(buffer, 0);
while (!futureResult.isDone()) {
System.out.println("Файл еще не загружен в буффер");
}
var fileString = new String(buffer.array(), StandardCharsets.UTF_8);
System.out.println(fileString);
}
}
Листинг 4: асинхронное чтение файла с использованием каллбэка
void nio_async_readFile() throws URISyntaxException, IOException {
URL fileUrl = NioTest.class.getResource(testFilePath);
var path = Path.of(fileUrl.toURI());
try (var inputChannel = AsynchronousFileChannel.open(path)) {
var buffer = ByteBuffer.allocate(300_000);
inputChannel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
var fileString = new String(buffer.array(), StandardCharsets.UTF_8);
System.out.println(fileString);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
//do nothing
}
});
try {
Thread.sleep(3_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
Неблокирующие каналы
Данная группа каналов может переключаться между блокирующим и неблокирующим режимом. Можно заметить, что все реализации неблокирующих каналов в Java NIO работают с сокетами.
В статье будут рассмотрены неблокируемые каналы на примере двух классов – ServerSocketChannel
и SocketChannel
.
ServerSockerChannel
Серверный сокет открывается командой ServerSocketChannel.open()
. Созданный канал является открытым, но он не привязан к конкретному сокету. Что бы связать его с сокетом, необходимо вызвать serverSocketChannel.socket().bind().
По дефолту канал является блокирующим. Что бы перевести его в неблокирующий режим, нужно вызвать serverSocketChannel.configureBlocking(false)
.
Ловим соединения через вызов serverSocketChannel.accept()
. Если указан блокирующий режим, то вызывающий поток блокируется до момента, пока не примется соединение. В противном случае (включен неблокирующий режим), немедленно возвращается null, если нет ожидаемых подключений. Возвращаемые этим методом каналы всегда являются блокирующими, независимо от установленного типа канала (но их можно перевести в неблокируемый режим). .
Листинг 5: блокируемый сервер
void nio_server_blockable() throws IOException {
//Открытие канала. Под капотом вызывается SelectorProvider, реализация которого является платформозависимой
var ssc = ServerSocketChannel.open();
//Созданный канал является открытым, но не привязан к конкретному сокету. Что бы связать его с сокетом, необходимо вызвать код из следующей строки
ssc.socket().bind(new InetSocketAddress(9999));
//По дефолту канал является блокирующим. Что бы перевести его в неблокирующий режим, нужно в следующей строке передать false
ssc.configureBlocking(true);
var responseMessage = "Привет от сервера! : " + ssc.socket().getLocalSocketAddress();
var sendBuffer = ByteBuffer.wrap(responseMessage.getBytes());
while (true) {
//Ловим соединения через вызов ssc.accept()
//Поток блокируется до момента принятия соединения
try (SocketChannel sc = ssc.accept()) {
System.out.println("Принято соединение от " + sc.socket().getRemoteSocketAddress());
var receivedBuffer = ByteBuffer.allocate(100);
sc.read(receivedBuffer);
var requestMessage = new String(receivedBuffer.array());
System.out.println(requestMessage);
sendBuffer.rewind();
sc.write(sendBuffer);
}
}
}
На строке 14 приложение встает в ожидании запроса на подключение.
Листинг 6: неблокируемый сервер
void nio_server_non_blockable() throws IOException {
var ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(9999));
//Включаем неблокирующий режим канала
ssc.configureBlocking(false);
var responseMessage = "Привет от сервера! : " + ssc.socket().getLocalSocketAddress();
var sendBuffer = ByteBuffer.wrap(responseMessage.getBytes());
while (true) {
System.out.print(".");
//Ловим соединения через вызов ssc.accept().
//Т.к. стоит неблокирующий режим, метод accept немедленно вернет null, если нет ожидающих подключений
try (SocketChannel sc = ssc.accept()) {
if (sc != null) {
System.out.println();
System.out.println("Принято соединение от " + sc.socket().getRemoteSocketAddress());
var receivedBuffer = ByteBuffer.allocate(100);
sc.read(receivedBuffer);
var requestMessage = new String(receivedBuffer.array());
System.out.println(requestMessage);
sendBuffer.rewind();
sc.write(sendBuffer);
} else {
Thread.sleep(100);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
В случае, если нет подключений в состоянии ожидания, на строке 13 возвращается null.
Как мы видим, «неблокируемость» не является какой то серебряной пулей. ServerSocketChannel.accept()
просто-напросто не ждет подключения, а немедленно возвращает null
.
SocketChannel
Клиентский сокет открывается командой SocketChannel.open()
. Если мы включаем неблокируемый режим, то происходит почти то же самое, что и с ServerSocketChannel
– канал не ждет появления «отправляемых» или «получаемых» данных и просто продвигается дальше. Однако, если данные есть, поток блокируется и канал считывает их. Фактически, если канал еще не готов к чтению или записи на момент вызова операции, мы просто пропускаем эту операцию. В примерах используется сервер, написанный в листинге 6.
Листинг 7: блокируемый клиент
void nio_client_blockable() throws IOException {
try (SocketChannel sc = SocketChannel.open()) {
sc.configureBlocking(true);
sc.connect(new InetSocketAddress("localhost", 9999));
var requestMessage = "Привет от клиента! " + LocalDateTime.now();
ByteBuffer buffer = ByteBuffer.wrap(requestMessage.getBytes());
sc.write(buffer);
var receivedBuffer = ByteBuffer.allocate(100);
//Приложение останавливается в ожидании ответа
sc.read(receivedBuffer);
var responseMessage = new String(receivedBuffer.array());
System.out.println(responseMessage);
}
}
На 10 строке приложение останавливается в ожидании ответа.
Листинг 8: неблокирующий клиент
void nio_client_non_blockable() throws IOException {
try (SocketChannel sc = SocketChannel.open()) {
//Включаем неблокирующий режим канала
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("localhost", 9999));
while (!sc.finishConnect()) {
System.out.println("waiting to finish connection");
}
var requestMessage = "Привет от клиента! " + LocalDateTime.now();
ByteBuffer buffer = ByteBuffer.wrap(requestMessage.getBytes());
sc.write(buffer);
var receivedBuffer = ByteBuffer.allocate(100);
//Ответа еще нет, канал ничего не прочтет, буффер останется пустым
int receiveReadCount = sc.read(receivedBuffer);
Thread.sleep(1_000);
var resultBytes = new byte[receiveReadCount];
buffer.get(0, resultBytes);
var responseMessage = new String(resultBytes, StandardCharsets.UTF_8);
//Консоль выведет пустую строку
System.out.println(responseMessage);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
На строке 17 клиент ничего не читает из сокета, поскольку ответа еще нет. Операция чтения пропускается. Можно заметить, что в фоновом режиме ничего не читается, поскольку в консоль выводится пустая строка, несмотря на ожидание после вызова операции чтения.
Листинг 9: неблокируемый клиент, ожидающий ответа
void nio_client_non_blockable() throws IOException {
try (SocketChannel sc = SocketChannel.open()) {
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("localhost", 9999));
while (!sc.finishConnect()) {
System.out.println("waiting to finish connection");
}
ByteBuffer buffer = ByteBuffer.wrap(("Привет от клиента! " + LocalDateTime.now()).getBytes());
sc.write(buffer);
Thread.sleep(1_000);
var receivedBuffer = ByteBuffer.allocate(100);
int receiveReadCount = sc.read(receivedBuffer);
var resultBytes = new byte[receiveReadCount];
buffer.get(0, resultBytes);
var responseMessage = new String(resultBytes, StandardCharsets.UTF_8);
//Консоль выведет ответ
System.out.println(responseMessage);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Поскольку мы установили задержку в секунду между записью запроса и чтением ответа, к моменту вызова операции чтения канал уже имеет данные для чтения, и в консоль выведется ответ.
На первый взгляд, «неблокируемость» выглядит не в лучше свете – мы просто не находимся в состоянии ожидания, если операция еще не готова к выполнению, и возвращаем управление вызывающей функции. Операция пропускается, и мы не получаем (не записываем) никаких данных.
Прим.: тут можно заметить разницу между «асинхронностью» и «неблокируемостью». При асинхронном подходе приложение все равно ожидает данные, но в фоновом потоке (т.е. поток все равно находится в состоянии ожидания, но не вызывающий), в то время как в неблокируемом подходе мы пропускаем операцию, если она не готова к выполнению (сокет не готов принимать данные, или сокет не имеет данных для чтения).
Появляется вопрос – что же нам дает такая «неблокируемость»? Вряд ли кого то устроит пропуск операции, если канал еще не готов отдать данные. Для ответа на этот вопрос нам нужно разобраться с классом Selector
.
Selector
Selector
– это объект, относящийся к группе каналов и определяющий, какой канал готов к записи/чтению/подключению и т.д. Он позволяет одному потоку управлять несколькими каналами (подключениями). Это позволяет уменьшить траты на переключения между потоками.
Я не буду описывать методы рассматриваемых классов, они прекрасно описаны в документации. Но постараюсь описать процесс взаимодействия с селектором и его использования.
После открытия селектора в нем необходимо зарегистрировать используемый канал. Для использования с селектором можно использовать только неблокируемые каналы. Т.е. мы не сможем зарегистрировать, например, FileChannel
. Канал сам проверяет, поддерживает ли он переданные для наблюдения операции и регистрирует свой SelectionKey
в селекторе (по сути, селектор добавляет ключ в список наблюдаемых объектов). Selector.select()
возвращает количество готовых к использованию каналов. Как он понимает, сколько каналов готовы к использованию? SelectionKey
содержит ссылку на канал. Селектор проходит по каждому каналу и опрашивает, готов ли он к записи/чтению/и т.д., и считает количество таких каналов.
Далее, если есть каналы, ожидающие обработки, мы вытаскиваем множество готовых SelectedKey
и обрабатываем их. Кроме этого, есть возможность напрямую передать каллбэк в функцию select()
, и селектор применит его к готовым для взаимодействия каналам.
Для понимания доступных каналу операций существуют обозначающие их константы. Экземпляр SelectionKey
содержит готовые к выполнению операции в канале. Ниже представлен список доступных каналам операций. Они могут сочетаться любым образом.
Доступные операции
OP_READ |
Канал имеет данные, доступные для чтения |
OP_WRITE |
Канал доступен для записи |
OP_CONNECT |
Канал готов завершить подключение или ожидает сообщение об ошибке |
OP_ACCEPT |
Канал готов к приему подключения (только для |
Листинг 10: реализация неблокируемого сервера с использованием селектора
void nio_non_blockable_selector_server() throws IOException {
try (ServerSocketChannel channel = ServerSocketChannel.open();
//Открытие селектора. Под капотом вызывается SelectorProvider, реализация которого является платформозависимой
Selector selector = Selector.open()) {
channel.socket().bind(new InetSocketAddress(9999));
channel.configureBlocking(false);
//Регистрируем серверный канал в селекторе с интересующим типом операции - принятие подключения
SelectionKey registeredKey = channel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//Получаем количество готовых к обработке каналов.
int numReadyChannels = selector.select();
if (numReadyChannels == 0) {
continue;
}
//Получаем готовые к обработке каналы
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
//Обрабатываем каналы в соответствии с типом доступной каналу операции
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
//Принятие подключения серверным сокетом
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
if (client == null) {
continue;
}
client.configureBlocking(false);
//Регистрируем принятое подключение в селекторе с интересующим типом операции - чтение
client.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
//Тут происходит обработка принятых подключений
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer requestBuffer = ByteBuffer.allocate(100);
int r = client.read(requestBuffer);
if (r == -1) {
client.close();
} else {
//В этом блоке происходит обработка запроса
System.out.println(new String(requestBuffer.array()));
String responseMessage = "Привет от сервера! : " + client.socket().getLocalSocketAddress();
//Несмотря на то, что интересующая операция, переданная в селектор - чтение, мы все равно можем писать в сокет
client.write(ByteBuffer.wrap(responseMessage.getBytes()));
}
}
//Удаляем ключ после обработки. Если канал снова будет доступным, его ключ снова появится в selectedKeys
keyIterator.remove();
}
}
}
}
Листинг 11: реализация неблокируемого сервера с использованием селектора и регистрацией каллбэка
void nio_non_blockable_selector_server() throws IOException {
try (ServerSocketChannel channel = ServerSocketChannel.open();
Selector selector = Selector.open()) {
channel.socket().bind(new InetSocketAddress(9999));
channel.configureBlocking(false);
SelectionKey registeredKey = channel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//Обрабатываем доступные к ожиданию подключения с использованием каллбэка
selector.select(key -> {
if (key.isAcceptable()) {
try {
//Принятие подключения серверным сокетом
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
//Регистрируем принятое подключение в селекторе с интересующим типом операции - чтение
client.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (key.isReadable()) {
try {
//Тут происходит обработка принятых подключений
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer requestBuffer = ByteBuffer.allocate(100);
int r = client.read(requestBuffer);
if (r == -1) {
client.close();
} else {
//В этом блоке происходит обработка запроса
System.out.println(new String(requestBuffer.array()));
String responseMessage = "Привет от сервера! : " + client.socket().getLocalSocketAddress();
//Несмотря на то, что интересующая операция, переданная в селектор - чтение, мы все равно можем писать в сокет
client.write(ByteBuffer.wrap(responseMessage.getBytes()));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
}
Примерно так же работает и неблокирующий клиент: мы отправляем сообщение и вместо ожидания ответа регистрируем его в селекторе. При этом мы можем создать отдельный поток, который проверяет селектор и обрабатывает готовые к работе потоки.
Листинг 12 – неблокируемый клиент с использованием селектора и обработкой ответов в отдельном потоке
void nio_clientSocket_non_blockable_selector_1() throws IOException {
try (SocketChannel sc = SocketChannel.open();
Selector selector = Selector.open()) {
sc.configureBlocking(false);
//Регистрируем канал в селекторе с интересующим типом операции - чтение
SelectionKey registeredKey = sc.register(selector, SelectionKey.OP_READ);
//Создаем поток, который будет опрашивать селектор и обрабатывать ответы на наши запросы
var selectorThread = new Thread(() -> {
while (true) {
try {
int numReadyChannels = selector.select();
if (numReadyChannels == 0) {
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
//Этот тот канал, который мы открыли в начале функции
//Мы отловили его дя чтения ответа
SocketChannel client = (SocketChannel) key.channel();
var received = ByteBuffer.allocate(100);
client.read(received);
System.out.println(new String(received.array()));
}
keyIterator.remove();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
selectorThread.setDaemon(true);
selectorThread.start();
sc.connect(new InetSocketAddress("localhost", 9999));
while (!sc.finishConnect()) {
System.out.println("waiting to finish connection");
}
String requestMessage = "Привет от клиента! " + LocalDateTime.now();
ByteBuffer requestBuffer = ByteBuffer.wrap(requestMessage.getBytes());
sc.write(requestBuffer);
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Таким образом, основной поток не блокируется при ожидании ответа. Однако, мы можем создать отдельный ограниченный пулл потоков, которые будут обрабатывать наши селекторы и передавать обработку полученных запросов/ответов в исполняющие потоки.
Другие статьи из цикла "Внутренний мир"
Полезные ссылки
Документация - очень сильно помогает в понимании.
Неплохая статья для того, что бы начать понимать разницу между Java IO & Java NIO.
Хорошая статья с обширным описанием Java NIO.
Дополнительная информация о селекторах и их применении.
Комментарии (7)
aleksandy
21.03.2024 06:10Код из листинга 1 по функционалу эквивалентен следующему коду
Вообще нет. Так как код для NIO не сможет прочесть файл размером больше, чем выделенный буфер, а блокирующее чтение ограничено лишь максимально возможным размером для массива.
Ну, и строка для NIO создаётся неправильно, и в ней может оказаться мусор.
brutfooorcer Автор
21.03.2024 06:10Да, вы правы. Я предполагал, что это очевидно, а под функционалом имел ввиду "и там и там мы читаем файл".
Подправил статью.
dimentiyinfo
На самом деле NIO реализован в Java SE 1.4 (2002 год)
brutfooorcer Автор
Да, действительно. Спасибо, исправил.
dimentiyinfo
Просто интересно, зачем нужно писать такие статьи.
Это чисто рейтинг накачать или какая-то другая цель?
brutfooorcer Автор
Я даже не знаю, как ответить на такой вопрос.
Во первых, если бы я хотел набить рейтинг, я бы взял более хайповую тему, а не Java NIO, которая, как вы верно подметили, была реализована аж в 1.4 версии.
Во вторых, я систематизировал то, что знаю, и постарался это описать, в т. ч. для себя. Хотя я думаю, не мне одному это будет полезно и интересно.
В третьих, мне нравится писать код и нравится писать статьи. Собственно, это самое важное - захотел, и написал.
А с какой целью вы интересуетесь?
man4j
Просто хабр с определенного времени стал таким...