Vavr Option:Java Optional 的另一個選項

專注的阿熊發表於2019-11-06

每當涉及Java,總會有很多選項。  這篇文章討論了 Java 基礎類 Optional 用法,與 Vavr 中的對應方法進行比較。Java 8最早引入了 Optional,把它定義為“一種容器物件,可以儲存 null 或非 null 值”。

通常,在返回值可能為null的地方,會出現NullPointerException。開發人員可以使用 Optional 避免 null 值檢查。在這種情況下,Optional 提供了一些方便的功能。但可惜的是,Java 8並沒有包含所有功能。Optional中的某些功能需要使用 Java 11。要解決這類問題還可以使用 Vavr Option類。

本文將介紹如何使用 Java Optional類,並與 Vavr Option 進行比較。注意:示例程式碼要求使用Java 11及更高版本。所有程式碼在 Vavr0.10.2環境下完成測試。

讓我們開始吧。

Java Optional 簡介

Optional 並不是什麼新概念,像 Haskell、Scala 這樣的函數語言程式設計語言已經提供了實現。呼叫方法後,返回值未知或者不存在(比如 null)的情況下,用 Optional 處理非常好用。下面透過例項進行介紹。

新建 Optional 例項

首先,需要獲得 Optional 例項,有以下幾種方法可以新建 Optional 例項。不僅如此,還可以建立empty Optional。方法一,透過 value 建立,過程非常簡單:

Optional<Integer> four = Optional.of(Integer.valueOf(
4));

if (four.isPresent){
System.out.println( "Hoorayy! We have a value");
} else {
System.out.println( "No value");
}

為Integer 4 新建一個Optional例項。這種方法得到的 Optional 始終包含一個 value 且不為 null,例如上面這個示例。使用 ifPresent() 可以檢查value是否存在。可以注意到 four 不是 Integer,而是一個裝有整數的容器。如果確認 value 存在,可以用 get() 方法執行拆箱操作。具有諷刺意味的是,呼叫 get() 前如果不進行檢查,可能會丟擲 NoSuchElementException。

方法二,得到 Optional 物件的另一種方法是使用 stream。Stream提供的一些方法會返回Optional,可以用來檢查結果是否存在,例如:

  • findAny 

  • findFirst 

  • max 

  • min 

  • reduce 

檢視下面的程式碼段:

Optional<Car> car = cars.stream().filter(car->car.getId().equalsIgnoreCase(id)).findFirst();

方法三,使用 Nullable 新建 Optional。可能產生 null:

Optional<Integer> nullable = Optional.ofNullable(client.getRequestData());

最後,可以新建一個 empty Optional:

Optional<Integer> nothing = Optional.empty();

如何使用 Optional

獲得 Optional 物件後即可使用。一種典型的場景是在 Spring 倉庫中根據 Id 查詢記錄。可以使用 Optional 實現程式碼邏輯,避免 null 檢查(順便提一下,Spring 也支援 Vavr Option)。比如,從圖書倉庫裡查詢一本書。

Optional<Book> book = repository.findOne("some id");

首先,如果有這本書,可以繼續執行對應的業務邏輯。在前面的章節用 if-else實現了功能。當然,還有其他辦法:Optional 提供了一個方法,接收 Consumer 物件作為輸入:

repository.findOne("some id").ifPresent(book -> System.out.println(book));

還可以直接使用方法引用,看起來更簡單:

repository.findOne("some id").ifPresent(System.out::println);

如果倉庫中沒有該書,可以用ifPresentOrElseGet提供回撥函式:

repository.findOne(
"some id").ifPresentOrElseGet(book->{

// 如果 value 存在
}, ()->{
// 如果 value 不存在
});

如果結果不存在,可以返回另一個value:

Book result = repository.findOne("some id").orElse(defaultBook);

