Introducing FP in Java8

背景

自從2013年放棄了Java就再也沒有碰過。期間Java還發布了重大更新:引入lambda,但是那會兒我已經玩了一段時間Scala,對Java已經瞧不上眼。相比Scala Java 8 的lambda還是too young, too naive!再后來,我有機會學習了一下C#。雖然C#師承Java,但它已經是一門非常現代化的語言了,青出于藍而勝于藍。首先是LINQ,提供了一致的,并且非常有表達力的數據操作API。還有像擴展函數,匿名類這些語法特性,用起來也是非常趁手。在技術層面微軟比甲骨文強了不知道多少倍。而且這幾年Java社區也有點蕭條,國內再也沒有出現過類似JavaEye這樣的高質量技術社區。種種原因都導致我離Java越來越遠。

今年五月份,公司要做新的業務,團隊里的一些老成員已經被動態語言(主要是Python,還有一些Ruby)折磨的無可奈何,決定重新用回Java。如果不是應為這,我恐怕再也不會用Java了。但是工作嘛,沒有太多選擇的余地。既然團隊決定了那就硬著頭皮上吧。第一步肯定是要了解一下Java8的函數式特性(這可是千呼萬喚始出來啊)。這段時間用下來的總體感覺是,沒我想象中的那么糟,還湊合。下邊總結了一些Java8函數式編程的要點:包括Optional,lambda表達式,Stream

Optional

NullPointerException,這個應該是Java里被人詬病最多的了,有的公司應為這個損失的錢可不是小數。那在Java 8里引入了一個Optional類型來解決這個問題。Optional是什么呢?在函數式編程里Optional其實就是一個Monad。和其他編程語言中,比如Scala的Option,Haskell的Maybe異曲同工。

在沒有Optional之前,通常做法是返回null,或者程序拋出異常,具體使用要看團隊的規范。但這兩種方式都有各自的問題,我再數落一遍。

對于返回null,試想如果所有的方法都由可能返回null,那對方法使用者來說非常恐怖的。處處防,處處防。而且每一個null都是一個地雷,哪一個地方疏忽了都有可能被“炸”到。

拋出異常呢,不會有處處寫防御代碼的問題了。但是這種解決方案也是非常“湊合”。Java中異常分為兩種:受檢異常和非受檢異常。如果使用了非受檢異常程序就會直接被異常中斷,通常在Spring中是提倡直接拋出非受檢異常的,再搭配上Spring的攔截器,省去了程序員不少麻煩。

受檢異常就不一樣了,使用受檢異常絕不會比使用null好到哪里去。在方法的簽名上附帶上函數可能拋出的異常,讓方法使用者去判斷如何處理這個異常。看起來這是一種負責任的做法,事實確是把自己不想做的事情(異常處理)交給了方法調用者。Jackson類庫就是這樣,每次使用它序列化對象時都得考慮是try-catch還是修改方法簽名(告知外部方法處理)。非常不友好。

再有一點,拋出Exception意味著這個函數(方法)是帶有副作用的。函數副作用會給程序設計帶來不必要的麻煩,引入潛在的bug,并降低程序的可讀性。試想一個函數從其簽名上來看應該返回一個訂單,但是結果卻是它不總能夠返回訂單,有時還拋出異常。

終于可以開始說Optional了。用白話形容,Optional就是一個盒子。當你拿到這個盒子的時候,盒子可能里邊有想要的東西,但也可能只是一個空盒子。所以原來返回null,或者跑出異常的方法我們可以直接返回一個盒子。來一個例子,這個例子來改進一下Jackson的API:

public Optional<String> json(Object object) {
    String jsonString = null;
    try {
        jsonString = new ObjectMapper().writeValueAsString(object);
    } catch (JsonProcessingException e) {
        e.printStackTrace();
    }

    return Optional.ofNullable(jsonString);
}

這個新的方法不再讓調用者處理異常,也不存在返回null的風險。但是問題來了,拿到了這個“盒子”我們接下來該怎么辦啊?別著急,Optional提供了非常豐富的接口,幾乎能夠滿足你日常全部的使用場景。以下接口都是非常常用的:

  1. 判斷Optional是否有值:op.isPresent()
  2. 基于Optional封裝的結果做一些操作,并繼續返回一個Optional類型的結果:op.map(str -> str.trim())
  3. 合并兩個Optional為一個:op.flatMap(s -> op2.map(s2 -> s1 + s2))
  4. 只有在有值的情況下才進行一些操作:op.ifPresent(s -> s.trim())
  5. 如果Optional有值則返回,沒有返回給定的默認值:op.orElse("hello")
    還有一些其它的接口,請自行查閱Optional API文檔。注意在使用Optional的時候不推薦直接使用get(),因為這樣可能會拋出異常。

我曾經把方法的參數也置為Optional類型,但是的到了IDEA的警告:Optional不推薦用作方法的參數。我隨后Google了一些帖子,得到的答案就是:對于方法的參數,null可能比Optional更好用一些。在Scala和Haskell里是沒有這樣的限制的,你可以任意的把Option和Maybe當做參數。因為在Scala和Haskell中Option和Maybe是基本的數據類型。Java中的Optional則只是一種受限的實現,主要的目的是提供一種清晰,友好的表達“空”的方式。

