JavaRush /Курсы /JAVA 25 SELF /Асинхронная обработка текстовых файлов

Асинхронная обработка текстовых файлов

JAVA 25 SELF
56 уровень , 2 лекция
Открыта

1. Чтение файла по частям: ByteBuffer и кодировка

Сегодня мы редко имеем дело с маленькими текстовыми файлами. Обычно это огромные логи серверов, отчёты, CSV-файлы или гигабайтные дампы данных. Поэтому важно не просто читать файл, а делать это эффективно и без «замирания» приложения.

Асинхронный подход помогает именно в этом: он не блокирует основной поток — будь то интерфейс или серверная логика, — позволяет читать и записывать большие объёмы данных параллельно и делает приложение масштабируемым, когда нужно работать сразу с несколькими файлами.

Главное понимать: асинхронный ввод-вывод не ускоряет сам диск — чудес не бывает. Он просто позволяет вашей программе не скучать в ожидании, пока диск выполняет операцию, и заниматься другими делами в это время.

Как работает асинхронное чтение?

Асинхронный канал (AsynchronousFileChannel) читает не строки, а блоки байтов в объект ByteBuffer. Это как если бы вы таскали коробки с буквами, а не отдельные слова. После чтения вам нужно превратить эти байты в строки — с учётом кодировки!

Пример: асинхронное чтение файла блоками

Давайте напишем простейший пример асинхронного чтения файла блоками по 4096 байт и вывода содержимого в консоль.

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;
import java.nio.charset.StandardCharsets;
import java.io.IOException;

public class AsyncTextReadExample {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("bigfile.txt");
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(4096);
            int position = 0;
            Future<Integer> future = channel.read(buffer, position);

            while (future.get() > 0) {
                buffer.flip();
                // Преобразуем байты в строку (UTF-8)
                String chunk = StandardCharsets.UTF_8.decode(buffer).toString();
                System.out.print(chunk);
                buffer.clear();
                position += chunk.getBytes(StandardCharsets.UTF_8).length;
                future = channel.read(buffer, position);
            }
        }
    }
}

Важные моменты:

  • Мы читаем файл по частям (буферу), а не целиком.
  • После чтения байты декодируются в строку с помощью Charset.
  • Не забываем про buffer.clear() — иначе следующий read не сработает!

Почему просто декодировать байты недостаточно?

Беда в том, что строка может «разорваться» между двумя блоками, особенно если используется многобайтовая кодировка (например, "UTF-8"). Если последний байт в буфере — это половина символа, то следующий блок начнётся с «остатка» символа. Без специальной обработки вы получите «кракозябры» или даже ошибку декодирования.

2. Преобразование байтов в строки: обработка разрывов

Проблема разрыва строк

Допустим, у вас строка "Привет\nМир\n", а буфер закончился на "Прив", а "ет\nМир\n" попало в следующий блок. Если просто склеивать строки, можно потерять символы или получить некорректную строку.

Решение: использовать CharsetDecoder

Java предоставляет класс CharsetDecoder, который умеет корректно обрабатывать такие случаи. Он «запоминает» недекодированные байты и корректно восстанавливает символы на стыке блоков.

Пример использования CharsetDecoder

import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.CharBuffer;
import java.nio.ByteBuffer;

CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
ByteBuffer buffer = ... // ваши байты
CharBuffer charBuffer = CharBuffer.allocate(buffer.capacity());
decoder.decode(buffer, charBuffer, false);
// Теперь charBuffer содержит корректно декодированные символы

В реальной задаче вы будете хранить «остаток» между чтениями и декодировать с учётом этого остатка.

3. Асинхронная запись текстовых файлов

Чтение — это только половина дела. Запись также выполняется блоками байт, которые нужно сначала получить из строк (кодировать).

Пример: асинхронная запись строки в файл

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;
import java.nio.charset.StandardCharsets;

public class AsyncTextWriteExample {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("output.txt");
        String text = "Привет, мир!\n";
        ByteBuffer buffer = ByteBuffer.wrap(text.getBytes(StandardCharsets.UTF_8));
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
            Future<Integer> future = channel.write(buffer, 0);
            // Для демонстрации подождём завершения (обычно делать не надо!)
            future.get();
            System.out.println("Данные записаны асинхронно.");
        }
    }
}

Комментарий: В реальных асинхронных сценариях не стоит вызывать future.get() в основном потоке — это превращает асинхронный код в синхронный. Лучше использовать CompletionHandler (см. предыдущую лекцию).

4. Практика: асинхронное чтение большого текстового файла и подсчёт строк

Давайте реализуем практическую задачу: асинхронно прочитать большой текстовый файл и посчитать количество строк ("\n"). Результат — вывести количество строк в консоль.

Пример с использованием CompletionHandler

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.CharBuffer;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;

