Vodič kroz kompletibilnu budućnost

1. Uvod

Ovaj je vodič vodič za funkcionalnost i slučajeve upotrebe CompletableFuture klasa koja je predstavljena kao poboljšanje API-ja Java 8 Concurrency.

2. Asinkrono računanje u Javi

O asinkronom računanju teško je razmišljati. Obično o bilo kojem računanju želimo razmišljati kao o nizu koraka, ali u slučaju asinkronog računanja, radnje predstavljene kao povratni pozivi obično su raštrkane po kodu ili duboko ugniježđene jedna u drugu. Stvari se pogoršavaju kada trebamo riješiti pogreške koje bi se mogle dogoditi tijekom jednog od koraka.

The Budućnost sučelje je dodano u Javi 5 kako bi poslužilo kao rezultat asinkronog računanja, ali nije imalo nikakve metode za kombiniranje tih izračuna ili obradu mogućih pogrešaka.

Java 8 je predstavila CompletableFuture razred. Zajedno sa Budućnost sučelje, također je implementirao CompletionStage sučelje. Ovo sučelje definira ugovor za asinkroni korak izračuna koji možemo kombinirati s drugim koracima.

CompletableFuture je istodobno gradivni blok i okvir, sa oko 50 različitih metoda za sastavljanje, kombiniranje i izvršavanje asinkronih koraka računanja i rukovanje pogreškama.

Tako velik API može biti neodoljiv, ali uglavnom pada u nekoliko jasnih i različitih slučajeva korištenja.

3. Korištenje CompletableFuture kao Jednostavan Budućnost

Prije svega, CompletableFuture razred provodi Budućnost sučelje, pa možemo koristite ga kao Budućnost implementacija, ali s dodatnom logikom dovršenja.

Na primjer, možemo stvoriti primjerak ove klase s konstruktorom no-arg koji će predstavljati neki budući rezultat, predati ga potrošačima i dovršiti ga u neko vrijeme u budućnosti pomoću dovršen metoda. Potrošači mogu koristiti dobiti metoda za blokiranje trenutne niti dok se ne dobije taj rezultat.

U donjem primjeru imamo metodu koja stvara a CompletableFuture instanci, zatim odvoji neko računanje u drugoj niti i vrati datoteku Budućnost odmah.

Kada je izračunavanje završeno, metoda dovršava Budućnost pružanjem rezultata dovršen metoda:

public Future CalcuAsync () baca InterruptedException {CompletableFuture completableFuture = new CompletableFuture (); Izvršitelji.newCachedThreadPool (). Submit (() -> {Thread.sleep (500); completableFuture.complete ("Hello"); return null;}); povratak completableFuture; }

Za odvajanje računanja koristimo Izvršitelj API. Ova metoda stvaranja i dovršavanja a CompletableFuture može se koristiti zajedno s bilo kojim mehanizmom istodobnosti ili API-jem, uključujući sirove niti.

Primijeti da the izračunajAsink metoda vraća a Budućnost primjer.

Jednostavno zovemo metodu, primamo Budućnost instance i pozovite dobiti metodu na njemu kada smo spremni blokirati rezultat.

Također primijetite da dobiti metoda baca neke provjerene iznimke, naime ExecutionException (enkapsuliranje iznimke koja se dogodila tijekom izračuna) i InterruptedException (iznimka koja znači da je prekinuta nit koja izvršava metodu):

Budućnost completableFuture = izračunajAsync (); // ... Rezultat niza = completableFuture.get (); assertEquals ("Zdravo", rezultat);

Ako već znamo rezultat izračuna, možemo koristiti statički dovršenoBudućnost metoda s argumentom koji predstavlja rezultat ovog računanja. Slijedom toga, dobiti metoda Budućnost nikada neće blokirati, umjesto toga odmah vraća ovaj rezultat:

Future completableFuture = CompletableFuture.completedFuture ("Pozdrav"); // ... Rezultat niza = completableFuture.get (); assertEquals ("Zdravo", rezultat);

Kao alternativni scenarij, možda bismo htjeli otkazati izvršenje a Budućnost.

4. CompletableFuture s enkapsuliranom računskom logikom

Gornji kod omogućuje nam odabir bilo kojeg mehanizma istodobnog izvršavanja, ali što ako želimo preskočiti ovaj obrazac i jednostavno izvršiti neki kôd asinkrono?