Lambda 表達式

lambda表達式在上邊我們已經用到了,比如op.map(str -> str.trim())str -> str.trim()就是一個lambda表達式。Java和其他大多數支持lambda的語言一樣采用箭頭:-> 來標示lambda。在箭頭的的左邊是0或多個參數,箭頭的右邊是一個表達式或者代碼塊。我們來看幾個lambda的實例:


() -> {}                // No parameters; result is void
() -> 42                // No parameters, expression body
() -> null              // No parameters, expression body
() -> { return 42; }    // No parameters, block body with return
() -> { System.gc(); }  // No parameters, void block body

() -> {                 // Complex block body with returns
  if (true) return 12;
  else {
    int result = 15;
    for (int i = 1; i < 10; i++)
      result *= i;
    return result;
  }
}                          

(int x) -> x+1              // Single declared-type parameter
(int x) -> { return x+1; }  // Single declared-type parameter
(x) -> x+1                  // Single inferred-type parameter
x -> x+1                    // Parentheses optional for
                            // single inferred-type parameter

(String s) -> s.length()      // Single declared-type parameter
(Thread t) -> { t.start(); }  // Single declared-type parameter
s -> s.length()               // Single inferred-type parameter
t -> { t.start(); }           // Single inferred-type parameter

(int x, int y) -> x+y  // Multiple declared-type parameters
(x, y) -> x+y          // Multiple inferred-type parameters
(x, int y) -> x+y    // Illegal: can't mix inferred and declared types
(x, final y) -> x+y  // Illegal: no modifiers with inferred types

憶苦思甜,我們先來回憶一下在沒有lambda之前如果我們想為Optional實現map功能我們應該如何做。通常做法是這樣的:

interface Function<E> {
    public E exec(E e);
}

public static <E> Optional<E> map(Optional<E> original, Function<E> fn) {
    if(!original.isPresent()) return Optional.empty();
    E e = original.get();
    return Optional.of(fn.exec(e));
}

map(Optional.of(1), new Function<Integer>() {
    @Override public Integer exec(final Integer i) {
        return i * i;
    }
});

使用一個接口來承載一個函數。在Java中函數不是一等公民,是不能用來傳遞的。所以只能采用這種“曲線救國”的方式。Java 8則是把這種”曲線救國”拿到了臺面上,并昭告天下,同時還對lambda提供了一些語法支持。所以上邊我們看到的一些非常簡短的lambda表達式,其實都是一個interface+一個抽象方法。

我們可以借助IDE(我使用的是IDEA)把上邊列舉的一些lambda表達式抽作變量,IDE可以幫助我們自動推導類型,我們來觀察下他們的類型。

Runnable runnable = () -> {};
DoubleSupplier doubleSupplier = () -> 42;
Callable vCallable = () -> null;
IntToDoubleFunction intToDoubleFunction = (int x) -> x + 1;
IntBinaryOperator intBinaryOperator = (int x, int y) -> x + y;

像Callable,Runnable,DoubleSupplier這些接口就是Java內置的函數式接口。Java還提供了非常多的函數式接口,你可以在java.util.function下找到他們。函數式接口相比普通的接口有一個限制:只能有一個抽象方法。而且Java還提供了一個注解:@FunctionalInterface。你可以自己聲明新的接口并為它加上這個注解。

@FunctionalInterface
interface Function<E> {
    public E exec(E e);
}

上邊說過Java 8對lambda提供了一些額外支持,這種額外的支持就是一些已經實現的方法也能夠用作lambda表達式。我們看一個例子:

Optional<Integer> arg = ...;
arg.ifPresent(System.out::print);

print是PrintStream中已經實現的方法。這種用法相當于:x -> System.out.print(x)。對于類的靜態方法,類的實例方法,對象的實例方法都可以使用::操作符用在需要傳遞lambda表達式的地方。

我們接下來比較一下Java8前后,實現閉包的異同。先來看一下閉包的概念。
閉包是指可以包含自由變量的代碼塊。自由變量沒有在當前代碼塊內或者任何全局上下文中定義的,而是在定義代碼塊的環境中定義(執行環境上下文)。所以一次閉包過程即要執行的代碼塊中的自由變量獲得了執行環境上下文中的值。

在Java 8之前閉包可以通過內部類來實現。

import java.util.HashMap;
import java.util.Map;

class Cache {
    private Map<String, Object> contents = new HashMap<>();

    class Monitor {
        public Integer size() {
            return Cache.this.contents.size();
        }
    }
}

public class Library {
    public static void main(String[] args) {
        Cache cache = new Cache();
        Cache.Monitor monitor = cache.new Monitor();
        System.out.println("Cache size is: " + monitor.size());
    }
}

contents是自由變量,cache提供執行上下文,monitor.size()觸發閉包執行。如果有其他函數式語言背景的人看到這種方式可能會感到非常的奇怪,但這就是Java的方式。再來看一下Java 8中如何實現一個閉包。
由于有了lambda表達式,創建一個閉包就相當簡潔了:(Integer x) -> x + y。而且這種形式也非常的functional。接著創建一個執行上下文:

