引言
相信很多人關注 Vavr 的原因,還是因爲 Hystrix 庫。Hystrix 不更新了,並在 GitHub 主頁上推薦了 Resilience4j,而 Vavr 作爲 Resilience4j 的唯一依賴被提及。對於 Resilience4j 這個以輕依賴作爲特色之一的容錯庫,爲什麼還會引用 Vavr 呢?
以下是 Resilience4j 官方原文:
❝Resilience4j is a lightweight fault tolerance library inspired by Netflix Hystrix, but designed for Java 8 and functional programming. Lightweight, because the library only uses Vavr, which does not have any other external library dependencies.
❞
Resilience4j 除了輕量,另一特點是對 Java 8 函數式編程的支持,經過一番瞭解,Vavr 正是爲了提升 Java 函數式編程體驗而開發的,通過它可以幫助我們編寫出更簡潔、高效的函數風格代碼。
限於篇幅,該系列分爲上、下兩篇:上篇着重回顧函數式編程的一些基礎知識,以及 Vavr 總體介紹、Vavr 對元組、函數的支持,通過上篇的學習;下篇着重講述 Vavr 中對各種值類型、增強集合、參數檢查、模式匹配等特性。力求通過這兩篇文章,把 Vavr 的總體特性呈現給大家,讓大家對 Vavr 有個全面的認識。
簡介
Vavr是 Java 8+ 函數式編程的增強庫。提供了不可變數據類型和函數式控制結構,旨在讓 Java 函數編程更便捷高效。特別是功能豐富的集合庫,可以與Java的標準集合平滑集成。
Vavr 的讀音是 /ˈweɪ.vɚ/,早期版本叫 Javaslang,由於和 Java™ 商標衝突(類似國內的 JavaEye 改名),所以把 Java 倒過來取名。
函數式編程
學習 Vavr 之前,我們先回顧下 Java 函數式編程及 Lambda (λ) 表達式的一些相關內容。
Java 8 開始,在原有面向對象、命令式編程範式的基礎上,增加了函數式編程支持,其核心是行爲參數化,把行爲具體理解爲一個程序函數(方法),即是將函數作爲其它函數的參數傳遞,組成高階函數。
舉個例子,人(People)這個類存在一個年齡(age)屬性,我們想根據每個人的年齡進行排序比較。
首先看下 Java 8 之前的做法:
Comparator<People> comparator = new Comparator<People>() {
@Override
public int compare(People p1, People p2) {
return Integer.compare(p1.getAge(), p2.getAge());
}
};
再看 Java 8 之後的做法:
Comparator<People> comparator
= (p1, p2) -> Integer.compare(p1.getAge(), p2.getAge());
利用 Lambda 表達式的語法,確實少了很多模板代碼,看起來更加簡潔了。關於 Java 的函數式編程及 Lambda 表達式語法,有以下需要掌握的知識點:
函數式接口
函數式接口 (Functional Interface) 就是一個有且僅有一個抽象方法,但是可以有多個非抽象方法的接口,通常會用 @FunctionalInterface
進行標註,但不是必須的。Java 8 自帶了常用的函數式接口,存放在 java.util.function
包下,包括 Function
、Supplier
、Consumer
、Predicate
等,此外在其它地方也用到很多函數式接口,比如前面演示的 Comparator
。
Lambda 表達式
Lambda 表達式是一種匿名函數,在 Java 中,定義一個匿名函數的實質依然是函數式接口的匿名實現類,它沒有名稱,只有參數列表、函數主體、返回類型,可能還有一個異常列表聲明。Lambda 表達式有以下重要特徵:
可選類型聲明:不需要聲明參數類型,編譯器可以進行類型識別;
可選的參數圓括號:一個參數無需定義圓括號,但多個參數需要定義圓括號;
可選的花括號:如果主體包含了一個語句,就不需要使用花括號;
可選的 return 關鍵字:如果主體只有一個表達式返回值,則編譯器會自動返回值,加了花括號需要指定表達式返回一個數值。
// 1. 不需要參數,返回值爲 1
() -> 1
// 2. 接收一個參數(數字類型),返回值爲 x + 1
x -> x + 1
// 3. 接受2個參數(數字),返回值爲 x + y
(x, y) -> x + y
// 4. 接收2個int型整數,返回值爲 x + y
(int x, int y) -> x + y
// 5. 接受一個 String 對象,並在控制檯打印,不返回任何值(返回 void)
(String s) -> System.out.print(s)
副作用(Side-Effects)
❝如果一個操作、函數或表達式在其本地環境之外修改了某個狀態變量值,則會產生副作用,也就是說,除了向操作的調用者返回一個值(主要效果)之外,還會產生可觀察到的效果。
❞
例如,一個函數產生異常,並且這個異常向上傳遞,就是一種影響程序的副作用,此外,異常就像一種非本地的 goto 語句,打斷了正常的程序流程。具體看以下代碼:
int divide(int dividend, int divisor) {
// 如果除數爲0,會產生異常
return dividend / divisor;
}
怎麼處理這種副作用呢?在 Vavr 中,可以把它封裝到一個 Try
實例,具體實現:
// = Success(result) or Failure(exception)
Try<Integer> safeDivide(Integer dividend, Integer divisor) {
return Try.of(() -> divide(dividend, divisor));
}
這個版本的除法函數不再拋出異常,我們通過 Try
類型明確了可能的錯誤。
引用透明(Referential Transparency)
❝引用透明的概念與函數的副作用相關,且受其影響。如果程序中任意兩處具有相同輸入值的函數調用能夠互相置換,而不影響程序的動作,那麼該程序就具有引用透明性。
❞
從以下示例可以看出,第一種實現,隨機數是根據可變的外部狀態生成的,所以每次調用產生的結果都不同,無法做到引用透明;第二種實現,我們給隨機對象指定一個隨機種子,這樣保證不受外部狀態的影響,達到引用透明的效果:
// 非引用透明
Math.random();
// 引用透明
new Random(1).nextDouble();
不可變對象
不可變對象是指其狀態在創建後不能修改的對象。它有以下好處:
本質上是線程安全的,因此不需要同步;
對於equals和hashCode來說是穩定的,因此是可靠的哈希鍵;
不需要克隆;
在未檢查的協變強制轉換(特定於java)中使用時表現爲類型安全。
使用 Vavr
受限於 Java 標準庫的通用性要求及體量大小考慮,JDK API 對函數式編程的支持比較有限,這時候可以引入 Vavr 來提供更便捷的安全集合類型、支持更多的 stream 流操作、豐富函數式接口類型……
在 Vavr 中,所有類型都是基於 Tuple, Value, λ 構建的:
引入依賴
這裏使用 Maven 項目構建,完整的 pom.xml
配置如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo-vavr</artifactId>
<version>0.0.1</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<vavr.version>0.9.3</vavr.version>
</properties>
<dependencies>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>${vavr.version}</version>
</dependency>
</dependencies>
</project>
Java 必須是 1.8 以上版本,這是使用 Java 函數式編程的前提,另外 Vavr 使用的是 0.9.3 版本。Vavr 本身沒有其它外部依賴,Jar 包大小僅有 800+K,相當輕量。
元組(Tuple)
Java 自身並沒有元組的概念,元組是將固定數量的元素組合在一起,這樣它們就可以作爲一個整體傳遞,但它與數組或集合的區別是,元組能包含不同類型的對象,且是不可變的。
Vavr 提供了 Tuple1
、Tuple2
到 Tuple8
等8個具體的元組類型,分別代表可存儲1~8個元素的元組,並可以通過 _1
、_2
..._8
等屬性訪問對應元素。
以下是創建並獲取元組的示例:
// 通過 Tuple.of() 靜態方法創建一個二元組
Tuple2<String, Integer> people = Tuple.of("Bob", 18);
// 獲取第一個元素,名稱:Bob
String name = people._1;
// 獲取第二個元素,年齡:18
Integer age = people._2;
元組也提供了對元素映射處理的能力,以下兩種寫法效果是相同的:
// ("Hello, Bob", 9)
people.map(
name -> "Hello, " + name,
age-> age / 2
);
// ("Hello, Bob", 9)
people.map(
(name, age) -> Tuple.of("Hello, " + name, age / 2)
);
此外,元組還提供了基於元素內容轉換創建新的類型:
// 返回 name: Bob, age: 18
String str = people.apply(
(name, age) -> "name: " + name + ", age: " + age
);
函數(Function)
Java 8 僅提供了接受一個參數的函數式接口 Function
和接受兩個參數的函數式接口 BiFunction
,vavr 則提供了最多可以接受8個參數的函數式接口:Function0
、Function1
、Function2
...Function8
。如果需要拋出受檢異常的函數,可以使用 CheckedFunction{0...8}
版本。
以下是使用函數的示例:
// 聲明一個接收兩個參數的函數
Function2<String, Integer, String> description = (name, age) -> "name: " + name + ", age: " + age;
// 返回 "name: Bob, age: 18"
String str = description.apply("Bob", 18);
Vavr 函數是 Java 8 函數的增強,它提供了以下特性:
組合(Composition)
組合是將一個函數 f(x)
的結果作爲另一個函數 g(y)
的參數,產生新函數 h: g(f(x))
的操作,可以使用 andThen
或 compose
方法實現函數組合:
Function1<Integer, Integer> plusOne = a -> a + 1;
Function1<Integer, Integer> multiplyByTwo = a -> a * 2;
// 以下兩種寫法結果一致,都是 z -> (z + 1) * 2
Function1<Integer, Integer> addOneAndMultiplyByTwo1 = plusOne.andThen(multiplyByTwo);
Function1<Integer, Integer> addOneAndMultiplyByTwo2 = plusOne.andThen(multiplyByTwo);
提升(Lifting)
提升是針對部分函數(partial function)的操作,如果一個函數 f(x) 的定義域是 x,另一個函數 g(y) 跟 f(x) 定義相同,只是定義域 y 是 x 的子集,就說 f(x) 是全函數(total function),g(y) 是部分函數。函數的提升會返回當前函數的全函數,返回類型爲 Option
,以下是一個部分函數定義:
// 當除數 0 時,將導致程序異常
Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
我們再利用lift
將divide
提升爲可以接收所有輸入的全函數:
Function2<Integer, Integer, Option<Integer>> safeDivide = Function2.lift(divide);
// = None
Option<Integer> i1 = safeDivide.apply(1, 0);
// = Some(2)
Option<Integer> i2 = safeDivide.apply(4, 2);
通過以上示例可以看出,如果使用不允許的輸入值調用提升後的全函數,則返回None
而不是引發異常。
部分應用(Partial application)
部分應用是通過固定函數的前n個參數值,產生一個新函數,該新函數參數爲原函數總參數個數減n,具體示例如下:
Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
Function1<Integer, Integer> add2 = sum.apply(2);
sum
函數通過部分應用,第一個參數被固定爲2
,併產生新的add2
函數。
柯里化(Currying)
柯里化是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數而且返回結果的新函數的技術。
當Function2
被柯里化時,結果與部分應用沒有區別,因爲兩者都會產生單參數的函數。但函數參數多於2個,就能明顯看出柯里化的不同:
Function3<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
final Function1<Integer, Function1<Integer, Integer>> add2 = sum.curried().apply(2);
// = 9
Integer i = add2.apply(4).apply(3);
記憶(Memoization)
函數記憶是利用緩存技術,第一次執行後,將結果緩存起來,後續從緩存返回結果。以下函數在第一次調用時,生成隨機數並進行緩存,第二次調用直接從緩存返回結果,所以多次返回的隨機數是同一個:
Function0<Double> hashCache = Function0.of(Math::random).memoized();
double randomValue1 = hashCache.apply();
double randomValue2 = hashCache.apply();
總結
今天對 Vavr 的介紹先到這裏,下篇我們將會接着介紹另外一些特性:
值類型(Values)
集合(Collections)
參數檢查(Property Checking)
模式匹配(Pattern Matching)
推薦閱讀