Statičke metode runAsync i supplyAsync dopustite nam da stvorimo CompletableFuture primjer iz Izvodljivo i Dobavljač funkcionalni tipovi u skladu s tim.

Oba Izvodljivo i Dobavljač su funkcionalna sučelja koja omogućuju prosljeđivanje njihovih instanci kao lambda izraza zahvaljujući novoj značajci Java 8.

The Izvodljivo sučelje je isto staro sučelje koje se koristi u nitima i ne dopušta vraćanje vrijednosti.

The Dobavljač sučelje je generičko funkcionalno sučelje s jednom metodom koja nema argumente i vraća vrijednost parametriziranog tipa.

To nam omogućuje pružiti primjerak Dobavljač kao lambda izraz koji vrši izračun i vraća rezultat. Jednostavno je kao:

CompletableFuture future = CompletableFuture.supplyAsync (() -> "Zdravo"); // ... assertEquals ("Pozdrav", future.get ());

5. Obrada rezultata asinkronih izračunavanja

Najopćenitiji način obrade rezultata izračuna je dodavanje funkcije. The ondaPrimijeni metoda čini upravo to; prihvaća a Funkcija instance, koristi ga za obradu rezultata i vraća a Budućnost koja sadrži vrijednost koju vraća funkcija:

CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> "Pozdrav"); CompletableFuture future = completableFuture .thenApply (s -> s + "Svijet"); assertEquals ("Pozdrav svijetu", future.get ());

Ako ne trebamo vratiti vrijednost prema dolje Budućnost lanca, možemo koristiti instancu Potrošač funkcionalno sučelje. Njegova pojedinačna metoda uzima parametar i vraća se poništiti.

Postoji metoda za ovaj slučaj upotrebe u CompletableFuture. The ondaprihvati metoda prima a Potrošač i prosljeđuje mu rezultat izračuna. Zatim finale future.get () poziv vraća instancu Poništiti tip:

CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> "Pozdrav"); CompletableFuture future = completableFuture .thenAccept (s -> System.out.println ("Izračun vraćen:" + s)); future.get ();

Napokon, ako nam niti treba vrijednost izračuna, niti želimo vratiti neku vrijednost na kraju lanca, tada možemo proslijediti Izvodljivo lambda do ondaRun metoda. U sljedećem primjeru jednostavno ispisujemo redak u konzoli nakon poziva future.get ():

CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> "Pozdrav"); CompletableFuture future = completableFuture .thenRun (() -> System.out.println ("Izračun završen.")); future.get ();

6. Kombiniranje budućnosti

Najbolji dio CompletableFuture API je sposobnost kombiniranja CompletableFuture primjerci u lancu koraka računanja.

Rezultat ovog lanca je sam po sebi a CompletableFuture koji omogućuje daljnje ulančavanje i kombiniranje. Ovaj je pristup sveprisutan u funkcionalnim jezicima i često se naziva monadskim uzorkom dizajna.

U sljedećem primjeru koristimo zatimSastavi metoda za ulančavanje dva Budućnosti sekvencijalno.

Primijetite da ova metoda uzima funkciju koja vraća a CompletableFuture primjer. Argument ove funkcije rezultat je prethodnog koraka računanja. To nam omogućuje upotrebu ove vrijednosti unutar sljedećeg CompletableFutureLambda:

CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> "Pozdrav"). ThenCompose (s -> CompletableFuture.supplyAsync (() -> s + "Svijet")); assertEquals ("Pozdrav svijetu", completableFuture.get ());

The zatimSastavi metoda, zajedno s onda se prijavi, implementirati osnovne gradivne dijelove monadijskog uzorka. Oni su usko povezani s karta i flatMap metode Stream i Neobvezno klase dostupne i u Javi 8.

Obje metode primaju funkciju i primjenjuju je na rezultat izračuna, ali zatimSastavi (flatMap) metoda prima funkciju koja vraća drugi objekt iste vrste. Ova funkcionalna struktura omogućuje sastavljanje primjeraka ovih klasa kao gradivnih blokova.

Ako želimo izvršiti dva neovisna Budućnosti i učiniti nešto s njihovim rezultatima, možemo koristiti zatimKombinat metoda koja prihvaća a Budućnost i a Funkcija s dva argumenta za obradu oba rezultata:

CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> "Pozdrav"). ThenCombine (CompletableFuture.supplyAsync (() -> "Svijet"), (s1, s2) -> s1 + s2)); assertEquals ("Pozdrav svijetu", completableFuture.get ());

Jednostavniji je slučaj kada želimo nešto napraviti s dvoje Budućnosti'Rezultati, ali ne trebaju prenijeti bilo koju rezultirajuću vrijednost prema a Budućnost lanac. The thenAcceptBoth metoda je tu da pomogne:

CompletableFuture future = CompletableFuture.supplyAsync (() -> "Hello"). ThenAcceptBoth (CompletableFuture.supplyAsync (() -> "Svijet"), (s1, s2) -> System.out.println (s1 + s2));

7. Razlika između thenApply () i thenCompose ()

U našim prethodnim odjeljcima prikazali smo primjere u vezi s thenApply () i thenCompose (). Oba API-ja pomažu u lancu različitih CompletableFuture poziva, ali uporaba ove dvije funkcije je različita.

7.1. thenApply ()

Ovu metodu možemo koristiti za rad s rezultatom prethodnog poziva. Međutim, ključna stvar koju treba imati na umu jest da će se vrsta povrata kombinirati od svih poziva.

Dakle, ova je metoda korisna kada želimo transformirati rezultat a CompletableFuture poziv:

CompletableFuture finalResult = compute (). ThenApply (s-> s + 1);

7.2. thenCompose ()

The thenCompose () metoda je slična thenApply () u tom obojici vraćaju novu Fazu završetka. Međutim, thenCompose () koristi prethodnu fazu kao argument. Sravnit će se i vratiti a Budućnost s izravnim rezultatom, a ne s ugniježđenom budućnošću kako smo primijetili u thenApply ():

CompletableFuture computeAnother (Integer i) {return CompletableFuture.supplyAsync (() -> 10 + i); } CompletableFuture finalResult = compute (). ThenCompose (this :: computeAnother);

Dakle, ako je ideja lančani CompletableFuture onda je bolje koristiti thenCompose ().

Također, imajte na umu da je razlika između ove dvije metode analogna razlici između karta() i flatMap ().

8. Trčanje višestruko Budućnosti paralelno

Kad trebamo izvršiti višestruko Budućnosti paralelno, obično želimo pričekati da se svi izvrše, a zatim obraditi njihove kombinirane rezultate.

The CompletableFuture.allOf statička metoda omogućuje čekanje dovršetka svih Budućnosti predviđeno kao var-arg:

CompletableFuture future1 = CompletableFuture.supplyAsync (() -> "Zdravo"); CompletableFuture future2 = CompletableFuture.supplyAsync (() -> "Lijepo"); CompletableFuture future3 = CompletableFuture.supplyAsync (() -> "Svijet"); CompletableFuture kombinacijiFuture = CompletableFuture.allOf (budućnost1, budućnost2, budućnost3); // ... kombiniranoFuture.get (); assertTrue (future1.isDone ()); assertTrue (future2.isDone ()); assertTrue (future3.isDone ());

Primijetite da je vrsta povrata CompletableFuture.allOf () je CompletableFuture. Ograničenje ove metode je to što ne vraća kombinirane rezultate svih Budućnosti. Umjesto toga, moramo ručno dobivati ​​rezultate Budućnosti. Srećom, CompletableFuture.join () metoda i Java 8 Streams API čine to jednostavnim:

Kombinirani niz = Stream.of (future1, future2, future3) .map (CompletableFuture :: join) .collect (Collectors.joining ("")); assertEquals ("Hello Beautiful World", kombinirano);

The CompletableFuture.join () metoda slična je dobiti metoda, ali baca neprovjerenu iznimku u slučaju da Budućnost ne dovršava normalno. To ga omogućuje upotrebu kao referencu metode u Stream.map () metoda.

9. Rukovanje pogreškama

Za rukovanje pogreškama u lancu asinkronih koraka izračunavanja moramo prilagoditi baciti / uloviti idiom na sličan način.

Umjesto da uhvati iznimku u sintaktičkom bloku, CompletableFuture klasa omogućuje nam da to riješimo u posebnom drška metoda. Ova metoda prima dva parametra: rezultat izračuna (ako je uspješno završio) i izuzetak (ako se neki korak izračuna nije normalno dovršio).

U sljedećem primjeru koristimo drška metoda za pružanje zadane vrijednosti kada je asinkroni izračun pozdrava završen s pogreškom jer nije navedeno ime:

