何時在 Java 中使用 Callable 和 Supplier
一、概述
在本教程中,我們將討論結構相似但使用不同的Callable
和Supplier
功能接口。
兩者都返回一個類型值並且不接受任何參數。執行上下文是確定差異的判別式。
在本教程中,我們將重點關注異步任務的上下文。
2.型號
在我們開始之前,讓我們定義一個類:
public class User {
private String name;
private String surname;
private LocalDate birthDate;
private Integer age;
private Boolean canDriveACar = false;
// standard constructors, getters and setters
}
3. Callable
Callable
是 Java 版本 5 中引入的接口,在版本 8 中演變為函數式接口。
它的SAM(Single Abstract Method)是方法call()
,返回一個泛型值,可能會拋出異常:
V call() throws Exception;
它旨在封裝一個應該由另一個線程執行的任務,例如Runnable
接口。那是因為Callable
實例可以通過ExecutorService
。
那麼讓我們定義一個實現:
public class AgeCalculatorCallable implements Callable<Integer> {
private final LocalDate birthDate;
@Override
public Integer call() throws Exception {
return Period.between(birthDate, LocalDate.now()).getYears();
}
// standard constructors, getters and setters
}
當call()
方法返回一個值時,主線程檢索它以執行其邏輯。為此,我們可以使用Future
,一個在另一個線程上執行的任務完成時跟踪並獲取值的對象。
3.1.單一任務
讓我們定義一個只執行一個異步任務的方法:
public User execute(User user) {
ExecutorService executorService = Executors.newCachedThreadPool();
try {
Future<Integer> ageFuture = executorService.submit(new AgeCalculatorCallable(user.getBirthDate()));
user.setAge(age.get());
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e.getCause());
}
return user;
}
我們可以通過 lambda 表達式重寫submit()
的內部塊:
Future<Integer> ageFuture = executorService.submit(
() -> Period.between(user.getBirthDate(), LocalDate.now()).getYears());
當我們嘗試通過調用get()
方法訪問返回值時,我們必須處理兩個已檢查的異常:
-
InterruptedException
:當線程處於休眠、活動或占用狀態時發生中斷時拋出 -
ExecutionException
:當通過拋出異常中止任務時拋出。換句話說,它是一個包裝器異常,中止任務的真正異常是原因(可以使用getCause
() 方法檢查)。
3.2.任務鏈
執行屬於鏈的任務取決於先前任務的狀態。如果其中之一失敗,則無法執行當前任務。
所以讓我們定義一個新的Callable
:
public class CarDriverValidatorCallable implements Callable<Boolean> {
private final Integer age;
@Override
public Boolean call() throws Exception {
return age > 18;
}
// standard constructors, getters and setters
}
接下來,讓我們定義一個任務鏈,其中第二個任務將前一個任務的結果作為輸入參數:
public User execute(User user) {
ExecutorService executorService = Executors.newCachedThreadPool();
try {
Future<Integer> ageFuture = executorService.submit(new AgeCalculatorCallable(user.getBirthDate()));
Integer age = ageFuture.get();
Future<Boolean> canDriveACarFuture = executorService.submit(new CarDriverValidatorCallable(age));
Boolean canDriveACar = canDriveACarFuture.get();
user.setAge(age);
user.setCanDriveACar(canDriveACar);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e.getCause());
}
return user;
}
在任務鏈中使用Callable
和Future
有一些問題:
- 鏈中的每個任務都遵循“提交-獲取”模式。在長鏈中,這會產生冗長的代碼。
- 當鏈可以容忍任務失敗時,我們應該創建一個專用的
try
/catch
塊。 - 調用時,
get()
方法會一直等待,直到Callable
返回一個值。所以鏈的總執行時間等於所有任務執行時間的總和。但是,如果下一個任務僅依賴於前一個任務的正確執行,則鍊式過程會大大減慢。
4. Supplier
Supplier
是一個功能接口,其 SAM(單一抽象方法)是get()
。
它不接受任何參數,返回一個值,並且只拋出未經檢查的異常:
T get();
此接口最常見的用例之一是推遲執行某些代碼。
Optional
類有一些方法接受Supplier
作為參數,例如Optional.or()
、 Optional.orElseGet().
所以Supplier
只有在Optional
為空的時候才會執行。
我們還可以在異步計算上下文中使用它,特別是在 CompletableFuture API 中。
一些方法接受Supplier
作為參數,例如supplyAsync()
方法。
4.1.單一任務
讓我們定義一個只執行一個異步任務的方法:
public User execute(User user) {
ExecutorService executorService = Executors.newCachedThreadPool();
CompletableFuture<Integer> ageFut = CompletableFuture.supplyAsync(() -> Period.between(user.getBirthDate(), LocalDate.now())
.getYears(), executorService)
.exceptionally(throwable -> {throw new RuntimeException(throwable);});
user.setAge(ageFut.join());
return user;
}
在這種情況下,lambda 表達式定義了Supplier
,但我們也可以定義一個實現類。感謝CompletableFuture,
我們為異步操作定義了一個模板,使其更易於理解和修改。
join()
方法提供Supplier.
4.2.任務鏈
我們還可以在Supplier
接口和CompletableFuture
的支持下開發一系列任務:
public User execute(User user) {
ExecutorService executorService = Executors.newCachedThreadPool();
CompletableFuture<Integer> ageFut = CompletableFuture.supplyAsync(() -> Period.between(user.getBirthDate(), LocalDate.now())
.getYears(), executorService);
CompletableFuture<Boolean> canDriveACarFut = ageFut.thenComposeAsync(age -> CompletableFuture.supplyAsync(() -> age > 18, executorService))
.exceptionally((ex) -> false);
user.setAge(ageFut.join());
user.setCanDriveACar(canDriveACarFut.join());
return user;
}
使用CompletableFuture
– Supplier
方法定義異步任務鏈可以解決之前使用Future
– Callable
方法引入的一些問題:
- 鏈中的每個任務都是孤立的。因此,如果任務執行失敗,我們可以通過
exceptionally()
塊來處理它。 -
join()
方法不需要在編譯時處理已檢查的異常。 - 我們可以設計一個異步任務模板,完善每個任務的狀態處理。
5.結論
在本文中,我們討論了Callable
和Supplier
接口之間的區別,重點關注異步任務的上下文。
接口設計級別的主要區別是Callable
拋出的已檢查異常。
Callable
不適用於功能上下文。它隨著時間的推移而適應,函數式編程和檢查異常不相容。
所以任何函數式 API(例如CompletableFuture
API)總是接受Supplier
而不是Callable
。
與往常一樣,可以在 GitHub 上找到示例的完整源代碼。