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 для справді асинхронного коду.
  • Після кожного читання буфер декодується за допомогою 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()) після завершення всіх операцій, навіть якщо була помилка.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