這個(gè)類庫(kù)可以幫助你理解Java中的函數(shù)式編程
每當(dāng)JDK發(fā)布了新版本就有同學(xué)說“你發(fā)任你發(fā),我用Java 8”,可在工作中有不少人依然不太擅長(zhǎng)使用Java8的新特性,而這些特性往往讓Java不再“臃腫”。不過我個(gè)人認(rèn)為Java8所有的新特性中最具有代表性的一定是函數(shù)式編程。有人會(huì)說這種風(fēng)格太抽象難懂了,當(dāng)你熟練掌握這種設(shè)定之后,你一定會(huì)感到很香。慢慢地你也會(huì)領(lǐng)會(huì)到函數(shù)式編程的魅力和精髓。今天介紹一個(gè)函數(shù)式Java工具包,它表現(xiàn)了很多優(yōu)秀的函數(shù)式編程思想。以前介紹的熔斷降級(jí)組件Hystrix的替代品resilience4j就基于vavr庫(kù)。
Vavr
Vavr是一個(gè)Java8函數(shù)庫(kù),它運(yùn)用了大量的函數(shù)式編程范式。創(chuàng)造性地封裝了一些持久性的數(shù)據(jù)結(jié)構(gòu)和函數(shù)式控制結(jié)構(gòu)。而且從中可以學(xué)到很多有用的編程思想。
可觀察的副作用
我們的代碼中經(jīng)常會(huì)出現(xiàn)一些看不見的陷阱,從代碼語(yǔ)義中這些陷阱是無(wú)法被觀察的。例如
- int divide(int a, int b){
- return a/b;
- }
我們知道a/b會(huì)得到一個(gè)整數(shù),但是卻不能從代碼上明確地知道如果b=0將會(huì)拋出java.lang.ArithmeticException異常;而如果是a+b則不會(huì)帶來(lái)任何副作用。所以我們需要讓這種副作用是可觀察的。對(duì)于這一點(diǎn)Vavr做出了一種設(shè)計(jì):
- Try<Integer> divide(Integer a, Integer b) {
- return Try.of(() -> a / b);
- }
將可能的副作用封裝到一個(gè)容器中,明確了可能的失敗,當(dāng)你看到返回的是Try
不可變的數(shù)據(jù)結(jié)構(gòu)
很多語(yǔ)言都在使用不可變的數(shù)據(jù)結(jié)構(gòu),比如Golang、Kotlin。主要原因是不可變的值:
- 本質(zhì)上是線程安全的,因此不需要同步
- 對(duì)于equals和hashCode是可靠的
- 不需要克隆
- 在非受檢unchecked類型轉(zhuǎn)換中是類型安全的
- 對(duì)于函數(shù)式編程來(lái)說不可變值是最透明的
為此Vavr設(shè)計(jì)了一個(gè)集合類庫(kù),旨在代替Java中的集合框架。Vavr 的集合庫(kù)包含一組豐富的函數(shù)式數(shù)據(jù)結(jié)構(gòu),這些數(shù)據(jù)結(jié)構(gòu)建立在 lambdas 之上。它們與 Java 原始集合共享的唯一接口是Iterable。這些數(shù)據(jù)結(jié)構(gòu)是持久性的,一旦初始化本身就不可改變,你可以使用一些操作來(lái)返回更改后的副本。例如經(jīng)典的數(shù)據(jù)結(jié)構(gòu)單向鏈表:
- // 1 2 3
- List<Integer> source = List.of(1, 2, 3);
如果我們將一個(gè)新元素0放在原始鏈表尾部的前面
- // 0 2 3
- List<Integer> newHeadList = source.tail().prepend(0);
- // 1 2 3
- System.out.println(source);
原始鏈表保持不變,新的鏈表大小保持不變?cè)乇惶鎿Q了。當(dāng)然你可以使用其它API來(lái)生成一個(gè)大小變化的副本,不過可以肯定的是原始的鏈表一定不會(huì)發(fā)生改變。
- // 0 1 2 3
- List<Integer> prepend = source.prepend(0);
- // 1 2 3 0
- List<Integer> append = source.append(0);
這只是其中的一部分編程思想,接下來(lái)我將介紹Vavr的一些特色。
Vavr的一些特色
Vavr提供了一些非常有用的而且有特色的API。
元組
熟悉Python的同學(xué)對(duì)元組(Tuple)一定不陌生。元組將固定數(shù)量的元素組合在一起,以便它們可以作為一個(gè)整體傳遞。與數(shù)組或列表不同,元組可以包含不同類型的對(duì)象,但它也是不可變的。目前Vavr提供了最多8個(gè)元素的元組結(jié)構(gòu)。
- // (felord.cn, 22)
- Tuple2<String, Integer> java8 = Tuple.of("felord.cn", 22);
- // felord.cn
- String s = java8._1;
- // 22
- Integer i = java8._2;
這個(gè)可以用來(lái)模擬Java中不具有的多返回值的特性。
Function
Java本身提供了Function接口,但是Vavr則提供了更加豐富的Function擴(kuò)展,例如可以組合多個(gè)Function
- Function1<Integer, Integer> multiplyByTwo = a -> a * 2;
- Function1<Integer, Integer> compose = multiplyByTwo.compose(a -> a + 1);
- // 6
- Integer apply = compose.apply(2);
除此之外,還可以讓潛在的副作用降級(jí)(lift),有點(diǎn)類似于微服務(wù)的熔斷,以避免在函數(shù)執(zhí)行中處理異常
- Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
- // 降級(jí)
- Function2<Integer, Integer, Option<Integer>> safeDivide = Function2.lift(divide);
- // 返回一個(gè)加強(qiáng)版的Optional
- Option<Integer> apply = safeDivide.apply(1, 0);
- boolean empty = apply.isEmpty();
- // true
- System.out.println(empty);
還有派生操作:
- Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
- Function1<Integer, Integer> a = divide.apply(4);
- Integer apply = a.apply(2);
這有點(diǎn)類似于柯里化,當(dāng)我們用到更多入?yún)r(shí)柯里化才更加明顯:
- Function3<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
- final Function1<Integer, Function1<Integer, Integer>> add2 = sum.curried().apply(1);
- Integer apply = add2.apply(2).apply(3);
猜一猜答案是幾?
帶有特性的值容器
這個(gè)不太好用中文說明,有一些值帶有獨(dú)特的性質(zhì),比如開頭提到的Try,用來(lái)顯式表明可能遇到異常。Vavr提供了很多具有獨(dú)特性質(zhì)的值容器。
Option
類似Optional,但是比Optional更加強(qiáng)大。
Lazy
Lazy是一個(gè)惰性計(jì)算的容器,表示當(dāng)使用時(shí)才去計(jì)算且只計(jì)算一次。
- Lazy<Double> lazy = Lazy.of(Math::random);
- lazy.isEvaluated(); // = false
- lazy.get(); // = 0.123
- lazy.isEvaluated(); // = true
- lazy.get(); // = 0.123
- // 需要使用數(shù)據(jù)時(shí)才從數(shù)據(jù)源加載
- Data lazyData = Lazy.val(DataSourceService::get, Data.class);
其它還有一些非常有用的容器,你可以嘗試它們。
模式匹配
函數(shù)式編程語(yǔ)言大都支持模式匹配,同為JVM語(yǔ)言的Scala中就有這種特性,而Java目前是沒有的??梢杂行У貛椭覀儨p少if-else,舉個(gè)例子:
- public static String convert(int input) {
- String output;
- if (input == 1) {
- output = "one";
- } else if (input == 2) {
- output = "two";
- } else if (input == 3) {
- output = "three";
- } else {
- output = "unknown";
- }
- return output;
- }
你就說吧,繞不繞?,Vavr就清爽多了。
- public static String vavrMatch(int input) {
- return Match(input).of(
- Case($(1), "one"),
- Case($(2), "two"),
- Case($(3), "three"),
- Case($(), "unknown")
- );
- }
當(dāng)然還有其它一些玩法需要你自己去發(fā)現(xiàn)。
總結(jié)
函數(shù)式編程作為Java8最大的一個(gè)亮點(diǎn)(個(gè)人認(rèn)為),對(duì)于習(xí)慣于傳統(tǒng)OOP編程的開發(fā)者來(lái)說確實(shí)不容易接受。你不妨從Vavr類庫(kù)入手去學(xué)習(xí)函數(shù)式編程的思想。今天介紹的只是它很少的一部分,還有更多等著你去發(fā)現(xiàn)、去借鑒。忘記說了,如果你想在項(xiàng)目中引用它,可以引入下面這個(gè)坐標(biāo):
- <!-- https://mvnrepository.com/artifact/io.vavr/vavr -->
- <dependency>
- <groupId>io.vavr</groupId>
- <artifactId>vavr</artifactId>
- <version>0.10.3</version>
- </dependency>
本文轉(zhuǎn)載自微信公眾號(hào)「 碼農(nóng)小胖哥」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系碼農(nóng)小胖哥公眾號(hào)。