1. Обробка помилок в асинхронних операціях
У синхронному коді все просто: якщо файл не знайдено або немає доступу, ви одразу перехоплюєте виняток у try-catch. В асинхронному коді, особливо коли використовуєте колбеки (CompletionHandler), помилка може статися вже після того, як ваш метод завершився — десь у надрах пулу потоків. Якщо не обробити її правильно, програма може поводитися непередбачувано: від «тихої» втрати даних до падіння всього застосунку.
Як помилки передаються в CompletionHandler?
В інтерфейсі CompletionHandler<V, A> є два методи:
- completed(V result, A attachment) — викликається, якщо операція пройшла успішно.
- failed(Throwable exc, A attachment) — викликається, якщо сталася помилка.
Ось приклад використання:
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;
public class AsyncErrorDemo {
public static void main(String[] args) throws Exception {
Path path = Paths.get("nonexistent.txt");
ByteBuffer buffer = ByteBuffer.allocate(1024);
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("Успішно прочитано " + result + " байтів");
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("Помилка читання файлу: " + exc.getMessage());
// Можна логувати, сповіщати користувача, або прокинути виняток далі
}
});
} catch (IOException ex) {
System.out.println("Помилка відкриття файлу: " + ex.getMessage());
}
// Дамо час асинхронній операції завершитися (у реальних застосунках використовуйте CountDownLatch або інші механізми)
Thread.sleep(500);
}
}
Що тут відбувається?
- Якщо файл не існує, метод failed буде викликано з відповідним винятком (NoSuchFileException).
- Якщо операцію завершено успішно — спрацює completed.
Приклади типових помилок
- Файл не знайдено: NoSuchFileException
- Немає доступу: AccessDeniedException
- Помилка читання/запису: різні підкласи IOException
- Проблеми з буфером: BufferOverflowException, BufferUnderflowException
Логування та сповіщення користувача
Помилка в асинхронному колбеку — не привід для паніки, але й не привід робити вигляд, що нічого не сталося. Хороша практика — логувати помилку (наприклад, через Logger), а якщо це важливо для користувача — показати повідомлення або викликати обробник в UI.
Приклад логування:
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.err.println("Помилка асинхронної операції: " + exc);
exc.printStackTrace();
}
У продакшн-коді використовуйте повноцінні логери (наприклад, java.util.logging або Log4j), а не System.err.
2. Скасування асинхронних операцій
Коли може знадобитися скасування операції?
Іноді асинхронне завдання потрібно зупинити прямо посеред роботи. Наприклад, користувач передумав і натиснув «Скасувати» під час завантаження файлу. Або вікно програми закрилося, і операція більше не має сенсу. Буває й так, що під час завершення програми просто потрібно акуратно звільнити ресурси.
Для таких випадків асинхронний ввід-вивід у Java підтримує скасування через інтерфейс Future. З його допомогою можна в будь-який момент перервати завдання, що виконується, і не витрачати ресурси намарно.
Як скасувати операцію за допомогою Future?
Методи read або write в AsynchronousFileChannel повертають об’єкт Future<Integer>. У цього об’єкта є метод cancel(boolean mayInterruptIfRunning).
Приклад: скасування асинхронного читання
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;
public class AsyncCancelDemo {
public static void main(String[] args) throws Exception {
Path path = Paths.get("bigfile.txt");
ByteBuffer buffer = ByteBuffer.allocate(1024);
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
Future<Integer> future = channel.read(buffer, 0);
// Трохи зачекаємо, а потім скасуємо операцію
Thread.sleep(100);
boolean cancelled = future.cancel(true);
if (cancelled) {
System.out.println("Операцію читання скасовано!");
} else {
System.out.println("Не вдалося скасувати операцію (можливо, її вже завершено)");
}
}
}
}
Важливі нюанси:
- Скасування працює лише для операцій, які ще не завершилися.
- Якщо операцію вже завершено — скасувати її не вийде.
- Після скасування, під час спроби викликати get() на цьому Future, буде кинуто виняток CancellationException.
Коли скасувати операцію вже не можна?
Якщо завдання встигло завершитися — хоч успішно, хоч із помилкою — зупинити його вже неможливо, поїзд пішов.
Крім того, не всі реалізації справді вміють переривати операції на рівні операційної системи. Наприклад, під час роботи з деякими файловими системами «скасування» буде суто символічною: операція продовжить виконуватися, але результат ви просто проігноруєте.
3. Практика: обробка помилок і скасування
Приклад 1: обробка помилки під час читання неіснуючого файлу
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.io.IOException;
public class AsyncErrorExample {
public static void main(String[] args) throws Exception {
Path path = Paths.get("no_such_file.txt");
ByteBuffer buffer = ByteBuffer.allocate(1024);
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("Операцію успішно завершено");
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("Помилка під час читання файлу: " + exc.getClass().getSimpleName() + " - " + exc.getMessage());
}
});
} catch (IOException ex) {
System.out.println("Помилка під час відкриття файлу: " + ex.getMessage());
}
Thread.sleep(500);
}
}
Що побачимо в консолі?
Помилка під час відкриття файлу: no_such_file.txt
або, якщо помилка виникне саме під час читання, а не відкриття:
Помилка під час читання файлу: NoSuchFileException - no_such_file.txt
Приклад 2: скасування довгої операції та коректне завершення
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;
public class AsyncCancelExample {
public static void main(String[] args) throws Exception {
Path path = Paths.get("bigfile.txt");
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 10 МБ
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
Future<Integer> future = channel.read(buffer, 0);
// Через 50 мс скасовуємо операцію (для експерименту)
Thread.sleep(50);
boolean cancelled = future.cancel(true);
if (cancelled) {
System.out.println("Операцію читання було скасовано!");
} else {
System.out.println("Не вдалося скасувати операцію (ймовірно, її вже завершено)");
}
try {
// Спробуємо отримати результат (буде кинуто CancellationException)
future.get();
} catch (java.util.concurrent.CancellationException ex) {
System.out.println("Спіймали CancellationException: операцію справді скасовано.");
}
}
}
}
4. Найкращі практики: як робити правильно
Звільняйте ресурси навіть у разі помилок
Використовуйте try-with-resources для автоматичного закриття каналів:
try (AsynchronousFileChannel channel = /* open channel */ null) {
// ...
}
Якщо використовуєте CompletionHandler, не забудьте закрити канал після завершення всіх операцій. Це особливо важливо, якщо виконуєте кілька асинхронних операцій підряд.
Не блокуйте UI/основний потік
Асинхронні операції потрібні для того, щоб не блокувати основний потік. Не викликайте future.get() в UI‑потоці — інакше сенс асинхронності втрачається.
Логуйте всі помилки
У CompletionHandler завжди реалізовуйте метод failed і логуйте (або передавайте далі) всі винятки.
Перевіряйте завершення всіх операцій перед завершенням програми
Якщо програма завершиться до того, як операцію буде закінчено, результат може бути втрачено. Для демонстрацій у консолі іноді доводиться робити Thread.sleep(500), але в реальних застосунках використовуйте CountDownLatch, CompletableFuture або інші механізми синхронізації.
Не забувайте про скасування
Якщо операція більше не потрібна (наприклад, користувач закрив вікно), скасовуйте її через Future.cancel. Це заощадить ресурси та пришвидшить відгук програми.
5. Типові помилки під час обробки помилок і скасування в async IO
Помилка № 1: Ігнорування методу failed у CompletionHandler.
Якщо не реалізувати обробку помилок, ваша програма поводитиметься непередбачувано: помилки «загубляться», а користувач не розумітиме, чому нічого не відбувається.
Помилка № 2: Канал не закрито після завершення операцій.
Забули закрити AsynchronousFileChannel — отримаєте витік ресурсів і, можливо, блокування файлу в ОС.
Помилка № 3: Очікування результату асинхронної операції в головному потоці.
Викликали future.get() у UI‑потоці — інтерфейс «завис», і вся асинхронність пішла нанівець.
Помилка № 4: Спроба скасувати вже завершену операцію.
Викликали cancel() надто пізно — операцію вже завершено, скасування не спрацює. Це не критично, але може збити з пантелику під час відладки.
Помилка № 5: Не перевіряєте результат скасування.
Викликали cancel(), але не перевірили значення, що повертається, і не обробили CancellationException під час виклику get() — програма може впасти або поводитися дивно.
Помилка № 6: Не звільняєте ресурси у разі помилки або скасування.
Якщо канал не закрити після помилки або скасування, можуть виникнути витоки або блокування файлів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