JavaRush /Курси /JAVA 25 SELF /Future, CompletionHandler: обробка завершення операцій

Future, CompletionHandler: обробка завершення операцій

JAVA 25 SELF
Рівень 56 , Лекція 1
Відкрита

1. Future: «Почекаю, доки буде готово»

Асинхронні операції в Java — це як надіслати листа поштою: ви не стоїте біля скриньки й не чекаєте на відповідь, а займаєтеся своїми справами. Коли лист надійде — ви просто отримаєте сповіщення. У коді потрібно зрозуміти, що операцію завершено, і дізнатися результат або помилку. У Java є два підходи: через Future і через CompletionHandler.

З Future ви отримуєте «квитанцію» — обіцянку результату в майбутньому. Можна час від часу перевіряти, чи готовий він, або дочекатися результату, викликавши get().

З CompletionHandler можна взагалі не чекати: заздалегідь вказуєте обробник, і він викличеться сам — у разі успіху або помилки.

Як це працює?

Коли ви викликаєте асинхронний метод read() або write() у AsynchronousFileChannel, ви можете отримати об’єкт типу Future<Integer>. Це як талончик в електронній черзі: операція почалася, а ви можете будь-якої миті спитати: «Ну що, готово?».

Сигнатура методу

Future<Integer> read(ByteBuffer dst, long position)

або

Future<Integer> write(ByteBuffer src, long position)
  • dst — буфер, куди читати дані.
  • src — буфер, звідки записувати дані.
  • position — позиція у файлі для читання/запису.

Приклад: асинхронне читання з Future

Код, що асинхронно читає перші 1024 байти з файлу й явно очікує на результат:

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.io.IOException;

public class FutureReadExample {
    public static void main(String[] args) {
        Path path = Path.of("example.txt");
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            // Запускаємо асинхронне читання
            Future<Integer> future = channel.read(buffer, 0);

            // ... тут можна робити інші справи, поки файл читається

            // Чекаємо завершення операції (блокувальний виклик!)
            int bytesRead = future.get(); // Може згенерувати InterruptedException або ExecutionException

            System.out.println("Прочитано байтів: " + bytesRead);

            // Переходимо до читання з буфера
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
        } catch (Exception e) {
            System.err.println("Помилка читання файлу: " + e);
        }
    }
}
  • Метод future.get() блокує потік до завершення операції — це як «чекати біля телефону», доки не зателефонують.
  • Можна викликати future.isDone(), щоб дізнатися, чи завершено операцію, і лише потім викликати get().
  • Якщо операцію завершено з помилкою, get() згенерує ExecutionException (усередині — причина).

Коли зручний Future?

Future зручний, коли ви хочете запустити завдання й потім, коли зручно, дочекатися результату: читати файл, зробити запит, порахувати щось у тлі. Підхід зручний і для запуску кількох операцій паралельно з подальшим збиранням результатів.

Але якщо потрібна справжня асинхронність без блокувань і з автоматичним повідомленням про завершення, варто розглянути CompletionHandler.

2. CompletionHandler: «Поклич мене, коли закінчиш»

CompletionHandler — це інтерфейс, який ви реалізуєте та передаєте в метод read() або write(). Коли операцію буде завершено (або станеться помилка), Java автоматично викличе ваш обробник. Це як залишити номер телефону: «Зателефонуйте, коли буде готово».

Сигнатура методу

void read(ByteBuffer dst,
          long position,
          A attachment,
          CompletionHandler<Integer, ? super A> handler)
  • dst — буфер для читання.
  • position — позиція у файлі.
  • attachment — довільний об’єкт, який буде передано в обробник (можна null або, наприклад, ім’я файлу).
  • handler — ваш обробник.

Інтерфейс CompletionHandler

public interface CompletionHandler<V, A> {
    void completed(V result, A attachment);
    void failed(Throwable exc, A attachment);
}
  • completed викликається у разі успіху; result — кількість байтів, attachment — ваш об’єкт.
  • failed викликається у разі помилки; exc — виняток.

Приклад: асинхронне читання з 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.io.IOException;

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

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        channel.read(buffer, 0, "example.txt", new CompletionHandler<Integer, String>() {
            @Override
            public void completed(Integer result, String attachment) {
                System.out.println("Файл " + attachment + " прочитано, байтів: " + result);
                buffer.flip();
                while (buffer.hasRemaining()) {
                    System.out.print((char) buffer.get());
                }
                closeChannel();
            }

            @Override
            public void failed(Throwable exc, String attachment) {
                System.err.println("Помилка читання файлу " + attachment + ": " + exc);
                closeChannel();
            }

            private void closeChannel() {
                try {
                    channel.close();
                } catch (IOException e) {
                    System.err.println("Помилка закриття каналу: " + e);
                }
            }
        });

        // Важливо: метод main завершиться до завершення читання, якщо не "притримати" потік!
        // У реальних застосунках потік зазвичай не завершується одразу (наприклад, сервер, UI).
        try {
            Thread.sleep(500); // Дамо час асинхронній операції (лише для прикладу!)
        } catch (InterruptedException ignored) {}
    }
}

Ми передаємо в read() анонімний обробник, який знає, що робити після завершення. У completed отримуємо результат і обробляємо його, а в failed — реагуємо на помилку. Не забудьте закривати канал усередині обробника, щоб не залишити відкриті ресурси.

Є нюанс: метод main може завершитися раніше, ніж закінчиться асинхронна операція, тому в прикладі додано Thread.sleep(500) — лише щоб побачити результат. У реальних застосунках такий трюк зазвичай не потрібен.

Коли зручніше використовувати CompletionHandler?

CompletionHandler зручний, коли потрібна справжня асинхронність без очікувань і блокувань: ви запускаєте операцію та описуєте, що робити по завершенні. Це критично для UI (JavaFX, Swing), щоб інтерфейс не «зависав», і корисно на серверах — потоки не простоюють в очікуванні, а використовуються лише за наявності роботи.

3. Порівняння Future та CompletionHandler

Підхід Блокує потік? Коли використовувати? Приклад сценарію
Future Так (під час get()) Проста послідовна обробка Копіювання файлу, звіти
CompletionHandler Ні Справжня асинхронність, UI, сервери, паралельні операції Сервер, GUI, масовий I/O
  • Future — простіший для розуміння, але для отримання результату потрібне блокування.
  • CompletionHandler — трохи складніший, але дає справжню асинхронність і не блокує потоки.

4. Практика: асинхронний запис у файл з 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.io.IOException;
import java.nio.charset.StandardCharsets;

public class CompletionHandlerWriteExample {
    public static void main(String[] args) throws IOException {
        String text = "Привіт, асинхронний світе!";
        ByteBuffer buffer = ByteBuffer.wrap(text.getBytes(StandardCharsets.UTF_8));
        Path path = Path.of("async_output.txt");

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

        channel.write(buffer, 0, path, new CompletionHandler<Integer, Path>() {
            @Override
            public void completed(Integer result, Path attachment) {
                System.out.println("Успішно записано " + result + " байтів у файл " + attachment);
                try {
                    channel.close();
                } catch (IOException e) {
                    System.err.println("Помилка закриття каналу: " + e);
                }
            }

            @Override
            public void failed(Throwable exc, Path attachment) {
                System.err.println("Помилка запису у файл " + attachment + ": " + exc);
                try {
                    channel.close();
                } catch (IOException e) {
                    System.err.println("Помилка закриття каналу: " + e);
                }
            }
        });

        // Притримаємо main, щоб побачити результат (лише для прикладу)
        try {
            Thread.sleep(500);
        } catch (InterruptedException ignored) {}
    }
}
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