Naziv niza = null; // ... CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> {if (name == null) {throw new RuntimeException ("Computation error!");} Return "Hello," + name;})}). handle ((s, t) -> s! = null? s: "Zdravo, Stranče!"); assertEquals ("Zdravo, stranče!", completableFuture.get ());

Pretpostavimo da kao alternativni scenarij želimo ručno dovršiti Budućnost s vrijednošću, kao u prvom primjeru, ali također imaju mogućnost dopunjavanja s iznimkom. The kompletnoIzuzetno metoda je namijenjena upravo tome. The completableFuture.get () metoda u sljedećem primjeru baca ExecutionException s RuntimeException kao njegov uzrok:

CompletableFuture completableFuture = novo CompletableFuture (); // ... completableFuture.completeExceptionally (novi RuntimeException ("Izračun nije uspio!")); // ... completableFuture.get (); // ExecutionException

U gornjem primjeru mogli smo riješiti iznimku s drška metoda asinkrono, ali s dobiti metodom možemo koristiti tipičniji pristup sinkrone obrade iznimke.

10. Async metode

Većina metoda tečnog API-ja u CompletableFuture razreda imaju dvije dodatne varijante s Async postfiks. Ove su metode obično namijenjene izvođenje odgovarajućeg koraka izvršenja u drugoj niti.

Metode bez Async postfix izvodi sljedeću fazu izvršenja pomoću pozivne niti. Suprotno tome, Async metoda bez Izvršitelj argument pokreće korak koristeći common račvati / spojiti implementacija bazena Izvršitelj kojem se pristupa s ForkJoinPool.commonPool () metoda. Napokon, Async metoda s an Izvršitelj argument pokreće korak pomoću proslijeđenog Izvršitelj.

Evo modificiranog primjera koji obrađuje rezultat izračuna pomoću a Funkcija primjer. Jedina vidljiva razlika je thenApplyAsync metoda, ali ispod haube aplikacija funkcije je umotana u ForkJoinTask instanci (za više informacija o račvati / spojiti pogledajte članak "Vodič za Fork / Join Framework u Javi"). To nam omogućuje još veću paralelizaciju izračunavanja i učinkovitiju upotrebu sistemskih resursa:

CompletableFuture completableFuture = CompletableFuture.supplyAsync (() -> "Pozdrav"); CompletableFuture future = completableFuture .thenApplyAsync (s -> s + "Svijet"); assertEquals ("Pozdrav svijetu", future.get ());

11. JDK 9 CompletableFuture API

Java 9 poboljšava CompletableFuture API sa sljedećim promjenama:

  • Dodane su nove tvorničke metode
  • Podrška za kašnjenja i vremenska ograničenja
  • Poboljšana podrška za potklasiranje

i novi API-ji instance:

  • Izvršitelj defaultExecutor ()
  • CompletableFuture newIncompleteFuture ()
  • Kompletna buduća kopija ()
  • CompletionStage minimalCompletionStage ()
  • CompletableFuture completeAsync (dobavljač dobavljača, izvršitelj izvršitelja)
  • CompletableFuture completeAsync (dobavljač dobavljač)
  • CompletableFuture iliTimeout (dugo vrijeme čekanja, jedinica vremenske jedinice)
  • CompletableFuture completeOnTimeout (vrijednost T, dugo vremensko ograničenje, jedinica vremenske jedinice)

Sada također imamo nekoliko statičkih korisnih metoda:

  • Izvršitelj odgođenIzvršitelj (dugo kašnjenje, jedinica vremenske jedinice, izvršitelj izvršitelja)
  • Izvršitelj odgođen Izvršitelj (dugo kašnjenje, jedinica vremenske jedinice)
  • CompletionStage CompleteStage (U vrijednost)
  • CompletionStage failedStage (Mogućnost bacanja ex)
  • CompletableFuture failedFuture (Može se baciti ex)

Napokon, za rješavanje vremenskog ograničenja, Java 9 je uvela još dvije nove funkcije:

  • iliTimeout ()
  • completeOnTimeout ()

Evo detaljnog članka za daljnje čitanje: Poboljšanja Java 9 CompletableFuture API.

12. Zaključak

U ovom smo članku opisali metode i tipične slučajeve upotrebe CompletableFuture razred.

Izvorni kôd članka dostupan je na GitHubu.