將未來轉變為完整的未來
一、簡介
在本教學中,我們將探索如何將Future
轉換為CompletableFuture
。這種轉換使我們能夠利用CompletableFuture
的高級功能,例如非阻塞操作、任務鍊和更好的錯誤處理,同時仍使用傳回Future
的 API 或函式庫。
2. 為什麼要將Future
轉變為CompletableFuture
?
Java 中的Future
介面表示非同步計算的結果。它提供了檢查計算是否完成、等待計算完成並檢索結果的方法。
但是, Future
有局限性,例如阻止需要使用get()
檢索結果的呼叫。它也缺乏對連結多個非同步任務或處理回調的支援。
另一方面, Java 8 中引入的CompletableFuture
**解決了這些缺點。它透過thenApply()
和thenAccept()
等方法支援非阻塞操作,用於任務鍊和回調,以及使用exceptionally()
進行錯誤處理。**
透過將Future
轉換為CompletableFuture
,我們可以利用這些功能,同時仍使用傳回Future
API 或函式庫。
3. 逐步轉型
在本節中,我們將示範如何將Future
轉換為CompletableFuture.
3.1.使用ExecutorService
模擬Future
為了了解Future
工作原理,我們首先使用ExecutorService
模擬非同步計算。 ExecutorService
是一個用於在單獨執行緒中管理和調度任務的框架。這將有助於我們理解Future
的阻塞性質:
@Test
void givenFuture_whenGet_thenBlockingCall() throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "Hello from Future!";
});
String result = future.get();
executor.shutdown();
assertEquals("Hello from Future!", result);
}
在這段程式碼中,我們使用executor.submit()
來模擬一個回傳Future
物件的長時間運行的任務。 future.get()
呼叫會阻塞主線程,直到計算完成,然後列印結果。
這種阻塞行為凸顯了Future
的限制之一,我們旨在透過CompletableFuture
解決這個問題。
3.2.將Future
包裝成CompletableFuture
未來
要將Future
轉換為CompletableFuture
,我們需要彌合Future
的阻塞性質與CompletableFuture
的非阻塞、回呼驅動設計之間的差距。
為了實現這一點,我們建立一個名為toCompletableFuture()
的方法,該方法將Future
和ExecutorService
作為輸入並傳回CompletableFuture
:
static <T> CompletableFuture<T> toCompletableFuture(Future<T> future, ExecutorService executor) {
CompletableFuture<T> completableFuture = new CompletableFuture<>();
executor.submit(() -> {
try {
completableFuture.complete(future.get());
} catch (Exception e) {
completableFuture.completeExceptionally(e);
}
});
return completableFuture;
}
在上面的範例中, toCompletableFuture()
方法首先建立一個新的CompletableFuture
。然後,來自快取執行緒池的單獨執行緒監視Future
。
當Future
完成時,使用阻塞get()
方法檢索其結果,然後使用complete()
方法將其傳遞給CompletableFuture
。如果Future
拋出異常, CompletableFuture
將異常完成以確保錯誤被傳播。
這個包裝的CompletableFuture
允許我們非同步處理結果並使用像thenAccept()
這樣的回呼方法。讓我們來示範如何使用toCompletableFuture()
:
@Test
void givenFuture_whenWrappedInCompletableFuture_thenNonBlockingCall() throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "Hello from Future!";
});
CompletableFuture<String> completableFuture = toCompletableFuture(future, executor);
completableFuture.thenAccept(result -> assertEquals("Hello from Future!", result));
executor.shutdown();
}
與future.get()
不同,這種方法避免了阻塞主線程並使程式碼更加靈活。我們還可以將多個階段連結在一起,從而對結果進行更複雜的處理。
例如,我們可以轉換Future
的結果,然後執行附加操作:
@Test
void givenFuture_whenTransformedAndChained_thenCorrectResult() throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "Hello from Future!";
});
CompletableFuture<String> completableFuture = toCompletableFuture(future, executor);
completableFuture
.thenApply(result -> result.toUpperCase()) // Transform result
.thenAccept(transformedResult -> assertEquals("HELLO FROM FUTURE!", transformedResult));
executor.shutdown();
}
在此範例中,將結果轉換為大寫後,我們列印轉換後的結果。這展示了使用CompletableFuture
進行鍊式操作的強大功能。
3.3.使用CompletableFuture
的supplyAsync()
方法
另一種方法是利用CompletableFuture
的名為supplyAsync()
的方法,該方法可以非同步執行任務並將其結果作為CompletableFuture
傳回。
讓我們看看如何將阻塞Future
呼叫包裝在supplyAsync()
方法中以實作轉換:
@Test
void givenFuture_whenWrappedUsingSupplyAsync_thenNonBlockingCall() throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "Hello from Future!";
});
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
return future.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
completableFuture.thenAccept(result -> assertEquals("Hello from Future!", result));
executor.shutdown();
}
在這個方法中,我們使用CompletableFuture.supplyAsync()
非同步執行任務。此任務將阻塞呼叫future.get()
包裝在 lambda 表達式內。這樣, Future
的結果就以非阻塞的方式檢索,使我們能夠使用CompletableFuture
方法進行回調和連結。
此方法更簡單,因為它避免了手動管理單獨的執行緒。 CompletableFuture
為我們處理非同步執行。
4. 將多個Future
物件組合成一個CompletableFuture
在某些情況下,我們可能需要使用多個Future
對象,這些物件應組合成一個CompletableFuture
。當聚合來自不同任務的結果或等待所有任務完成後再繼續進一步處理時,這種情況很常見。使用CompletableFuture
,我們可以有效地組合多個Future
物件並以非阻塞方式處理它們。
為了組合多個Future
對象,我們首先將它們轉換為CompletableFuture
實例。然後,我們使用CompletableFuture.allOf()
等待所有任務完成。讓我們看一個如何實現的範例:
static CompletableFuture<Void> allOfFutures(List<Future<String>> futures, ExecutorService executor) {
// Convert all Future objects into CompletableFuture instances
List<CompletableFuture<String>> completableFutures = futures.stream()
.map(future -> FutureToCompletableFuture.toCompletableFuture(future, executor))
.toList();
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0]));
}
所有任務完成後, CompletableFuture.allOf()
方法會發出完成訊號。為了演示這一點,讓我們考慮一個場景,其中多個任務會傳回帶有字串作為結果的Future
物件。我們將匯總結果並確保所有任務均成功完成:
@Test
void givenMultipleFutures_whenCombinedWithAllOf_thenAllResultsAggregated() throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
List<Future<String>> futures = List.of(
executor.submit(() -> {
return "Task 1 Result";
}),
executor.submit(() -> {
return "Task 2 Result";
}),
executor.submit(() -> {
return "Task 3 Result";
})
);
CompletableFuture<Void> allOf = allOfFutures(futures, executor);
allOf.thenRun(() -> {
try {
List<String> results = futures.stream()
.map(future -> {
try {
return future.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toList();
assertEquals(3, results.size());
assertTrue(results.contains("Task 1 Result"));
assertTrue(results.contains("Task 2 Result"));
assertTrue(results.contains("Task 3 Result"));
} catch (Exception e) {
fail("Unexpected exception: " + e.getMessage());
}
}).join();
executor.shutdown();
}
在此範例中,我們模擬三個任務,每個任務使用ExecutorService
傳回一個結果。接下來,提交每個任務並傳回一個Future
物件。我們將Future
物件清單傳遞給allOfFutures()
方法,將它們轉換為CompletableFuture
並使用CompletableFuture.allOf()
組合它們。
當所有任務完成後,我們使用thenRun()
方法來聚合結果並斷言其正確性。這種方法在需要聚合結果的獨立任務的平行處理等場景中非常有用。
5. 結論
在本教學中,我們探討如何在 Java 中將Future
轉換為CompletableFuture
。透過利用CompletableFuture
,我們可以利用非阻塞操作、任務鍊和強大的異常處理。當我們想要增強非同步程式設計模型的功能時,這種轉換特別有用。
像往常一樣,這裡討論的程式碼可以在 GitHub 上找到。