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), чтобы интерфейс не «вис», и полезно на серверах — потоки не простаивают в ожидании, а используются только при наличии работы.
4. Сравнение Future и CompletionHandler
| Подход | Блокирует поток? | Когда использовать? | Пример сценария |
|---|---|---|---|
| Future | Да (при get()) | Простая последовательная обработка | Копирование файла, отчёты |
| CompletionHandler | Нет | Truly async, UI, сервер, параллельные операции | Сервер, GUI, массовый IO |
- Future — проще для понимания, но для получения результата требуется блокировка.
- CompletionHandler — чуть сложнее, но даёт настоящую асинхронность и не блокирует потоки.
5. Практика: Асинхронная запись в файл с 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) {}
}
}
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