Java 8 est là, il est temps d'apprendre quelque chose de nouveau. Java 7 et Java 6 ne sont que des versions légèrement modifiées, mais Java 8 bénéficiera d'améliorations majeures. Peut-être que Java 8 est trop gros ? Aujourd'hui, je vais vous donner une explication détaillée de la nouvelle abstraction CompletableFuture dans JDK 8. Comme nous le savons tous, Java 8 sera publié dans moins d'un an, cet article est donc basé sur le JDK 8 build 88 avec support lambda. CompletableFuture étend Future fournit des méthodes, des opérateurs unaires et favorise l'asynchronicité et un modèle de programmation basé sur les événements qui ne s'arrête pas aux anciennes versions de Java. Si vous ouvrez le JavaDoc de CompletableFuture, vous serez choqué. Il existe une cinquantaine de méthodes (!), et certaines sont très intéressantes et difficiles à comprendre, par exemple :
Copiez le code comme suit : public <U,V> CompletableFuture<V> thenCombineAsync(
CompleteableFuture<? étend U> autre,
BiFonction<? super T,? super U,? étend V> fn,
exécuteur testamentaire)
Ne vous inquiétez pas, continuez à lire. CompletableFuture rassemble toutes les caractéristiques de ListenableFuture dans Guava et SettableFuture. De plus, les expressions lambda intégrées le rapprochent des futurs Scala/Akka. Cela peut paraître trop beau pour être vrai, mais poursuivez votre lecture. CompletableFuture a deux aspects principaux qui sont supérieurs au rappel/conversion asynchrone de Future dans ol, qui permet de définir la valeur de CompletableFuture à partir de n'importe quel thread à tout moment.
1. Extrayez et modifiez la valeur du package
Les futurs représentent souvent du code exécuté dans d’autres threads, mais ce n’est pas toujours le cas. Parfois, vous souhaitez créer un Future pour indiquer que vous savez ce qui va se passer, comme l'arrivée d'un message JMS. Vous avez donc un avenir mais pas de travail asynchrone potentiel dans le futur. Vous voulez simplement avoir terminé (résolu) lorsqu'un futur message JMS arrive, qui est piloté par un événement. Dans ce cas, vous pouvez simplement créer un CompletableFuture à retourner à votre client, et simplement complete() débloquera tous les clients en attente du Future tant que vous pensez que votre résultat est disponible.
Tout d’abord, vous pouvez simplement créer un nouveau CompletableFuture et le donner à votre client :
Copiez le code comme suit : public CompletableFuture<String> Ask() {
final CompletableFuture<String> future = new CompletableFuture<>();
//...
retour futur;
}
Notez que ce futur n'a aucune connexion avec Callable, il n'y a pas de pool de threads et il ne fonctionne pas de manière asynchrone. Si le code client appelle maintenant request().get(), il bloquera pour toujours. Si les registres terminent le rappel, ils ne prendront jamais effet. Alors, quelle est la clé ? Maintenant, vous pouvez dire :
Copiez le code comme suit : future.complete("42")
...À ce moment, tous les clients Future.get() obtiendront le résultat de la chaîne, et cela prendra effet immédiatement après avoir terminé le rappel. Ceci est très pratique lorsque vous souhaitez représenter la tâche d'un Future et qu'il n'est pas nécessaire de calculer la tâche d'un thread d'exécution. CompletableFuture.complete() ne peut être appelé qu'une seule fois, les appels suivants seront ignorés. Mais il existe également une porte dérobée appelée CompletableFuture.obtrudeValue(...) qui écrase la valeur précédente d'un nouveau Future, veuillez donc l'utiliser avec prudence.
Parfois, vous souhaitez voir ce qui se passe lorsqu'un signal échoue, car vous savez qu'un objet Future peut gérer le résultat ou l'exception qu'il contient. Si vous souhaitez transmettre certaines exceptions plus loin, vous pouvez utiliser CompletableFuture.completeExceptionally(ex) (ou utiliser une méthode plus puissante comme obtrudeException(ex) pour remplacer l'exception précédente). completeExceptionally() déverrouille également tous les clients en attente, mais cette fois lève une exception de get(). En parlant de get(), il existe également la méthode CompletableFuture.join() avec des changements subtils dans la gestion des erreurs. Mais dans l’ensemble, ils sont tous pareils. Enfin, il existe la méthode CompletableFuture.getNow(valueIfAbsent) qui ne bloque pas mais renverra la valeur par défaut si le Future n'est pas encore terminé, ce qui la rend très utile lors de la construction de systèmes robustes où l'on ne veut pas attendre trop longtemps.
La méthode statique finale consiste à utilisercompleteFuture(value) pour renvoyer l'objet Future terminé, ce qui peut être très utile lors du test ou de l'écriture de certaines couches d'adaptateur.
2. Créez et obtenez CompleteableFuture
D'accord, donc créer manuellement un CompletableFuture est notre seule option ? incertain. Tout comme les Futures classiques, nous pouvons associer des tâches existantes et CompletableFuture utilise des méthodes d'usine :
Copiez le code comme suit :
static <U> CompleteableFuture<U> supplyAsync(Supplier<U> fournisseur);
static <U> CompleteableFuture<U> supplyAsync(Supplier<U> fournisseur, Executor exécuteur);
static CompletableFuture<Void> runAsync(Runnable runnable);
static CompletableFuture<Void> runAsync(Runnable runnable, Executor exécuteur);
La méthode sans paramètre Executor se termine par...Async et utilisera ForkJoinPool.commonPool() (pool commun global introduit dans JDK8), qui s'applique à la plupart des méthodes de la classe CompletableFuture. runAsync() est facile à comprendre, notez qu'il nécessite un Runnable, il renvoie donc CompletableFuture<Void> car le Runnable ne renvoie aucune valeur. Si vous devez gérer des opérations asynchrones et renvoyer des résultats, utilisez Supplier<U> :
Copiez le code comme suit :
final CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
@Outrepasser
chaîne publique get() {
//...de longue date...
renvoyer "42" ;
}
}, exécuteur testamentaire);
Mais n’oubliez pas qu’il existe des expressions lambdas dans Java 8 !
Copiez le code comme suit :
finalCompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
//...de longue date...
renvoyer "42" ;
}, exécuteur testamentaire);
ou:
Copiez le code comme suit :
final CompletableFuture<String> futur =
CompleteableFuture.supplyAsync(() -> longRunningTask(params), exécuteur);
Bien que cet article ne concerne pas les lambdas, j'utilise assez fréquemment les expressions lambda.
3. Conversion et action sur CompletableFuture (thenApply)
J'ai dit que CompletableFuture est meilleur que Future mais vous ne savez pas pourquoi ? En termes simples, car CompletableFuture est un atome et un facteur. Ce que j'ai dit n'est-il pas utile ? Scala et JavaScript vous permettent tous deux d'enregistrer un rappel asynchrone lorsqu'un futur se termine, et nous n'avons pas besoin d'attendre et de le bloquer jusqu'à ce qu'il soit prêt. On peut simplement dire : lorsque vous exécutez cette fonction, le résultat apparaît. De plus, nous pouvons empiler ces fonctions, combiner plusieurs futurs ensemble, etc. Par exemple, si nous convertissons de String en Integer, nous pouvons convertir de CompletableFuture en CompletableFuture<Integer sans association. Cela se fait via thenApply() :
Copiez le code comme suit :
<U> CompleteableFuture<U> thenApply(Function<? super T,? extends U> fn);
<U> CompleteableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn);
<U> CompleteableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor exécuteur);<p></p>
<p>Comme mentionné... la version Async fournit la plupart des opérations sur CompletableFuture, je les ignorerai donc dans les sections suivantes. N'oubliez pas que la première méthode appellera la méthode dans le même thread où le futur est terminé, tandis que les deux autres l'appelleront de manière asynchrone dans différents pools de threads.
Jetons un coup d'œil au workflow de thenApply() :</p>
<p><pré>
CompleteableFuture<String> f1 = //...
CompleteableFuture<Integer> f2 = f1.thenApply(Integer::parseInt);
CompleteableFuture<Double> f3 = f2.thenApply(r -> r * r * Math.PI);
</p>
Ou dans une déclaration :
Copiez le code comme suit :
CompletableFuture<Double> f3 =
f1.thenApply(Integer::parseInt).thenApply(r -> r * r * Math.PI);
Ici, vous verrez la conversion d'une séquence, de String en Integer à Double. Mais plus important encore, ces transformations ne s’exécutent pas immédiatement et ne s’arrêtent pas. Ces transformations ne s'exécutent ni immédiatement ni ne s'arrêtent. Ils se souviennent simplement du programme qu’ils ont exécuté lorsque le f1 original s’est terminé. Si certaines transformations prennent beaucoup de temps, vous pouvez fournir votre propre Executor pour les exécuter de manière asynchrone. Notez que cette opération est équivalente à une carte unaire en Scala.
4. Exécutez le code complété (thenAccept/thenRun)
Copiez le code comme suit :
CompleteableFuture<Void> thenAccept(Consumer<? super T> block);
CompleteableFuture<Void> thenRun (action exécutable);
Il existe deux méthodes typiques de l'étape « finale » dans les futurs pipelines. Ils sont préparés lorsque vous utilisez la valeur future. Lorsque thenAccept() fournit la valeur finale, thenRun exécute Runnable, qui n'a même pas de moyen de calculer la valeur. Par exemple:
Copiez le code comme suit :
future.thenAcceptAsync(dbl -> log.debug("Result: {}", dbl), exécuteur);
log.debug("Continu");
...Les variables asynchrones sont également disponibles de deux manières, exécuteurs implicites et explicites, et je n'insisterai pas trop sur cette méthode.
Les méthodes thenAccept()/thenRun() ne bloquent pas (même s'il n'y a pas d'exécuteur explicite). Ils sont comme un écouteur/gestionnaire d'événements, qui s'exécutera pendant un certain temps lorsque vous le connecterez à un futur. Le message « Continuant » apparaîtra immédiatement, même si le futur n'est même pas terminé.
5. Gestion des erreurs d'un seul CompletableFuture
Jusqu’à présent, nous n’avons discuté que des résultats des calculs. Et les exceptions ? Pouvons-nous les gérer de manière asynchrone ? certainement!
Copiez le code comme suit :
CompleteableFuture<String> safe =
future.exceptionally(ex -> "Nous avons un problème : " + ex.getMessage());
Lorsque exceptionnellement() accepte une fonction, le futur d'origine sera appelé pour lever une exception. Nous aurons l'opportunité de convertir cette exception en une valeur compatible avec le type Future à récupérer. Les conversions safeFurther ne déclencheront plus d'exception mais renverront à la place une valeur String de la fonction fournissant la fonctionnalité.
Une approche plus flexible consiste à ce que handle() accepte une fonction qui reçoit le résultat ou l'exception correct :
Copiez le code comme suit :
CompleteableFuture<Integer> safe = future.handle((ok, ex) -> {
si (ok != nul) {
return Integer.parseInt(ok);
} autre {
log.warn("Problème", ex);
renvoie -1 ;
}
});
handle() est toujours appelé, et les résultats et les exceptions ne sont pas nuls. Il s'agit d'une stratégie globale à guichet unique.
6. Combinez deux CompleteableFutures ensemble
CompleteableFuture en tant que processus asynchrone est formidable, mais il montre vraiment à quel point il est puissant lorsque plusieurs de ces futurs sont combinés de diverses manières.
7. Combinez (lien) ces deux futurs (thenCompose())
Parfois, vous souhaitez exécuter une valeur future (quand elle est prête), mais cette fonction renvoie également une valeur future. CompletableFuture est suffisamment flexible pour comprendre que le résultat de notre fonction doit désormais être utilisé comme un futur de premier niveau, par rapport à CompletableFuture<CompletableFuture>. La méthode thenCompose() est équivalente au flatMap de Scala :
Copiez le code comme suit :
<U> CompletableFuture<U> thenCompose(Function<? super T,CompletableFuture<U>> fn);
...Des variantes asynchrones sont également disponibles.Dans l'exemple suivant, observez attentivement les types et les différences entre thenApply()(map) et thenCompose()(flatMap). Lors de l'application de la méthode calculateRelevance(), un CompletableFuture est renvoyé :
Copiez le code comme suit :
CompleteableFuture<Document> docFuture = //...
CompletableFuture<CompletableFuture<Double>> f =
docFuture.thenApply(this::calculateRelevance);
CompleteableFuture<Double> pertinenceFuture =
docFuture.thenCompose(this::calculateRelevance);
//...
private CompletableFuture<Double> calculateRelevance(Document doc) //...
thenCompose() est une méthode importante qui permet de créer des pipelines robustes et asynchrones sans bloquer ni attendre les étapes intermédiaires.
8. Valeurs de conversion de deux contrats à terme (thenCombine())
Lorsque thenCompose() est utilisé pour enchaîner un futur qui dépend d'un autre thenCombine, lorsqu'ils sont tous deux terminés, il combine les deux futurs indépendants :
Copiez le code comme suit :
<U,V> CompletableFuture<V> thenCombine(CompletableFuture<? extends U> autre, BiFunction<? super T,? super U,? extends V> fn)
... Des variables asynchrones sont également disponibles, en supposant que vous disposez de deux CompletableFutures, l'une chargeant le client et l'autre chargeant la boutique récente. Ils sont complètement indépendants les uns des autres, mais une fois terminés, vous souhaitez utiliser leurs valeurs pour calculer l'itinéraire. Voici un exemple privable :
Copiez le code comme suit :
CompleteableFuture<Client> customerFuture = loadCustomerDetails(123);
CompleteableFuture<Shop> shopFuture = closeShop();
CompleteableFuture<Route> routeFuture =
customerFuture.thenCombine(shopFuture, (cust, shop) -> findRoute(cust, shop));
//...
itinéraire privé findRoute(Client client, Boutique) //...
Veuillez noter que dans Java 8, vous pouvez simplement remplacer la référence à la méthode this::findRoute par (cust, shop) -> findRoute(cust, shop) :
Copiez le code comme suit :
customerFuture.thenCombine(shopFuture, this::findRoute);
Comme vous le savez, nous avons customerFuture et shopFuture. Ensuite, routeFuture les enveloppe et « attend » qu’ils se terminent. Lorsqu'ils seront prêts, il exécutera la fonction que nous avons fournie pour combiner tous les résultats (findRoute()). Cette routeFuture se terminera lorsque les deux futurs de base seront terminés et que findRoute() se terminera également.
9. Attendez que tous les CompletableFutures soient terminés
Si au lieu de générer un nouveau CompletableFuture reliant ces deux résultats, nous voulons simplement être avertis lorsqu'il est terminé, nous pouvons utiliser la série de méthodes thenAcceptBoth()/runAfterBoth(), (...des variables asynchrones sont également disponibles). Ils fonctionnent de manière similaire à thenAccept() et thenRun(), mais attendent deux futurs au lieu d'un :
Copiez le code comme suit :
<U> CompleteableFuture<Void> thenAcceptBoth (CompletableFuture<? extends U> autre, BiConsumer<? super T,? super U> bloc)
CompleteableFuture<Void> runAfterBoth(CompletableFuture<?> autre, action exécutable)
Imaginez l'exemple ci-dessus, au lieu de générer un nouveau CompletableFuture, vous souhaitez simplement envoyer quelques événements ou actualiser l'interface graphique immédiatement. Cela peut être facilement réalisé : thenAcceptBoth() :
Copiez le code comme suit :
customerFuture.thenAcceptBoth(shopFuture, (cust, shop) -> {
route finale route = findRoute(client, boutique);
//rafraîchir l'interface graphique avec la route
});
J'espère que je me trompe, mais peut-être que certains se poseront une question : pourquoi ne puis-je pas simplement bloquer ces deux futurs ? Comme:
Copiez le code comme suit :
Future<Client> customerFuture = loadCustomerDetails(123);
Future<Shop> shopFuture = closeShop();
findRoute(customerFuture.get(), shopFuture.get());
Eh bien, bien sûr, vous pouvez le faire. Mais le point le plus critique est que CompletableFuture permet l'asynchronisme. Il s'agit d'un modèle de programmation basé sur les événements plutôt que de bloquer et d'attendre avec impatience les résultats. Donc fonctionnellement, les deux parties de code ci-dessus sont équivalentes, mais cette dernière n'a pas besoin d'occuper un thread pour s'exécuter.
10. Attendez que le premier CompletableFuture termine la tâche
Une autre chose intéressante est que CompletableFutureAPI peut attendre que le premier futur (par opposition à tous) soit terminé. C'est très pratique lorsque vous disposez des résultats de deux tâches du même type. Vous ne vous souciez que du temps de réponse et aucune tâche n'est prioritaire. Méthodes API (… Des variables asynchrones sont également disponibles) :
Copiez le code comme suit :
CompletableFuture<Void> acceptEither(CompletableFuture<? extends T> other, Consumer<? super T> bloc)
CompleteableFuture<Void> runAfterEither(CompletableFuture<?> autre, action exécutable)
A titre d’exemple, vous disposez de deux systèmes pouvant être intégrés. L’un a un temps de réponse moyen plus court mais un écart type élevé, l’autre est généralement plus lent mais plus prévisible. Pour tirer le meilleur parti des deux mondes (performances et prévisibilité), vous pouvez appeler les deux systèmes en même temps et attendre celui qui se termine en premier. Habituellement, ce sera le premier système, mais lorsque la progression devient lente, le deuxième système peut se terminer dans un délai acceptable :
Copiez le code comme suit :
CompleteableFuture<String> fast = fetchFast();
CompleteableFuture<String> prévisible = fetchPredictably();
fast.acceptEither (prévisible, s -> {
System.out.println("Résultat : " + s);
});
s représente la chaîne obtenue à partir de fetchFast() ou fetchPredictably(). Nous n’avons pas besoin de savoir ou de nous en soucier.
11. Convertissez complètement le premier système
applyToEither() est considéré comme le prédécesseur de acceptEither(). Lorsque deux futurs sont sur le point de se terminer, ce dernier appelle simplement un extrait de code et applyToEither() renverra un nouveau futur. Lorsque ces deux futurs initiaux seront terminés, le nouveau futur sera également terminé. L'API est quelque peu similaire (...des variables asynchrones sont également disponibles) :
Copiez le code comme suit :<U> CompletableFuture<U> applyToEither(CompletableFuture<? extends T> other, Function<? super T,U> fn)
Cette fonction fn supplémentaire peut être complétée lorsque le premier futur est appelé. Je ne suis pas sûr du but de cette méthode spécialisée, après tout, on pourrait simplement utiliser : fast.applyToEither(predictable).thenApply(fn). Puisque nous sommes coincés avec cette API, mais que nous n'avons pas vraiment besoin de fonctionnalités supplémentaires pour l'application, j'utiliserai simplement l'espace réservé Function.identity() :
Copiez le code comme suit :
CompleteableFuture<String> fast = fetchFast();
CompleteableFuture<String> prévisible = fetchPredictably();
CompleteableFuture<String> firstDone =
fast.applyToEither(predictable, Function.<String>identity());
Le premier futur terminé peut être exécuté. Notez que du point de vue du client, les deux futurs sont en réalité cachés derrière firstDone. Le client attend simplement que le futur se termine et utilise applyToEither() pour avertir le client lorsque les deux premières tâches sont terminées.
12. CompleteableFuture avec plusieurs combinaisons
Nous savons maintenant comment attendre que deux futurs se terminent (en utilisant thenCombine()) et que le premier se termine (applyToEither()). Mais peut-il s’adapter à un nombre illimité de futurs ? En effet, utilisez des méthodes d'assistance statiques :
Copiez le code comme suit :
statique CompletableFuture<Void< allOf(CompletableFuture<?<... cfs)
statique CompletableFuture<Object<anyOf(CompletableFuture<?<... cfs)
allOf() utilise un tableau de futurs et renvoie un futur (en attendant tous les obstacles) lorsque tous les futurs potentiels sont terminés. D'un autre côté, anyOf() attendra les contrats à terme potentiels les plus rapides. Veuillez regarder le type général de contrats à terme renvoyés. N'est-ce pas ce à quoi vous vous attendez ? Nous nous concentrerons sur cette question dans le prochain article.
Résumer
Nous avons exploré l’intégralité de l’API CompletableFuture. Je suis convaincu que cela sera invincible, c'est pourquoi dans le prochain article, nous examinerons la mise en œuvre d'un autre robot d'exploration Web simple utilisant les méthodes CompletableFuture et les expressions lambda Java 8. Nous examinerons également CompletableFuture.