CodeGym /Kursy /JAVA 25 SELF /Kombinowanie CompletableFuture: thenCombine, allOf, anyOf...

Kombinowanie CompletableFuture: thenCombine, allOf, anyOf

JAVA 25 SELF
Poziom 55 , Lekcja 2
Dostępny

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
thenCombine
Trzeba połączyć wyniki dwóch zadań Wynik połączenia
allOf
Trzeba poczekać na zakończenie wszystkich zadań
Void
anyOf
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.

1
Zadanie
JAVA 25 SELF, poziom 55, lekcja 2
Niedostępne
Koordynacja dostaw: oczekiwanie na wszystkie ładunki
Koordynacja dostaw: oczekiwanie na wszystkie ładunki
1
Zadanie
JAVA 25 SELF, poziom 55, lekcja 2
Niedostępne
Składanie powitania: łączenie fragmentów wypowiedzi
Składanie powitania: łączenie fragmentów wypowiedzi
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION