1. Future : « J’attendrai que ce soit prêt »
Les opérations asynchrones en Java, c’est comme envoyer une lettre par la poste : vous ne restez pas devant la boîte à attendre la réponse, vous vaquez à vos occupations. Quand la lettre arrive — vous recevez simplement une notification. Dans le code, il faut comprendre que l’opération est terminée et récupérer le résultat ou l’erreur. En Java, il existe deux approches : via Future et via CompletionHandler.
Avec Future, vous recevez un « reçu » — une promesse d’un résultat futur. Vous pouvez vérifier de temps en temps s’il est prêt, ou attendre le résultat en appelant get().
Avec CompletionHandler, vous n’avez pas besoin d’attendre : vous fournissez à l’avance un gestionnaire, et il sera appelé automatiquement — en cas de succès ou d’échec.
Comment ça fonctionne ?
Lorsque vous appelez la méthode asynchrone read() ou write() d’AsynchronousFileChannel, vous pouvez obtenir un objet de type Future<Integer>. C’est comme un ticket dans une file d’attente électronique : l’opération a commencé et vous pouvez, à tout moment, demander : « Alors, c’est prêt ? ».
Signature de méthode
Future<Integer> read(ByteBuffer dst, long position)
ou
Future<Integer> write(ByteBuffer src, long position)
- dst — le tampon dans lequel lire les données.
- src — le tampon depuis lequel écrire les données.
- position — la position dans le fichier pour la lecture/écriture.
Exemple : lecture asynchrone avec Future
Un code qui lit de façon asynchrone les premiers 1024 octets d’un fichier et attend explicitement le résultat :
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);
// On lance la lecture asynchrone
Future<Integer> future = channel.read(buffer, 0);
// ... ici on peut faire autre chose pendant que le fichier est lu
// On attend la fin de l’opération (appel bloquant !)
int bytesRead = future.get(); // Peut lever InterruptedException, ExecutionException
System.out.println("Octets lus : " + bytesRead);
// On passe à la lecture depuis le tampon
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
} catch (Exception e) {
System.err.println("Erreur de lecture du fichier : " + e);
}
}
}
- La méthode future.get() bloque le thread jusqu’à la fin de l’opération — c’est comme « attendre au téléphone » jusqu’à l’appel.
- Vous pouvez appeler future.isDone() pour savoir si l’opération est terminée, puis seulement appeler get().
- Si l’opération se termine par une erreur, get() lèvera une ExecutionException (la cause est à l’intérieur).
Quand Future est-il pratique ?
Future est utile lorsque vous voulez lancer une tâche puis, au moment opportun, attendre le résultat : lire un fichier, faire une requête, effectuer un calcul en arrière-plan. L’approche est pratique aussi pour lancer plusieurs opérations en parallèle puis collecter les résultats.
Mais si vous avez besoin d’une vraie asynchronie sans blocage et avec notification automatique d’achèvement, envisagez CompletionHandler.
2. CompletionHandler : « Appelle-moi quand tu auras fini »
CompletionHandler est une interface que vous implémentez et que vous passez à la méthode read() ou write(). Lorsque l’opération se termine (ou qu’une erreur survient), Java appelle automatiquement votre gestionnaire. C’est comme laisser un numéro de téléphone : « Appelez quand ce sera prêt ».
Signature de méthode
void read(ByteBuffer dst,
long position,
A attachment,
CompletionHandler<Integer, ? super A> handler)
- dst — tampon de lecture.
- position — position dans le fichier.
- attachment — un objet arbitraire qui sera passé au gestionnaire (peut être null ou, par exemple, un nom de fichier).
- handler — votre gestionnaire.
Interface CompletionHandler
public interface CompletionHandler<V, A> {
void completed(V result, A attachment);
void failed(Throwable exc, A attachment);
}
- completed est appelé en cas de succès ; result — le nombre d’octets, attachment — votre objet.
- failed est appelé en cas d’erreur ; exc — l’exception.
Exemple : lecture asynchrone avec 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("Fichier " + attachment + " lu, octets : " + result);
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
closeChannel();
}
@Override
public void failed(Throwable exc, String attachment) {
System.err.println("Erreur de lecture du fichier " + attachment + " : " + exc);
closeChannel();
}
private void closeChannel() {
try {
channel.close();
} catch (IOException e) {
System.err.println("Erreur lors de la fermeture du canal : " + e);
}
}
});
// Important : la méthode main se terminera avant la fin de la lecture si on ne 'retient' pas le thread !
// Dans les applications réelles, le thread ne se termine généralement pas tout de suite (par exemple, serveur, UI).
try {
Thread.sleep(500); // Laissons du temps à l’opération asynchrone (pour l’exemple uniquement !)
} catch (InterruptedException ignored) {}
}
}
Nous passons à read() un gestionnaire anonyme qui sait quoi faire après l’achèvement. Dans completed, nous recevons le résultat et le traitons, et dans failed — nous réagissons à l’erreur. N’oubliez pas de fermer le canal à l’intérieur du gestionnaire afin de ne pas laisser de ressources ouvertes.
Il y a un point d’attention : la méthode main peut se terminer avant la fin de l’opération asynchrone, c’est pourquoi Thread.sleep(500) a été ajouté — uniquement pour voir le résultat. Dans les applications réelles, cette astuce n’est généralement pas nécessaire.
Quand est-il préférable d’utiliser CompletionHandler ?
CompletionHandler est idéal lorsque vous avez besoin d’une vraie asynchronie sans attentes ni blocages : vous lancez l’opération et décrivez quoi faire à l’achèvement. C’est crucial pour l’UI (JavaFX, Swing) afin que l’interface ne se « fige » pas, et utile sur les serveurs — les threads ne restent pas inactifs en attente, ils ne sont utilisés que lorsqu’il y a du travail.
4. Comparaison de Future et CompletionHandler
| Approche | Bloque le thread ? | Quand l’utiliser ? | Exemple de scénario |
|---|---|---|---|
| Future | Oui (lors de get()) | Traitement séquentiel simple | Copie de fichier, rapports |
| CompletionHandler | Non | Vraiment asynchrone, UI, serveur, opérations parallèles | Serveur, GUI, IO massif |
- Future — plus simple à comprendre, mais nécessite un blocage pour obtenir le résultat.
- CompletionHandler — un peu plus complexe, mais fournit une véritable asynchronie et ne bloque pas les threads.
5. Pratique : écriture asynchrone dans un fichier avec CompletionHandler
Un exemple qui écrit de manière asynchrone une chaîne dans un fichier et signale le résultat :
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 = "Bonjour, monde asynchrone !";
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("Écrit avec succès " + result + " octets dans le fichier " + attachment);
try {
channel.close();
} catch (IOException e) {
System.err.println("Erreur lors de la fermeture du canal : " + e);
}
}
@Override
public void failed(Throwable exc, Path attachment) {
System.err.println("Erreur d’écriture dans le fichier " + attachment + " : " + exc);
try {
channel.close();
} catch (IOException e) {
System.err.println("Erreur lors de la fermeture du canal : " + e);
}
}
});
// On 'retient' main pour voir le résultat (pour l’exemple seulement)
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {}
}
}
GO TO FULL VERSION