在本系列的上一篇文章中對 Java 平臺(tái)提供的 Lambda 表達(dá)式和流做了介紹。受限于 Java 標(biāo)準(zhǔn)庫的通用性要求和二進(jìn)制文件大小荚斯,Java 標(biāo)準(zhǔn)庫對函數(shù)式編程的 API 支持相對比較有限妖啥。函數(shù)深入理解 Java 函數(shù)式編程系列 第 1 部分 函數(shù)式編程思想概論的聲明只提供了 Function 和 BiFunction 兩種霉颠,流上所支持的操作的數(shù)量也較少。為了更好地進(jìn)行函數(shù)式編程荆虱,我們需要第三方庫的支持蒿偎。Vavr 是 Java 平臺(tái)上函數(shù)式編程庫中的佼佼者朽们。
Vavr 這個(gè)名字對很多開發(fā)人員可能比較陌生。它的前身 Javaslang 可能更為大家所熟悉诉位。Vavr 作為一個(gè)標(biāo)準(zhǔn)的 Java 庫骑脱,使用起來很簡單。只需要添加對 io.vavr:vavr 庫的 Maven 依賴即可苍糠。Vavr 需要 Java 8 及以上版本的支持叁丧。本文基于 Vavr 0.9.2 版本,示例代碼基于 Java 10岳瞭。
元組
元組(Tuple)是固定數(shù)量的不同類型的元素的組合拥娄。元組與集合的不同之處在于,元組中的元素類型可以是不同的瞳筏,而且數(shù)量固定稚瘾。元組的好處在于可以把多個(gè)元素作為一個(gè)單元傳遞。如果一個(gè)方法需要返回多個(gè)值姚炕,可以把這多個(gè)值作為元組返回摊欠,而不需要?jiǎng)?chuàng)建額外的類來表示。根據(jù)元素?cái)?shù)量的不同柱宦,Vavr 總共提供了 Tuple0些椒、Tuple1 到 Tuple8 等 9 個(gè)類。每個(gè)元素類都需要聲明其元素類型捷沸。如 Tuple2<String, Integer>表示的是兩個(gè)元素的元組摊沉,第一個(gè)元素的類型為 String,第二個(gè)元素的類型為 Integer痒给。對于元組對象说墨,可以使用 _1、_2 到 _8 來訪問其中的元素苍柏。所有元組對象都是不可變的尼斧,在創(chuàng)建之后不能更改。
元組通過接口 Tuple 的靜態(tài)方法 of 來創(chuàng)建试吁。元組類也提供了一些方法對它們進(jìn)行操作棺棵。由于元組是不可變的,所有相關(guān)的操作都返回一個(gè)新的元組對象熄捍。在 清單 1 中烛恤,使用 Tuple.of 創(chuàng)建了一個(gè) Tuple2 對象。Tuple2 的 map 方法用來轉(zhuǎn)換元組中的每個(gè)元素余耽,返回新的元組對象缚柏。而 apply 方法則把元組轉(zhuǎn)換成單個(gè)值。其他元組類也有類似的方法碟贾。除了 map 方法之外币喧,還有 map1轨域、map2、map3 等方法來轉(zhuǎn)換第 N 個(gè)元素杀餐;update1干发、update2 和 update3 等方法用來更新單個(gè)元素。
清單 1. 使用元組
Tuple2<String, Integer> tuple2 = Tuple.of("Hello", 100);
Tuple2<String, Integer> updatedTuple2 = tuple2.map(String::toUpperCase, v -> v * 5);
String result = updatedTuple2.apply((str, number) -> String.join(", ",
str, number.toString()));
System.out.println(result);
雖然元組使用起來很方便史翘,但是不宜濫用枉长,尤其是元素?cái)?shù)量超過 3 個(gè)的元組。當(dāng)元組的元素?cái)?shù)量過多時(shí)恶座,很難明確地記住每個(gè)元素的位置和含義搀暑,從而使得代碼的可讀性變差。這個(gè)時(shí)候使用 Java 類是更好的選擇跨琳。
函數(shù)
Java 8 中只提供了接受一個(gè)參數(shù)的 Function 和接受 2 個(gè)參數(shù)的 BiFunction。Vavr 提供了函數(shù)式接口 Function0桐罕、Function1 到 Function8脉让,可以描述最多接受 8 個(gè)參數(shù)的函數(shù)。這些接口的方法 apply 不能拋出異常功炮。如果需要拋出異常溅潜,可以使用對應(yīng)的接口 CheckedFunction0、CheckedFunction1 到 CheckedFunction8薪伏。
Vavr 的函數(shù)支持一些常見特征滚澜。
組合
函數(shù)的組合指的是用一個(gè)函數(shù)的執(zhí)行結(jié)果作為參數(shù),來調(diào)用另外一個(gè)函數(shù)所得到的新函數(shù)嫁怀。比如 f 是從 x 到 y 的函數(shù)设捐,g 是從 y 到 z 的函數(shù),那么 g(f(x))是從 x 到 z 的函數(shù)塘淑。Vavr 的函數(shù)式接口提供了默認(rèn)方法 andThen 把當(dāng)前函數(shù)與另外一個(gè) Function 表示的函數(shù)進(jìn)行組合萝招。Vavr 的 Function1 還提供了一個(gè)默認(rèn)方法 compose 來在當(dāng)前函數(shù)執(zhí)行之前執(zhí)行另外一個(gè) Function 表示的函數(shù)。
在清單 2 中存捺,第一個(gè) function3 進(jìn)行簡單的數(shù)學(xué)計(jì)算槐沼,并使用 andThen 把 function3 的結(jié)果乘以 100。第二個(gè) function1 從 String 的 toUpperCase 方法創(chuàng)建而來捌治,并使用 compose 方法與 Object 的 toString 方法先進(jìn)行組合岗钩。得到的方法對任何 Object 先調(diào)用 toString,再調(diào)用 toUpperCase肖油。
清單 2. 函數(shù)的組合
Function3< Integer, Integer, Integer, Integer> function3 = (v1, v2, v3)
-> (v1 + v2) * v3;
Function3< Integer, Integer, Integer, Integer> composed =
function3.andThen(v -> v * 100);
int result = composed.apply(1, 2, 3);
System.out.println(result);
// 輸出結(jié)果 900
Function1< String, String> function1 = String::toUpperCase;
Function1< Object, String> toUpperCase = function1.compose(Object::toString);
String str = toUpperCase.apply(List.of("a", "b"));
System.out.println(str);
// 輸出結(jié)果[A, B]
部分應(yīng)用
在 Vavr 中兼吓,函數(shù)的 apply 方法可以應(yīng)用不同數(shù)量的參數(shù)。如果提供的參數(shù)數(shù)量小于函數(shù)所聲明的參數(shù)數(shù)量(通過 arity() 方法獲裙乖稀)周蹭,那么所得到的結(jié)果是另外一個(gè)函數(shù)趋艘,其所需的參數(shù)數(shù)量是剩余未指定值的參數(shù)的數(shù)量。在清單 3 中凶朗,F(xiàn)unction4 接受 4 個(gè)參數(shù)瓷胧,在 apply 調(diào)用時(shí)只提供了 2 個(gè)參數(shù),得到的結(jié)果是一個(gè) Function2 對象棚愤。
清單 3. 函數(shù)的部分應(yīng)用
Function4< Integer, Integer, Integer, Integer, Integer> function4 =
(v1, v2, v3, v4) -> (v1 + v2) * (v3 + v4);
Function2< Integer, Integer, Integer> function2 = function4.apply(1, 2);
int result = function2.apply(4, 5);
System.out.println(result);
// 輸出 27
柯里化方法
使用 curried 方法可以得到當(dāng)前函數(shù)的柯里化版本搓萧。由于柯里化之后的函數(shù)只有一個(gè)參數(shù),curried 的返回值都是 Function1 對象宛畦。在清單 4 中瘸洛,對于 function3,在第一次的 curried 方法調(diào)用得到 Function1 之后次和,通過 apply 來為第一個(gè)參數(shù)應(yīng)用值反肋。以此類推,通過 3 次的 curried 和 apply 調(diào)用踏施,把全部 3 個(gè)參數(shù)都應(yīng)用值石蔗。
清單 4. 函數(shù)的柯里化
Function3<Integer, Integer, Integer, Integer> function3 = (v1, v2, v3)
-> (v1 + v2) * v3;
int result =
function3.curried().apply(1).curried().apply(2).curried().apply(3);
System.out.println(result);
記憶化方法
使用記憶化的函數(shù)會(huì)根據(jù)參數(shù)值來緩存之前計(jì)算的結(jié)果。對于同樣的參數(shù)值畅形,再次的調(diào)用會(huì)返回緩存的值养距,而不需要再次計(jì)算。這是一種典型的以空間換時(shí)間的策略日熬」餮幔可以使用記憶化的前提是函數(shù)有引用透明性。
在清單 5 中竖席,原始的函數(shù)實(shí)現(xiàn)中使用 BigInteger 的 pow 方法來計(jì)算乘方耘纱。使用 memoized 方法可以得到該函數(shù)的記憶化版本。接著使用同樣的參數(shù)調(diào)用兩次并記錄下時(shí)間怕敬。從結(jié)果可以看出來揣炕,第二次的函數(shù)調(diào)用的時(shí)間非常短,因?yàn)橹苯訌木彺嬷蝎@取結(jié)果东跪。
清單 5. 函數(shù)的記憶化
Function2<BigInteger, Integer, BigInteger> pow = BigInteger::pow;
Function2<BigInteger, Integer, BigInteger> memoized = pow.memoized();
long start = System.currentTimeMillis();
memoized.apply(BigInteger.valueOf(1024), 1024);
long end1 = System.currentTimeMillis();
memoized.apply(BigInteger.valueOf(1024), 1024);
long end2 = System.currentTimeMillis();
System.out.printf("%d ms -> %d ms", end1 - start, end2 - end1);
注意畸陡,memoized 方法只是把原始的函數(shù)當(dāng)成一個(gè)黑盒子,并不會(huì)修改函數(shù)的內(nèi)部實(shí)現(xiàn)虽填。因此丁恭,memoized 并不適用于直接封裝本系列第二篇文章中用遞歸方式計(jì)算斐波那契數(shù)列的函數(shù)。這是因?yàn)樵诤瘮?shù)的內(nèi)部實(shí)現(xiàn)中斋日,調(diào)用的仍然是沒有記憶化的函數(shù)牲览。
值
Vavr 中提供了一些不同類型的值。
Option
Vavr 中的 Option 與 Java 8 中的 Optional 是相似的恶守。不過 Vavr 的 Option 是一個(gè)接口第献,有兩個(gè)實(shí)現(xiàn)類 Option.Some 和 Option.None贡必,分別對應(yīng)有值和無值兩種情況。使用 Option.some 方法可以創(chuàng)建包含給定值的 Some 對象庸毫,而 Option.none 可以獲取到 None 對象的實(shí)例仔拟。Option 也支持常用的 map、flatMap 和 filter 等操作飒赃,如清單 6 所示利花。
清單 6. 使用 Option 的示例
Option<String> str = Option.of("Hello");
str.map(String::length);
str.flatMap(v -> Option.of(v.length()));
Either
Either 表示可能有兩種不同類型的值,分別稱為左值或右值载佳。只能是其中的一種情況炒事。Either 通常用來表示成功或失敗兩種情況。慣例是把成功的值作為右值蔫慧,而失敗的值作為左值挠乳。可以在 Either 上添加應(yīng)用于左值或右值的計(jì)算藕漱。應(yīng)用于右值的計(jì)算只有在 Either 包含右值時(shí)才生效欲侮,對左值也是同理。
在清單 7 中肋联,根據(jù)隨機(jī)的布爾值來創(chuàng)建包含左值或右值的 Either 對象。Either 的 map 和 mapLeft 方法分別對右值和左值進(jìn)行計(jì)算刁俭。
清單 7. 使用 Either 的示例
import io.vavr.control.Either;
import java.util.concurrent.ThreadLocalRandom;
public class Eithers {
? private static ThreadLocalRandom random =
ThreadLocalRandom.current();
? public static void main(String[] args) {
? ? Either<String, String> either = compute()
? ? ? ? .map(str -> str + " World")
? ? ? ? .mapLeft(Throwable::getMessage);
? ? System.out.println(either);
? }
? private static Either<Throwable, String> compute() {
? ? return random.nextBoolean()
? ? ? ? ? Either.left(new RuntimeException("Boom!"))
? ? ? ? : Either.right("Hello");
? }
}
Try
Try 用來表示一個(gè)可能產(chǎn)生異常的計(jì)算橄仍。Try 接口有兩個(gè)實(shí)現(xiàn)類,Try.Success 和 Try.Failure牍戚,分別表示成功和失敗的情況侮繁。Try.Success 封裝了計(jì)算成功時(shí)的返回值,而 Try.Failure 則封裝了計(jì)算失敗時(shí)的 Throwable 對象如孝。Try 的實(shí)例可以從接口 CheckedFunction0宪哩、Callable、Runnable 或 Supplier 中創(chuàng)建第晰。Try 也提供了 map 和 filter 等方法锁孟。值得一提的是 Try 的 recover 方法,可以在出現(xiàn)錯(cuò)誤時(shí)根據(jù)異常進(jìn)行恢復(fù)茁瘦。
在清單 8 中品抽,第一個(gè) Try 表示的是 1/0 的結(jié)果,顯然是異常結(jié)果甜熔。使用 recover 來返回 1圆恤。第二個(gè) Try 表示的是讀取文件的結(jié)果。由于文件不存在腔稀,Try 表示的也是異常盆昙。
清單 8. 使用 Try 的示例
Try<Integer> result = Try.of(() -> 1 / 0).recover(e -> 1);
System.out.println(result);
Try<String> lines = Try.of(() -> Files.readAllLines(Paths.get("1.txt")))
? ? .map(list -> String.join(",", list))
? ? .andThen((Consumer<String>) System.out::println);
System.out.println(lines);
Lazy
Lazy 表示的是一個(gè)延遲計(jì)算的值羽历。在第一次訪問時(shí)才會(huì)進(jìn)行求值操作,而且該值只會(huì)計(jì)算一次淡喜。之后的訪問操作獲取的是緩存的值秕磷。在清單 9 中,Lazy.of 從接口 Supplier 中創(chuàng)建 Lazy 對象拆火。方法 isEvaluated 可以判斷 Lazy 對象是否已經(jīng)被求值跳夭。
清單 9. 使用 Lazy 的示例
Lazy<BigInteger> lazy = Lazy.of(() ->
BigInteger.valueOf(1024).pow(1024));
System.out.println(lazy.isEvaluated());
System.out.println(lazy.get());
System.out.println(lazy.isEvaluated());
數(shù)據(jù)結(jié)構(gòu)
Vavr 重新在 Iterable 的基礎(chǔ)上實(shí)現(xiàn)了自己的集合框架。Vavr 的集合框架側(cè)重在不可變上们镜。Vavr 的集合類在使用上比 Java 流更簡潔币叹。
Vavr 的 Stream 提供了比 Java 中 Stream 更多的操作∧O粒可以使用 Stream.ofAll 從 Iterable 對象中創(chuàng)建出 Vavr 的 Stream颈抚。下面是一些 Vavr 中添加的實(shí)用操作:
groupBy:使用 Fuction 對元素進(jìn)行分組。結(jié)果是一個(gè) Map嚼鹉,Map 的鍵是分組的函數(shù)的結(jié)果贩汉,而值則是包含了同一組中全部元素的 Stream。
partition:使用 Predicate 對元素進(jìn)行分組锚赤。結(jié)果是包含 2 個(gè) Stream 的 Tuple2匹舞。Tuple2 的第一個(gè) Stream 的元素滿足 Predicate 所指定的條件,第二個(gè) Stream 的元素不滿足 Predicate 所指定的條件线脚。
scanLeft 和 scanRight:分別按照從左到右或從右到左的順序在元素上調(diào)用 Function赐稽,并累積結(jié)果。
zip:把 Stream 和一個(gè) Iterable 對象合并起來浑侥,返回的結(jié)果 Stream 中包含 Tuple2 對象姊舵。Tuple2 對象的兩個(gè)元素分別來自 Stream 和 Iterable 對象。
在清單 10 中寓落,第一個(gè) groupBy 操作把 Stream 分成奇數(shù)和偶數(shù)兩組括丁;第二個(gè) partition 操作把 Stream 分成大于 2 和不大于 2 兩組;第三個(gè) scanLeft 對包含字符串的 Stream 按照字符串長度進(jìn)行累積伶选;最后一個(gè) zip 操作合并兩個(gè)流史飞,所得的結(jié)果 Stream 的元素?cái)?shù)量與長度最小的輸入流相同。
清單 10. Stream 的使用示例
Map<Boolean, List<Integer>> booleanListMap = Stream.ofAll(1, 2, 3, 4, 5)
? ? .groupBy(v -> v % 2 == 0)
? ? .mapValues(Value::toList);
System.out.println(booleanListMap);
// 輸出 LinkedHashMap((false, List(1, 3, 5)), (true, List(2, 4)))
Tuple2<List<Integer>, List<Integer>> listTuple2 = Stream.ofAll(1, 2, 3, 4)
? ? .partition(v -> v > 2)
? ? .map(Value::toList, Value::toList);
System.out.println(listTuple2);
// 輸出 (List(3, 4), List(1, 2))
List<Integer> integers = Stream.ofAll(List.of("Hello", "World", "a"))
? ? .scanLeft(0, (sum, str) -> sum + str.length())
? ? .toList();
System.out.println(integers);
// 輸出 List(0, 5, 10, 11)
List<Tuple2<Integer, String>> tuple2List = Stream.ofAll(1, 2, 3)
? ? .zip(List.of("a", "b"))
? ? .toList();
System.out.println(tuple2List);
// 輸出 List((1, a), (2, b))
Vavr 提供了常用的數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)考蕾,包括 List祸憋、Set、Map肖卧、Seq蚯窥、Queue、Tree 和 TreeMap 等。這些數(shù)據(jù)結(jié)構(gòu)的用法與 Java 標(biāo)準(zhǔn)庫的對應(yīng)實(shí)現(xiàn)是相似的拦赠,但是提供的操作更多巍沙,使用起來也更方便。在 Java 中荷鼠,如果需要對一個(gè) List 的元素進(jìn)行 map 操作句携,需要使用 stream 方法來先轉(zhuǎn)換為一個(gè) Stream,再使用 map 操作允乐,最后再通過收集器 Collectors.toList 來轉(zhuǎn)換回 List矮嫉。而在 Vavr 中,List 本身就提供了 map 操作牍疏。清單 11 中展示了這兩種使用方式的區(qū)別蠢笋。
清單 11. Vavr 中數(shù)據(jù)結(jié)構(gòu)的用法
List.of(1, 2, 3).map(v -> v + 10); //Vavr
java.util.List.of(1, 2, 3).stream()
? .map(v -> v + 10).collect(Collectors.toList()); //Java 中 Stream
模式匹配
在 Java 中,我們可以使用 switch 和 case 來根據(jù)值的不同來執(zhí)行不同的邏輯鳞陨。不過 switch 和 case 提供的功能很弱昨寞,只能進(jìn)行相等匹配。Vavr 提供了模式匹配的 API厦滤,可以對多種情況進(jìn)行匹配和執(zhí)行相應(yīng)的邏輯援岩。在清單 12 中,我們使用 Vavr 的 Match 和 Case 替換了 Java 中的 switch 和 case掏导。Match 的參數(shù)是需要進(jìn)行匹配的值享怀。Case 的第一個(gè)參數(shù)是匹配的條件,用 Predicate 來表示趟咆;第二個(gè)參數(shù)是匹配滿足時(shí)的值凹蜈。$(value) 表示值為 value 的相等匹配,而 $() 表示的是默認(rèn)匹配忍啸,相當(dāng)于 switch 中的 default。
清單 12. 模式匹配的示例
String input = "g";
String result = Match(input).of(
? ? Case($("g"), "good"),
? ? Case($("b"), "bad"),
? ? Case($(), "unknown")
);
System.out.println(result);
// 輸出 good
在清單 13 中履植,我們用 $(v -> v > 0) 創(chuàng)建了一個(gè)值大于 0 的 Predicate计雌。這里匹配的結(jié)果不是具體的值,而是通過 run 方法來產(chǎn)生副作用玫霎。
清單 13. 使用模式匹配來產(chǎn)生副作用
int value = -1;
Match(value).of(
? ? Case($(v -> v > 0), o -> run(() -> System.out.println("> 0"))),
? ? Case($(0), o -> run(() -> System.out.println("0"))),
? ? Case($(), o -> run(() -> System.out.println("< 0")))
);
// 輸出<? 0
總結(jié)
當(dāng)需要在 Java 平臺(tái)上進(jìn)行復(fù)雜的函數(shù)式編程時(shí)凿滤,Java 標(biāo)準(zhǔn)庫所提供的支持已經(jīng)不能滿足需求。Vavr 作為 Java 平臺(tái)上流行的函數(shù)式編程庫庶近,可以滿足不同的需求翁脆。本文對 Vavr 提供的元組、函數(shù)鼻种、值反番、數(shù)據(jù)結(jié)構(gòu)和模式匹配進(jìn)行了詳細(xì)的介紹。下一篇文章將介紹函數(shù)式編程中的重要概念 Monad。
參考資源
參考 Vavr 的官方文檔罢缸。
查看 Vavr 的 Java API 文檔篙贸。
?