public class AsyncLineCounter {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("bigfile.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);

        ByteBuffer buffer = ByteBuffer.allocate(4096);
        AtomicLong position = new AtomicLong(0);
        CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
        StringBuilder leftover = new StringBuilder();
        AtomicLong lines = new AtomicLong(0);

        channel.read(buffer, position.get(), null, new CompletionHandler<Integer, Object>() {
            @Override
            public void completed(Integer result, Object attachment) {
                if (result == -1) {
                    // Файл прочитан до конца
                    if (leftover.length() > 0) lines.incrementAndGet();
                    System.out.println("Строк в файле: " + lines.get());
                    try { channel.close(); } catch (IOException e) { e.printStackTrace(); }
                    return;
                }
                buffer.flip();
                CharBuffer charBuffer = CharBuffer.allocate(buffer.remaining());
                decoder.decode(buffer, charBuffer, false);
                charBuffer.flip();
                String chunk = leftover.toString() + charBuffer.toString();
                leftover.setLength(0);

                // Считаем строки
                int last = 0;
                int idx;
                while ((idx = chunk.indexOf('\n', last)) != -1) {
                    lines.incrementAndGet();
                    last = idx + 1;
                }
                // Остаток (часть строки после последнего \n)
                if (last < chunk.length()) {
                    leftover.append(chunk.substring(last));
                }
                buffer.clear();
                position.addAndGet(result);
                channel.read(buffer, position.get(), null, this);
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                System.err.println("Ошибка чтения: " + exc.getMessage());
                try { channel.close(); } catch (IOException e) { e.printStackTrace(); }
            }
        });

        // Чтобы программа не завершилась раньше времени (только для примера!)
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
    }
}
  • Мы используем CompletionHandler для truly async-кода.
  • После каждого чтения буфер декодируется с помощью CharsetDecoder.
  • Остаток строки, не закончившейся на "\n", переносится в следующий блок.
  • После окончания файла, если что-то осталось в leftover, это тоже считается строкой.
  • Для простоты пример «засыпает» на 2000 мс, чтобы асинхронная операция завершилась (в реальных приложениях это не нужно — обычно есть главный цикл или UI).

5. Асинхронная запись результатов в файл

Допустим, мы хотим записать результат (например, количество строк) в новый файл — тоже асинхронно.

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.charset.StandardCharsets;
import java.io.IOException;

public class AsyncWriteResult {
    public static void main(String[] args) throws IOException {
        String result = "Строк в файле: 12345\n";
        ByteBuffer buffer = ByteBuffer.wrap(result.getBytes(StandardCharsets.UTF_8));
        Path path = Path.of("result.txt");

        AsynchronousFileChannel channel = AsynchronousFileChannel.open(
            path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

        channel.write(buffer, 0, null, new CompletionHandler<Integer, Object>() {
            @Override
            public void completed(Integer written, Object attachment) {
                System.out.println("Результат записан асинхронно!");
                try { channel.close(); } catch (IOException e) { e.printStackTrace(); }
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                System.err.println("Ошибка записи: " + exc.getMessage());
                try { channel.close(); } catch (IOException e) { e.printStackTrace(); }
            }
        });

        try { Thread.sleep(500); } catch (InterruptedException e) {}
    }
}

6. Советы по обработке частичных данных и кодировок

Частичные строки между блоками

Если строка разбита между двумя блоками, не пытайтесь «склеить» байты вручную! Используйте CharsetDecoder, который аккуратно обработает недостающие байты и не потеряет ни одного символа.

Работа с разными кодировками

"UTF-8" — стандарт для современных приложений, но если файл в другой кодировке (например, "Windows-1251" или "UTF-16"), используйте соответствующий Charset:

import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

Charset charset = Charset.forName("Windows-1251");
CharsetDecoder decoder = charset.newDecoder();

Использование CharsetDecoder и CharsetEncoder

Когда вы читаете или записываете данные по частям, важно правильно работать с кодировкой. Символ может «порваться» между блоками, и тогда без дополнительной обработки получится каша из байтов.

Чтобы этого избежать, используются CharsetDecoder и CharsetEncoder.

При чтении вызывается decode(ByteBuffer, CharBuffer, endOfInput), а при записи — encode(CharBuffer, ByteBuffer, endOfInput).

Они заботятся о том, чтобы даже если символ оказался разделён между двумя блоками, он всё равно был собран и обработан правильно.

7. Типичные ошибки при асинхронной обработке текстовых файлов

Ошибка №1: Игнорирование остатков строки. Если не хранить «хвост» строки между блоками, то часть строк может потеряться или быть некорректно декодирована.

Ошибка №2: Неправильная работа с буфером. Забыли вызвать buffer.clear() после обработки — следующий read не сработает или данные будут некорректны.

Ошибка №3: Использование неподходящей кодировки. Если байты декодируются не тем Charset, что был при записи файла, возможны «кракозябры» или даже ошибки.

Ошибка №4: Блокировка основного потока. Если вы вызываете future.get() или Thread.sleep() в UI-потоке, вы теряете смысл асинхронности. Используйте CompletionHandler и реактивные подходы.

Ошибка №5: Не закрыт канал после завершения. Всегда закрывайте канал (channel.close()) после окончания всех операций, даже если была ошибка.

1
Задача
JAVA 25 SELF, 56 уровень, 2 лекция
Недоступна
Секретарь-скороход: асинхронный подсчёт строк в огромном отчёте 📊
Секретарь-скороход: асинхронный подсчёт строк в огромном отчёте 📊
1
Задача
JAVA 25 SELF, 56 уровень, 2 лекция
Недоступна
Автоматизированная фабрика данных: чтение, обработка и асинхронная выгрузка 🤖
Автоматизированная фабрика данных: чтение, обработка и асинхронная выгрузка 🤖
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