但是,Optional 也有缺點,使用時需要注意。最後一個例子中,“確保”無論如何都能獲得一本書,可能在倉庫中,也可能來自 orElse。但如果預設的返回值不是常量或者需要支援一些複雜方法該怎麼辦?首先,Java 無論如何都會執行 findOne,然後呼叫 orElse方法。預設返回值可以為常量,但正如我之前所說那樣,執行過程比較耗時。

另一個示例

下面用一個簡單的示例介紹如何實際使用 Optional 和 Option 類。有一個 CarRepository,可以根據提供的 ID(比如車牌號)查詢汽車,接下來用這個示例介紹如何使用 Optional 和 Option。

首先,加入下面程式碼

從 POJO 類 Car 開始。它遵循 immutable 模式,所有欄位都標記為 final,只包含 getter 沒有 setter。初始化時提供所有資料:


public 
class Car {

    private final String name;
    private final String id;
    private final String color;
    public Car (String name, String id, String color){
        this.name = name;
        this.id = id;
        this.color = color;
   }
    public String getId (){
        return id;
   }
    public String getColor () {
        return color;
   }
    public String getName () {
        return name;
   }
   @ Override
    public String toString ()
{
        return "Car "+name+ " with license id "+id+ " and of color "+color;
   }
}

接下來建立 CarRepository類。提供兩種方法根據Id查詢汽車:一種是老辦法,使用 Optional。和之前在 Spring 倉庫的做法類似,結果可能為 null。


public

 
class

 CarRepository {

   
private List<Car> cars;
   
public CarRepository () {
      getSomeCars();
   }
   
Car findCarById (String id) {
       
for (Car car: cars){ function(){   //外匯跟單             if (car.getId().equalsIgnoreCase(id)){
               
return car;
           }
       }
       
return null;
   }
   Optional<Car> findCarByIdWithOptional(String id){
       
return cars.stream().filter(car->car.getId().equalsIgnoreCase(id)).findFirst();
   }
   
private void getSomeCars () {
       cars =
new ArrayList<>();
       cars.add(
new Car( "tesla" , "1A9 4321" , "red" ));
       cars.add(
new Car( "volkswagen" , "2B1 1292" , "blue" ));
       cars.add(
new Car( "skoda" , "5C9 9984" , "green" ));
       cars.add(
new Car( "audi" , "8E4 4321" , "silver" ));
       cars.add(
new Car( "mercedes" , "3B4 5555" , "black" ));
       cars.add(
new Car( "seat" , "6U5 3123" , "white" ));
   }
}

注意:初始化過程會在倉庫中新增一些汽車模擬資料,便於演示。為了突出重點,避免問題複雜化,下面的討論專注於 Optional 和 Option。

使用Java Optional

使用JUnit建立一個新測試:

@
Test

void getCarById ()
{
   Car car = repository.findCarById( "1A9 4321");
   Assertions.assertNotNull(car);
   Car nullCar = repository.findCarById( "M 432 KT");
   Assertions.assertThrows(NullPointerException. class, ()->{
        if (nullCar == null){
            throw new NullPointerException();
       }
   });
}

上面的程式碼段採用了之前的老辦法。查詢捷克牌照 1A9 4321對應的汽車,檢查該車是否存在。輸入俄羅斯車牌找不到對應的汽車,因為倉庫中只有捷克車。結果為 null 可能會丟擲 NullPointerException。

接下來用Java Optional。第一步,獲得 Optional 例項,從儲存庫中使用指定方法返回 Optional:

@
Test

void getCarByIdWithOptional ()
{
   Optional<Car> tesla = repository.findCarByIdWithOptional( "1A9 4321");
   tesla.ifPresent(System.out::println);
}

這時呼叫findCarByIdWithOptional方法列印車輛資訊(如果有的話)。執行程式,得到以下結果:

Car tesla with license id 1A9 4321 and of color red

但是,如果程式碼中沒有特定方法該怎麼辦?這種情況可以從方法返回可能包含 null 的 Optional,稱為nullable。

Optional<Car> nothing = Optional.ofNullable(repository.findCarById(
"5T1 0965"));