int y = 1;
Function<Integer, Integer> add = (Integer x) -> x + y;

add.apply(3); // 4

兩種風格迥異,單從語法表達力來說肯定是lambda更勝一籌。但社區里也有人擔心這種簡潔性會影響Java的簡單,造成代碼風格不一致,降低代碼可讀性增加維護成本。仁者見仁智者見智吧,我肯定是支持使用lambda的。代碼風格的話團隊最好能有一個標準,不要好東西給用爛了。

Stream

Stream可以說是集合處理的殺手锏(就當它是殺手锏吧)。我們先拿Stream來玩一玩:
Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).map(i -> i * i).reduce(0, (i, r) -> r + i)
這個例子是計算0到9的平方和。我們可以想象一下如果用for循環實現同樣的邏輯,代碼行數至少是四五倍。

下面我們對這個程序作一個分解:第一部分是初始化Stream,加載數據Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);第二部分是定義數據轉化:map(i -> i * i),第三部分是聚合結果:reduce(0, (i, r) -> r + i)。這基本囊括了Stream所有能作以及要作的事情。

創建函數

Stream類提供了提供了幾個工廠方法來構建一個新的Stream。我們先來看一下of,of用來構建有限個數的Stream,比如我們構建一個包含十個元素的Stream:Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 0)

Stream還有兩個方法:generate, iterate,這兩個函數都是用來構造無限流。

generate就收一個無參函數:Supplier<T> s,當需要一個常數或隨機數隊列的時候可以使用這個函數。Stream.generate(() -> 1)

iterate會生成一個迭代的Stream,你需要指定初始值,以及迭代函數。Stream.iterate(5, x -> x * 10)

除了上邊的幾種方式,我們還能夠方便的將集合轉化為Stream,比如List,Set。你只需要在集合變量上.stream()就可以得到一個Stream。實際場景中應用更多的還是將一個集合類轉化為Stream。

轉換函數

轉化函數我們最常用的是map和filter。Stream也提供了flatMap函數,但是它的使用比較受限,所以原本flatMap的威力大大減弱了。下邊具體解釋一下map和filter函數。map的工作原理是:為所有的元素應用一個函數。為了增強理解舉個現實生活中的例子:有一簍子蘋果,我們要為它們都貼上標簽。map過程就是拿出每個蘋果貼上標簽然后放到另一個簍子里。filter就是一個過濾器,過濾出我們想要的東西。比如,我們要從上邊簍子里挑選出大于500克的蘋果。逐個拿出蘋果稱重,如果大于500克留在簍子中,則放到新簍子中。所以操作數據時,直接往這兩個例子上套用就可以了。

看一個例子,計算所有學生的總分,并取出總分大于450分的學生:
students.map(s -> calculate(s.getScore())).filter(score -> score > 450);

聚合函數

聚合函數主要用來收集最終的結果。比如,求出一個數字隊列的總和,求最大最小值,或將結果收集到一個集合中。下邊我們來操作一個Integer的Stream,首先求出最大值和最小值:

Stream<Integer> ints = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 0);

Optional<Integer> max = ints.max(Integer::compare);
Optional<Integer> min = ints.min(Integer::compare);

接下來我們求出這個數列的總和,求和我們可以使用reduce函數。reduce函數起到一個匯總的作用。它和hadoop中的reduce,fork/join中的join作用都是一樣的。

Integer sum = ints.reduce(0, (x, y) -> x + y);

我們為reduce傳遞了兩個參數,第一個是初始值(執行累加操作的第一個值),第二個是求和lambda。

Java 8還為基本類型提供了相應的Stream,比如IntStream,LongStream。使用IntStream我們直接使用sum就可以執行求和操作:

IntStream intsStream = ints.mapToInt(i -> i);
intsStream.sum();

下邊我們看一下收集函數:collect()。collect()函數是將Stream中的元素放到一個新的容器中。我們上邊的例子中求出了總分大于450分的同學,我們把它放到一個List中,看一下如何操作:

students.map(s -> calculate(s.getScore())).filter(score -> score > 450).collect(Collectors.toList())

以上Java 8的函數式特性全部講完,這只是一個入門講解,不能涵蓋所有的特性及工具類。有興趣的同學,自行探索java.util.stream包。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,836評論 18 139
  • Java8 in action 沒有共享的可變數據,將方法和函數即代碼傳遞給其他方法的能力就是我們平常所說的函數式...
    鐵牛很鐵閱讀 1,259評論 1 2
  • 第一章 為什么要關心Java 8 使用Stream庫來選擇最佳低級執行機制可以避免使用Synchronized(同...
    謝隨安閱讀 1,508評論 0 4
  • Java 8自Java 5(發行于2004)以來最具革命性的版本。Java 8 為Java語言、編譯器、類庫、開發...
    誰在烽煙彼岸閱讀 907評論 0 4
  • 假期結束,收收心,上學啦,今早送去上學,害怕的一幕出現了,又開始發兇,不進校門,好說歹說,總算進去了 中午放學去接...
    琪子媽閱讀 263評論 0 4