1. Łączymy CompletableFuture
W prawdziwym życiu rzadko wszystko pochodzi z jednego źródła: profil użytkownika i jego zamówienia można ładować równolegle, dane z dwóch mikroserwisów — łączyć, a czasem chcemy po prostu obsłużyć pierwszą odpowiedź, która dotrze. Podejście synchroniczne każe czekać po kolei; klasa CompletableFuture pozwala uruchamiać wszystko jednocześnie i elegancko skombinować wyniki. Udostępnia w tym celu specjalne metody: thenCombine, allOf, anyOf. Przyjrzyjmy się każdej po kolei.
Zadania równoległe: dwa asynchroniczne żądania
Jak wyglądałoby to synchronicznie:
String name = loadUserName(); // długo
int balance = loadUserBalance(); // długo
System.out.println("Imię: " + name + ", Saldo: " + balance);
Problem: drugie wywołanie zacznie się dopiero po zakończeniu pierwszego.
Sposób asynchroniczny
Z pomocą CompletableFuture oba zadania można uruchomić jednocześnie:
CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());
Ale jak teraz pobrać oba wyniki i przetworzyć je razem? Do tego służy thenCombine.
2. thenCombine: łączymy wyniki dwóch zadań
Metoda thenCombine pozwala połączyć dwa CompletableFuture i wykonać działanie, gdy oba zadania się zakończą. Zwraca nowe CompletableFuture z wynikiem połączenia.
Sygnatura:
<A, B, C> CompletableFuture<C> thenCombine(
CompletionStage<? extends B> other,
BiFunction<? super A, ? super B, ? extends C> fn
)
- A — typ wyniku pierwszego future,
- B — typ drugiego,
- C — typ połączonego wyniku.
Przykład
CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());
CompletableFuture<String> resultFuture = nameFuture.thenCombine(
balanceFuture,
(name, balance) -> "Imię: " + name + ", Saldo: " + balance
);
resultFuture.thenAccept(System.out::println);
Jak to działa:
- Oba future startują równolegle.
- Gdy tylko oba się zakończą, wywoływana jest funkcja (name, balance) -> ....
- Końcowy future zawiera łańcuch znaków z wynikiem.
Mini-przykład z liczbami
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 3);
CompletableFuture<Integer> sum = f1.thenCombine(f2, Integer::sum);
sum.thenAccept(result -> System.out.println("Suma: " + result));
Wynik:
Suma: 5
Wariant asynchroniczny
Jeśli łączenie jest kosztowną operacją, użyj thenCombineAsync:
f1.thenCombineAsync(f2, (a, b) -> a * b);
3. allOf: gdy zadań jest dużo
A co jeśli mamy nie dwa zadania, lecz cały tuzin? Na przykład chcemy równolegle załadować dane o dziesięciu użytkownikach. Do tego służy metoda CompletableFuture.allOf.
Opis
CompletableFuture.allOf(f1, f2, ..., fn) zwraca nowe future, które zakończy się, gdy zakończą się wszystkie przekazane zadania. Jest jednak niuans: to future nie zawiera wyniku — jego typ to zawsze CompletableFuture<Void>. Aby uzyskać wyniki, trzeba osobno „wyciągnąć” je z pierwotnych future.
Przykład
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Pierwszy");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Drugi");
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);
all.thenRun(() -> {
// Wszystkie zadania zakończone!
String s1 = f1.join(); // join() — jak get(), ale rzuca unchecked exception
String s2 = f2.join();
System.out.println(s1 + " & " + s2);
});
Wynik:
Pierwszy & Drugi
Przykład z tablicą zadań
List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
int id = i;
futures.add(CompletableFuture.supplyAsync(() -> "Użytkownik " + id));
}
CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
all.thenRun(() -> {
for (CompletableFuture<String> f : futures) {
System.out.println(f.join());
}
});
Co się dzieje:
- Wszystkie zadania startują równolegle.
- allOf zakończy się, gdy zakończą się wszystkie zadania.
- W bloku thenRun możemy pobrać wyniki przez join().
Schemat wizualny
[Future1] \
[Future2] ----> [allOf] ---> thenRun
[Future3] /
4. anyOf: czekamy na pierwsze zakończone zadanie
Czasami nie trzeba czekać na wszystkie, lecz chcemy otrzymać wynik najszybszego zadania. Na przykład pytamy dwa serwery o dane — używamy tych, które nadejdą jako pierwsze. Do tego służy CompletableFuture.anyOf.
Opis
CompletableFuture.anyOf(f1, f2, ..., fn) zwraca future, które zakończy się, gdy zakończy się dowolne z przekazanych zadań. Typ wyniku to CompletableFuture<Object>, ponieważ typy zadań mogą być różne.
Przykład
CompletableFuture<String> fast = CompletableFuture.supplyAsync(() -> {
sleep(500);
return "Szybki serwer";
});
CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> {
sleep(2000);
return "Wolny serwer";
});
CompletableFuture<Object> any = CompletableFuture.anyOf(fast, slow);
any.thenAccept(result -> System.out.println("Otrzymano: " + result));
Wynik:
Otrzymano: Szybki serwer
Przykład z różnymi typami
Można łączyć zadania różnych typów, ale wtedy wynik będzie typu Object i będzie potrzebne jawne rzutowanie.
5. Przydatne niuanse
Zalecenia
- allOf nie zwraca tablicy wyników. Trzeba przechowywać oryginalne future, aby pobrać ich wartości przez join() lub get().
- anyOf zwraca pierwszy ukończony wynik, ale typ Object. Jeśli wszystkie zadania zwracają ten sam typ, można wykonać rzutowanie.
- Jeśli któreś z zadań w allOf zakończy się błędem — wynikowe future również zakończy się błędem.
- Dla thenCombine oba zadania muszą zakończyć się powodzeniem, w przeciwnym razie zostanie rzucony wyjątek.
Tabela porównawcza metod
| Metoda | Kiedy używać | Typ wyniku |
|---|---|---|
|
Trzeba połączyć wyniki dwóch zadań | Wynik połączenia |
|
Trzeba poczekać na zakończenie wszystkich zadań | |
|
Trzeba poczekać na zakończenie dowolnego z zadań | Object (wynik pierwszego zadania) |
6. Typowe błędy przy łączeniu CompletableFuture
Błąd nr 1: Oczekiwanie na wynik przez get()/join() w wątku głównym.
Jeśli piszesz kod asynchroniczny, a na końcu i tak wywołujesz get() lub join(), blokujesz wątek i tracisz wszystkie zalety asynchroniczności. Lepiej użyć thenAccept/thenRun do przetwarzania wyniku bez blokowania.
Błąd nr 2: Niezapisywanie odnośników do oryginalnych future przy użyciu allOf.
Jeśli wywołałeś CompletableFuture.allOf(f1, f2, f3), ale nie zachowałeś f1, f2, f3 — nie pobierzesz ich wyników. allOf zwraca tylko Void!
Błąd nr 3: Brak obsługi błędów w łańcuchu.
Jeśli jedno z zadań zakończy się błędem, cały allOf lub thenCombine również zakończy się błędem. Używaj metod obsługi błędów (exceptionally, handle, whenComplete), aby nie przegapić wyjątków.
Błąd nr 4: Niezgodność typów przy anyOf.
anyOf zwraca Object. Jeśli twoje future zwracają różne typy, trzeba rozpoznać, co przyszło jako pierwsze. Lepiej używać tych samych typów zadań, jeśli to możliwe.
Błąd nr 5: Zbyt skomplikowane łańcuchy bez komentarzy.
Gdy kodu robi się dużo, łańcuchy future mogą zamienić się w „spaghetti”. Nie wahaj się rozbijać łańcucha na osobne zmienne i komentować kroki.
GO TO FULL VERSION