Assertions.assertThrows(NoSuchElementException. class, ()->{
   Car car = nothing.orElseThrow(()-> new NoSuchElementException());
});

上面這段程式碼段中,我們發現了另一種方法。透過 findCarById 建立 Optional,如果未找到汽車可以返回 null。沒有找到車牌號 5T1 0965汽車時,可以用 orElseThrow 手動丟擲 NoSuchElementException。另一種情況,如果請求的資料不在倉庫中,可以用orElse返回預設值:

Car audi = repository.findCarByIdWithOptional(
"8E4 4311")

           .orElse( new Car( "audi", "1W3 4212", "yellow"));
  if (audi.getColor().equalsIgnoreCase( "silver")){
    System.out.println( "We have silver audi in garage!");
  } else {
    System.out.println( "Sorry, there is no silver audi, but we called you a taxi");
}

好的,車庫裡沒有找到銀色奧迪,只好叫車了!

使用 Vavr Option

Vavr OptionOption提供了另一種解決辦法。首先,在專案中新增依賴,(使用 Maven)安裝 Vavr:

<dependency>

  <groupId>io.vavr</groupId>
  <artifactId>vavr</artifactId>
  <version> 0.10.2</version>
</dependency>

簡而言之,Vavr 提供了類似的 API 新建 Option 例項。可以從 nullable 新建 Option 例項,像下面這樣:

Option<Car> nothing = Option.of(repository.findCarById("T 543 KK"));

也可以用 none 靜態方法建立一個empty容器:

Option<Car> nullable = Option.none();

此外,還有一種方法可以用 Java Optional 新建 Option。看下面這段程式碼:

Option<Car> result = Option.ofOptional(repository.findCarByIdWithOptional("5C9 9984"));

使用 Vavr Option,可以使用與 Optional相同的 API 來完成上述任務。例如,設定預設值:

Option<Car> result = Option.ofOptional(repository.findCarByIdWithOptional(
"5C9 9984"));

Car skoda = result.getOrElse( new Car( "skoda", "5E2 4232", "pink"));
System.out.println(skoda);

或者,請求的資料不存在時可以丟擲異常:

Option<Car> nullable = Option.none();

Assertions.assertThrows(NoSuchElementException. class, ()->{
nullable.getOrElseThrow(()-> new NoSuchElementException());
});

另外,當資料不可用時,可以執行以下操作:

nullable.onEmpty(()->{

///runnable
});

如何根據資料是否存在來執行相應操作,類似 Optional 中 ifPresent?有幾種實現方式。與 Optional 中 isPresent 類似,在 Option 中對應的方法稱為 isDefined:


if (result.isDefined()){

// 實現功能
}

然而,使用 Option能擺脫 if-else。 是否可以用Optional相同的方式完成? 使用 peek 操作:

result.peek(val -> System.out.println(val)).onEmpty(() -> System.out.println("Result is missed"));

此外,Vavr Option還提供了一些其他非常有用的方法,在函數語言程式設計上比Optional類效果更好。 因此,建議您花一些時間來探索 Vavr Option javadocs嘗試使用這些API。 我會持續跟進一些類似 map、narrow、isLazy 和 when 這樣有趣的功能。

另外,Option只是 Vavr 開發庫的一部分,其中還包含了許多其他關聯類。 不考慮這些類直接與 Optional 比較是不對的。 接下來我會繼續編寫 Vavr 主題的系列文章,介紹 Vavr 相關技術例如 Try、Collections 和 Streams。 敬請關注!

總結

本文中,我們討論了 Java Optional 類。 Optional 並不是什麼新概念,像 Haskell、Scala這樣的函數語言程式設計語言已經提供了實現。 呼叫方法後,返回值未知或者不存在(比如 null)的情況下,Optional 非常有用。 然後,介紹了 Optional API,並設計了一個汽車搜尋示例進行說明。 最後,介紹了 Optional 的另一種替代方案 Vavr Option 並透過示例進行了介紹。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946337/viewspace-2662859/,如需轉載,請註明出處,否則將追究法律責任。

相關文章