Java Lambda表達式

Java Lambda表達式的一個重要用法是簡化某些匿名內部類Anonymous Classes)的寫法声功。實際上Lambda表達式并不僅僅是匿名內部類的語法糖,JVM內部是通過invokedynamic指令來實現Lambda表達式的榨馁。具體原理放到下一篇均唉。本篇我們首先感受一下使用Lambda表達式帶來的便利之處镜会。

Lambda and Anonymous Classes(I)

本節(jié)將介紹如何使用Lambda表達式簡化匿名內部類的書寫檬寂,但Lambda表達式并不能取代所有的匿名內部類,只能用來取代函數接口(Functional Interface)的簡寫戳表。先別在乎細節(jié)桶至,看幾個例子再說。

例子1:無參函數的簡寫

如果需要新建一個線程匾旭,一種常見的寫法是這樣:

// JDK7 匿名內部類寫法
new Thread(new Runnable(){// 接口名
    @Override
    public void run(){// 方法名
        System.out.println("Thread run()");
    }
}).start();

上述代碼給Tread類傳遞了一個匿名的Runnable對象塞茅,重載Runnable接口的run()方法來實現相應邏輯。這是JDK7以及之前的常見寫法。匿名內部類省去了為類起名字的煩惱抵代,但還是不夠簡化涧郊,在Java 8中可以簡化為如下形式:

// JDK8 Lambda表達式寫法
new Thread(
        () -> System.out.println("Thread run()")// 省略接口名和方法名
).start();

上述代碼跟匿名內部類的作用是一樣的,但比匿名內部類更進一步鞭光。這里連接口名和函數名都一同省掉了,寫起來更加神清氣爽泞遗。如果函數體有多行惰许,可以用大括號括起來,就像這樣:

// JDK8 Lambda表達式代碼塊寫法
new Thread(
        () -> {
            System.out.print("Hello");
            System.out.println(" Hoolee");
        }
).start();

例子2:帶參函數的簡寫

如果要給一個字符串列表通過自定義比較器史辙,按照字符串長度進行排序汹买,Java 7的書寫形式如下:

// JDK7 匿名內部類寫法
List<String> list = Arrays.asList("I", "love", "you", "too");
Collections.sort(list, new Comparator<String>(){// 接口名
    @Override
    public int compare(String s1, String s2){// 方法名
        if(s1 == null)
            return -1;
        if(s2 == null)
            return 1;
        return s1.length()-s2.length();
    }
});

上述代碼通過內部類重載了Comparator接口的compare()方法,實現比較邏輯聊倔。采用Lambda表達式可簡寫如下:

// JDK8 Lambda表達式寫法
List<String> list = Arrays.asList("I", "love", "you", "too");
Collections.sort(list, (s1, s2) ->{// 省略參數表的類型
    if(s1 == null)
        return -1;
    if(s2 == null)
        return 1;
    return s1.length()-s2.length();
});

上述代碼跟匿名內部類的作用是一樣的晦毙。除了省略了接口名和方法名,代碼中把參數表的類型也省略了耙蔑。這得益于javac類型推斷機制见妒,編譯器能夠根據上下文信息推斷出參數的類型,當然也有推斷失敗的時候甸陌,這時就需要手動指明參數類型了须揣。注意盐股,Java是強類型語言,每個變量和對象都必需有明確的類型耻卡。

簡寫的依據

也許你已經想到了疯汁,能夠使用Lambda的依據是必須有相應的函數接口(函數接口,是指內部只有一個抽象方法的接口)卵酪。這一點跟Java是強類型語言吻合涛目,也就是說你并不能在代碼的任何地方任性的寫Lambda表達式。實際上Lambda的類型就是對應函數接口的類型凛澎。Lambda表達式另一個依據是類型推斷機制霹肝,在上下文信息足夠的情況下,編譯器可以推斷出參數表的類型塑煎,而不需要顯式指名沫换。Lambda表達更多合法的書寫形式如下:

// Lambda表達式的書寫形式
Runnable run = () -> System.out.println("Hello World");// 1
ActionListener listener = event -> System.out.println("button clicked");// 2
Runnable multiLine = () -> {// 3 代碼塊
    System.out.print("Hello");
    System.out.println(" Hoolee");
};
BinaryOperator<Long> add = (Long x, Long y) -> x + y;// 4
BinaryOperator<Long> addImplicit = (x, y) -> x + y;// 5 類型推斷

上述代碼中,1展示了無參函數的簡寫最铁;2處展示了有參函數的簡寫讯赏,以及類型推斷機制;3是代碼塊的寫法冷尉;4和5再次展示了類型推斷機制漱挎。

自定義函數接口

自定義函數接口很容易,只需要編寫一個只有一個抽象方法的接口即可雀哨。

// 自定義函數接口
@FunctionalInterface
public interface ConsumerInterface<T>{
    void accept(T t);
}

上面代碼中的@FunctionalInterface是可選的磕谅,但加上該標注編譯器會幫你檢查接口是否符合函數接口規(guī)范。就像加入@Override標注會檢查是否重載了函數一樣雾棺。

有了上述接口定義膊夹,就可以寫出類似如下的代碼:

ConsumerInterface<String> consumer = str -> System.out.println(str);

進一步的,還可以這樣使用:

class MyStream<T>{
    private List<T> list;
    ...
    public void myForEach(ConsumerInterface<T> consumer){// 1
        for(T t : list){
            consumer.accept(t);
        }
    }
}
MyStream<String> stream = new MyStream<String>();
stream.myForEach(str -> System.out.println(str));// 使用自定義函數接口書寫Lambda表達式

Lambda and Anonymous Classes(II)

讀過上一篇之后捌浩,相信對Lambda表達式的語法以及基本原理有了一定了解放刨。對于編寫代碼,有這些知識已經夠用尸饺。本文將進一步區(qū)分Lambda表達式和匿名內部類在JVM層面的區(qū)別进统,如果對這一部分不感興趣,可以跳過浪听。

經過第一篇的的介紹螟碎,我們看到Lambda表達式似乎只是為了簡化匿名內部類書寫,這看起來僅僅通過語法糖在編譯階段把所有的Lambda表達式替換成匿名內部類就可以了馋辈。但實時并非如此抚芦。在JVM層面,Lambda表達式和匿名內部類有著明顯的差別迈螟。

匿名內部類實現

匿名內部類仍然是一個類叉抡,只是不需要程序員顯示指定類名,編譯器會自動為該類取名答毫。因此如果有如下形式的代碼褥民,編譯之后將會產生兩個class文件:

public class MainAnonymousClass {
    public static void main(String[] args) {
        new Thread(new Runnable(){
            @Override
            public void run(){
                System.out.println("Anonymous Class Thread run()");
            }
        }).start();;
    }
}

編譯之后文件分布如下,兩個class文件分別是主類和匿名內部類產生的:

2-AnonymousClass.png

進一步分析主類MainAnonymousClass.class的字節(jié)碼洗搂,可發(fā)現其創(chuàng)建了匿名內部類的對象:

// javap -c MainAnonymousClass.class
public class MainAnonymousClass {
  ...
  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/Thread
       3: dup
       4: new           #3                  // class MainAnonymousClass$1 /*創(chuàng)建內部類對象*/
       7: dup
       8: invokespecial #4                  // Method MainAnonymousClass$1."<init>":()V
      11: invokespecial #5                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      14: invokevirtual #6                  // Method java/lang/Thread.start:()V
      17: return
}

Lambda表達式實現

Lambda表達式通過invokedynamic指令實現消返,書寫Lambda表達式不會產生新的類**。如果有如下代碼耘拇,編譯之后只有一個class文件:

public class MainLambda {
    public static void main(String[] args) {
        new Thread(
                () -> System.out.println("Lambda Thread run()")
            ).start();;
    }
}

編譯之后的結果:

2-Lambda

通過javap反編譯命名撵颊,我們更能看出Lambda表達式內部表示的不同:

// javap -c -p MainLambda.class
public class MainLambda {
  ...
  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/Thread
       3: dup
       4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable; /*使用invokedynamic指令調用*/
       9: invokespecial #4                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      12: invokevirtual #5                  // Method java/lang/Thread.start:()V
      15: return

  private static void lambda$main$0();  /*Lambda表達式被封裝成主類的私有方法*/
    Code:
       0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #7                  // String Lambda Thread run()
       5: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

反編譯之后我們發(fā)現Lambda表達式被封裝成了主類的一個私有方法,并通過invokedynamic指令進行調用惫叛。

推論倡勇,this引用的意義

既然Lambda表達式不是內部類的簡寫,那么Lambda內部的this引用也就跟內部類對象沒什么關系了嘉涌。在Lambda表達式中this的意義跟在表達式外部完全一樣妻熊。因此下列代碼將輸出兩遍Hello Hoolee,而不是兩個引用地址仑最。

public class Hello {
    Runnable r1 = () -> { System.out.println(this); };
    Runnable r2 = () -> { System.out.println(toString()); };
    public static void main(String[] args) {
        new Hello().r1.run();
        new Hello().r2.run();
    }
    public String toString() { return "Hello Hoolee"; }
}

Lambda and Collections

我們先從最熟悉的Java集合框架(Java Collections Framework, JCF)開始說起扔役。

為引入Lambda表達式,Java8新增了java.util.funcion包警医,里面包含常用的函數接口亿胸,這是Lambda表達式的基礎,Java集合框架也新增部分接口预皇,以便與Lambda表達式對接损敷。

首先回顧一下Java集合框架的接口繼承結構:

JCF_Collection_Interfaces

上圖中綠色標注的接口類,表示在Java8中加入了新的接口方法深啤,當然由于繼承關系拗馒,他們相應的子類也都會繼承這些新方法。下表詳細列舉了這些方法溯街。

接口名 Java8新加入的方法
Collection removeIf() spliterator() stream() parallelStream() forEach()
List replaceAll() sort()
Map getOrDefault() forEach() replaceAll() putIfAbsent() remove() replace() computeIfAbsent() computeIfPresent() compute() merge()

這些新加入的方法大部分要用到java.util.function包下的接口诱桂,這意味著這些方法大部分都跟Lambda表達式相關。我們將逐一學習這些方法呈昔。

Collection中的新方法

如上所示挥等,接口CollectionList新加入了一些方法,我們以是List的子類ArrayList為例來說明堤尾。了解Java7ArrayList實現原理肝劲,將有助于理解下文。

forEach()

該方法的簽名為void forEach(Consumer<? super E> action),作用是對容器中的每個元素執(zhí)行action指定的動作辞槐,其中Consumer是個函數接口掷漱,里面只有一個待實現方法void accept(T t)(后面我們會看到,這個方法叫什么根本不重要榄檬,你甚至不需要記憶它的名字)卜范。

需求:假設有一個字符串列表,需要打印出其中所有長度大于3的字符串.

Java7及以前我們可以用增強的for循環(huán)實現:

// 使用曾強for循環(huán)迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(String str : list){
    if(str.length()>3)
        System.out.println(str);
}

現在使用forEach()方法結合匿名內部類鹿榜,可以這樣實現:

// 使用forEach()結合匿名內部類迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach(new Consumer<String>(){
    @Override
    public void accept(String str){
        if(str.length()>3)
            System.out.println(str);
    }
});

上述代碼調用forEach()方法海雪,并使用匿名內部類實現Comsumer接口。到目前為止我們沒看到這種設計有什么好處舱殿,但是不要忘記Lambda表達式奥裸,使用Lambda表達式實現如下:

// 使用forEach()結合Lambda表達式迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach( str -> {
        if(str.length()>3)
            System.out.println(str);
    });

上述代碼給forEach()方法傳入一個Lambda表達式,我們不需要知道accept()方法沪袭,也不需要知道Consumer接口湾宙,類型推導幫我們做了一切。

removeIf()

該方法簽名為boolean removeIf(Predicate<? super E> filter)枝恋,作用是刪除容器中所有滿足filter指定條件的元素创倔,其中Predicate是一個函數接口,里面只有一個待實現方法boolean test(T t)焚碌,同樣的這個方法的名字根本不重要畦攘,因為用的時候不需要書寫這個名字。

需求:假設有一個字符串列表十电,需要刪除其中所有長度大于3的字符串知押。

我們知道如果需要在迭代過程沖對容器進行刪除操作必須使用迭代器,否則會拋出ConcurrentModificationException鹃骂,所以上述任務傳統(tǒng)的寫法是:

// 使用迭代器刪除列表元素
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Iterator<String> it = list.iterator();
while(it.hasNext()){
    if(it.next().length()>3) // 刪除長度大于3的元素
        it.remove();
}

現在使用removeIf()方法結合匿名內部類台盯,我們可是這樣實現:

// 使用removeIf()結合匿名名內部類實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(new Predicate<String>(){ // 刪除長度大于3的元素
    @Override
    public boolean test(String str){
        return str.length()>3;
    }
});

上述代碼使用removeIf()方法,并使用匿名內部類實現Precicate接口畏线。相信你已經想到用Lambda表達式該怎么寫了:

// 使用removeIf()結合Lambda表達式實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(str -> str.length()>3); // 刪除長度大于3的元素

使用Lambda表達式不需要記憶Predicate接口名静盅,也不需要記憶test()方法名,只需要知道此處需要一個返回布爾類型的Lambda表達式就行了寝殴。

replaceAll()

該方法簽名為void replaceAll(UnaryOperator<E> operator)蒿叠,作用是對每個元素執(zhí)行operator指定的操作,并用操作結果來替換原來的元素蚣常。其中UnaryOperator是一個函數接口市咽,里面只有一個待實現函數T apply(T t)

需求:假設有一個字符串列表抵蚊,將其中所有長度大于3的元素轉換成大寫施绎,其余元素不變溯革。

Java7及之前似乎沒有優(yōu)雅的辦法:

// 使用下標實現元素替換
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(int i=0; i<list.size(); i++){
    String str = list.get(i);
    if(str.length()>3)
        list.set(i, str.toUpperCase());
}

使用replaceAll()方法結合匿名內部類可以實現如下:

// 使用匿名內部類實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(new UnaryOperator<String>(){
    @Override
    public String apply(String str){
        if(str.length()>3)
            return str.toUpperCase();
        return str;
    }
});

上述代碼調用replaceAll()方法,并使用匿名內部類實現UnaryOperator接口谷醉。我們知道可以用更為簡潔的Lambda表達式實現:

// 使用Lambda表達式實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(str -> {
    if(str.length()>3)
        return str.toUpperCase();
    return str;
});

sort()

該方法定義在List接口中致稀,方法簽名為void sort(Comparator<? super E> c),該方法根據c指定的比較規(guī)則對容器元素進行排序孤紧。Comparator接口我們并不陌生豺裆,其中有一個方法int compare(T o1, T o2)需要實現拒秘,顯然該接口是個函數接口号显。

需求:假設有一個字符串列表,按照字符串長度增序對元素排序躺酒。

由于Java7以及之前sort()方法在Collections工具類中押蚤,所以代碼要這樣寫:

// Collections.sort()方法
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Collections.sort(list, new Comparator<String>(){
    @Override
    public int compare(String str1, String str2){
        return str1.length()-str2.length();
    }
});

現在可以直接使用List.sort()方法,結合Lambda表達式羹应,可以這樣寫:

// List.sort()方法結合Lambda表達式
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length()-str2.length());

spliterator()

方法簽名為Spliterator<E> spliterator()揽碘,該方法返回容器的可拆分迭代器。從名字來看該方法跟iterator()方法有點像园匹,我們知道Iterator是用來迭代容器的雳刺,Spliterator也有類似作用,但二者有如下不同:

  1. Spliterator既可以像Iterator那樣逐個迭代裸违,也可以批量迭代掖桦。批量迭代可以降低迭代的開銷。
  2. Spliterator是可拆分的供汛,一個Spliterator可以通過調用Spliterator<T> trySplit()方法來嘗試分成兩個枪汪。一個是this,另一個是新返回的那個怔昨,這兩個迭代器代表的元素沒有重疊雀久。

可通過(多次)調用Spliterator.trySplit()方法來分解負載,以便多線程處理趁舀。

stream()和parallelStream()

stream()parallelStream()分別返回該容器的Stream視圖表示赖捌,不同之處在于parallelStream()返回并行的StreamStream是Java函數式編程的核心類矮烹,我們會在后面章節(jié)中學習越庇。

Map中的新方法

相比CollectionMap中加入了更多的方法擂送,我們以HashMap為例來逐一探秘悦荒。了解Java7HashMap實現原理,將有助于理解下文嘹吨。

forEach()

該方法簽名為void forEach(BiConsumer<? super K,? super V> action)搬味,作用是Map中的每個映射執(zhí)行action指定的操作,其中BiConsumer是一個函數接口,里面有一個待實現方法void accept(T t, U u)碰纬。BinConsumer接口名字和accept()方法名字都不重要萍聊,請不要記憶他們。

需求:假設有一個數字到對應英文單詞的Map悦析,請輸出Map中的所有映射關系.

Java7以及之前經典的代碼如下:

// Java7以及之前迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()){
    System.out.println(entry.getKey() + "=" + entry.getValue());
}

使用Map.forEach()方法寿桨,結合匿名內部類,代碼如下:

// 使用forEach()結合匿名內部類迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach(new BiConsumer<Integer, String>(){
    @Override
    public void accept(Integer k, String v){
        System.out.println(k + "=" + v);
    }
});

上述代碼調用forEach()方法强戴,并使用匿名內部類實現BiConsumer接口亭螟。當然,實際場景中沒人使用匿名內部類寫法骑歹,因為有Lambda表達式:

// 使用forEach()結合Lambda表達式迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));
}

getOrDefault()

該方法跟Lambda表達式沒關系预烙,但是很有用。方法簽名為V getOrDefault(Object key, V defaultValue)道媚,作用是按照給定的key查詢Map中對應的value扁掸,如果沒有找到則返回defaultValue。使用該方法程序員可以省去查詢指定鍵值是否存在的麻煩.

需求最域;假設有一個數字到對應英文單詞的Map谴分,輸出4對應的英文單詞,如果不存在則輸出NoValue

// 查詢Map中指定的值镀脂,不存在時使用默認值
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
// Java7以及之前做法
if(map.containsKey(4)){ // 1
    System.out.println(map.get(4));
}else{
    System.out.println("NoValue");
}
// Java8使用Map.getOrDefault()
System.out.println(map.getOrDefault(4, "NoValue")); // 2

putIfAbsent()

該方法跟Lambda表達式沒關系牺蹄,但是很有用。方法簽名為V putIfAbsent(K key, V value)狗热,作用是只有在不存在key值的映射或映射值為null钞馁,才將value指定的值放入到Map中,否則不對Map做更改.該方法將條件判斷和賦值合二為一匿刮,使用起來更加方便.

remove()

我們都知道Map中有一個remove(Object key)方法僧凰,來根據指定key值刪除Map中的映射關系;Java8新增了remove(Object key, Object value)方法熟丸,只有在當前Mapkey正好映射到value才刪除該映射训措,否則什么也不做.

replace()

在Java7及以前,要想替換Map中的映射關系可通過put(K key, V value)方法實現光羞,該方法總是會用新值替換原來的值.為了更精確的控制替換行為绩鸣,Java8在Map中加入了兩個replace()方法,分別如下:

  • replace(K key, V value)纱兑,只有在當前Mapkey的映射存在時才用value去替換原來的值呀闻,否則什么也不做.
  • replace(K key, V oldValue, V newValue),只有在當前Mapkey的映射存在且等于oldValue才用newValue去替換原來的值潜慎,否則什么也不做.

replaceAll()

該方法簽名為replaceAll(BiFunction<? super K,? super V,? extends V> function)捡多,作用是對Map中的每個映射執(zhí)行function指定的操作蓖康,并用function的執(zhí)行結果替換原來的value,其中BiFunction是一個函數接口垒手,里面有一個待實現方法R apply(T t, U u).不要被如此多的函數接口嚇到蒜焊,因為使用的時候根本不需要知道他們的名字.

需求:假設有一個數字到對應英文單詞的Map,請將原來映射關系中的單詞都轉換成大寫.

Java7以及之前經典的代碼如下:

// Java7以及之前替換所有Map中所有映射關系
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()){
    entry.setValue(entry.getValue().toUpperCase());
}

使用replaceAll()方法結合匿名內部類科贬,實現如下:

// 使用replaceAll()結合匿名內部類實現
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll(new BiFunction<Integer, String, String>(){
    @Override
    public String apply(Integer k, String v){
        return v.toUpperCase();
    }
});

上述代碼調用replaceAll()方法泳梆,并使用匿名內部類實現BiFunction接口。更進一步的榜掌,使用Lambda表達式實現如下:

// 使用replaceAll()結合Lambda表達式實現
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll((k, v) -> v.toUpperCase());

簡潔到讓人難以置信.

merge()

該方法簽名為merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)优妙,作用是:

  1. 如果Mapkey對應的映射不存在或者為null,則將value(不能是null)關聯(lián)到key上唐责;
  2. 否則執(zhí)行remappingFunction鳞溉,如果執(zhí)行結果非null則用該結果跟key關聯(lián)瘾带,否則在Map中刪除key的映射.

參數中BiFunction函數接口前面已經介紹過鼠哥,里面有一個待實現方法R apply(T t, U u)

merge()方法雖然語義有些復雜,但該方法的用方式很明確看政,一個比較常見的場景是將新的錯誤信息拼接到原來的信息上朴恳,比如:

map.merge(key, newMsg, (v1, v2) -> v1+v2);

compute()

該方法簽名為compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction),作用是把remappingFunction的計算結果關聯(lián)到key上允蚣,如果計算結果為null于颖,則在Map中刪除key的映射.

要實現上述merge()方法中錯誤信息拼接的例子,使用compute()代碼如下:

map.compute(key, (k,v) -> v==null ? newMsg : v.concat(newMsg));

computeIfAbsent()

該方法簽名為V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)嚷兔,作用是:只有在當前Map不存在key值的映射或映射值為null森渐,才調用mappingFunction,并在mappingFunction執(zhí)行結果非null時冒晰,將結果跟key關聯(lián).

Function是一個函數接口同衣,里面有一個待實現方法R apply(T t)

computeIfAbsent()常用來對Map的某個key值建立初始化映射.比如我們要實現一個多值映射,Map的定義可能是Map<K,Set<V>>壶运,要向Map中放入新值耐齐,可通過如下代碼實現:

Map<Integer, Set<String>> map = new HashMap<>();
// Java7及以前的實現方式
if(map.containsKey(1)){
    map.get(1).add("one");
}else{
    Set<String> valueSet = new HashSet<String>();
    valueSet.add("one");
    map.put(1, valueSet);
}
// Java8的實現方式
map.computeIfAbsent(1, v -> new HashSet<String>()).add("yi");

使用computeIfAbsent()將條件判斷和添加操作合二為一,使代碼更加簡潔.

computeIfPresent()

該方法簽名為V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)蒋情,作用跟computeIfAbsent()相反埠况,即,只有在當前Map存在key值的映射且非null棵癣,才調用remappingFunction辕翰,如果remappingFunction執(zhí)行結果為null,則刪除key的映射狈谊,否則使用該結果替換key原來的映射.

這個函數的功能跟如下代碼是等效的:

// Java7及以前跟computeIfPresent()等效的代碼
if (map.get(key) != null) {
    V oldValue = map.get(key);
    V newValue = remappingFunction.apply(key, oldValue);
    if (newValue != null)
        map.put(key, newValue);
    else
        map.remove(key);
    return newValue;
}
return null;

  1. Java8為容器新增一些有用的方法喜命,這些方法有些是為完善原有功能,有些是為引入函數式編程尝胆,學習和使用這些方法有助于我們寫出更加簡潔有效的代碼.
  2. 函數接口雖然很多,但絕大多數時候我們根本不需要知道它們的名字贪染,書寫Lambda表達式時類型推斷幫我們做了一切.

Streams API(I)

你可能沒意識到Java對函數式編程的重視程度杭隙,看看Java 8加入函數式編程擴充多少功能就清楚了。Java 8之所以費這么大功夫引入函數式編程铣耘,原因有二:

  1. 代碼簡潔函數式編程寫出的代碼簡潔且意圖明確,使用stream接口讓你從此告別for循環(huán)炉媒。
  2. 多核友好,Java函數式編程使得編寫并行程序從未如此簡單水援,你需要的全部就是調用一下parallel()方法蜗元。

這一節(jié)我們學習stream,也就是Java函數式編程的主角惯豆。對于Java 7來說stream完全是個陌生東西地熄,stream并不是某種數據結構,它只是數據源的一種視圖。這里的數據源可以是一個數組,Java容器或I/O channel等闽晦。正因如此要得到一個stream通常不會手動創(chuàng)建,而是調用對應的工具方法,比如:

  • 調用Collection.stream()或者Collection.parallelStream()方法
  • 調用Arrays.stream(T[] array)方法

常見的stream接口繼承關系如圖:

image

圖中4種stream接口繼承自BaseStream振亮,其中IntStream, LongStream, DoubleStream對應三種基本類型(int, long, double,注意不是包裝類型)褒搔,Stream對應所有剩余類型的stream視圖。為不同數據類型設置不同stream接口惧辈,可以1.提高性能困食,2.增加特定接口函數硕盹。

image

你可能會奇怪為什么不把IntStream等設計成Stream的子接口芒澜?畢竟這接口中的方法名大部分是一樣的南吮。答案是這些方法的名字雖然相同,但是返回類型不同涂邀,如果設計成父子接口關系,這些方法將不能共存浩聋,因為Java不允許只有返回類型不同的方法重載。

雖然大部分情況下stream是容器調用Collection.stream()方法得到的坊夫,但streamcollections有以下不同:

  • 無存儲stream不是一種數據結構拷邢,它只是某種數據源的一個視圖屎慢,數據源可以是一個數組欲虚,Java容器或I/O channel等复哆。
  • 為函數式編程而生。對stream的任何修改都不會修改背后的數據源腌零,比如對stream執(zhí)行過濾操作并不會刪除被過濾的元素梯找,而是會產生一個不包含被過濾元素的新stream
  • 惰式執(zhí)行益涧。stream上的操作并不會立即執(zhí)行久免,只有等到用戶真正需要結果的時候才會執(zhí)行呼巴。
  • 可消費性stream只能被“消費”一次,一旦遍歷過就會失效,就像容器的迭代器那樣,想要再次遍歷必須重新生成复隆。

stream的操作分為為兩類亏栈,中間操作(intermediate operations)和結束操作(terminal operations)**闷游,二者特點是:

  1. 中間操作總是會惰式執(zhí)行,調用中間操作只會生成一個標記了該操作的新stream,僅此而已。
  2. 結束操作會觸發(fā)實際計算,計算發(fā)生時會把所有中間操作積攢的操作以pipeline的方式執(zhí)行,這樣可以減少迭代次數。計算完成之后stream就會失效。

如果你熟悉Apache Spark RDD,對stream的這個特點應該不陌生。

下表匯總了Stream接口的部分常見方法:

操作類型 接口方法
中間操作 concat() distinct() filter() flatMap() limit() map() peek()
skip() sorted() parallel() sequential() unordered()
結束操作 allMatch() anyMatch() collect() count() findAny() findFirst()
forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()

區(qū)分中間操作和結束操作最簡單的方法猪瞬,就是看方法的返回值锅风,返回值為stream的大都是中間操作边器,否則是結束操作。

stream方法使用

stream跟函數接口關系非常緊密,沒有函數接口stream就無法工作洋幻。回顧一下:函數接口是指內部只有一個抽象方法的接口蜕提。通常函數接口出現的地方都可以使用Lambda表達式,所以不必記憶函數接口的名字靶端。

forEach()

我們對forEach()方法并不陌生谎势,在Collection中我們已經見過。方法簽名為void forEach(Consumer<? super E> action)杨名,作用是對容器中的每個元素執(zhí)行action指定的動作脏榆,也就是對元素進行遍歷。

// 使用Stream.forEach()迭代
Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.forEach(str -> System.out.println(str));

由于forEach()是結束方法,上述代碼會立即執(zhí)行,輸出所有字符串级野。

filter()

image

函數原型為Stream<T> filter(Predicate<? super T> predicate),作用是返回一個只包含滿足predicate條件元素的Stream瞎领。

// 保留長度等于3的字符串
Stream<String> stream= Stream.of("I", "love", "you", "too");
stream.filter(str -> str.length()==3)
    .forEach(str -> System.out.println(str));

上述代碼將輸出為長度等于3的字符串youtoo泌辫。注意,由于filter()是個中間操作默刚,如果只調用filter()不會有實際計算甥郑,因此也不會輸出任何信息。

distinct()

image

函數原型為Stream<T> distinct()荤西,作用是返回一個去除重復元素之后的Stream澜搅。

Stream<String> stream= Stream.of("I", "love", "you", "too", "too");
stream.distinct()
    .forEach(str -> System.out.println(str));

上述代碼會輸出去掉一個too之后的其余字符串。

sorted()

排序函數有兩個邪锌,一個是用自然順序排序勉躺,一個是使用自定義比較器排序,函數原型分別為Stream<T> sorted()Stream<T> sorted(Comparator<? super T> comparator)觅丰。

Stream<String> stream= Stream.of("I", "love", "you", "too");
stream.sorted((str1, str2) -> str1.length()-str2.length())
    .forEach(str -> System.out.println(str));

上述代碼將輸出按照長度升序排序后的字符串饵溅,結果完全在預料之中。

map()

image

函數原型為<R> Stream<R> map(Function<? super T,? extends R> mapper)妇萄,作用是返回一個對當前所有元素執(zhí)行執(zhí)行mapper之后的結果組成的Stream蜕企。直觀的說,就是對每個元素按照某種操作進行轉換冠句,轉換前后Stream中元素的個數不會改變轻掩,但元素的類型取決于轉換之后的類型。

Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.map(str -> str.toUpperCase())
    .forEach(str -> System.out.println(str));

上述代碼將輸出原字符串的大寫形式懦底。

flatMap()

image

函數原型為<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)唇牧,作用是對每個元素執(zhí)行mapper指定的操作,并用所有mapper返回的Stream中的元素組成一個新的Stream作為最終返回結果聚唐。說起來太拗口丐重,通俗的講flatMap()的作用就相當于把原stream中的所有元素都”攤平”之后組成的Stream,轉換前后元素的個數和類型都可能會改變杆查。

Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
stream.flatMap(list -> list.stream())
    .forEach(i -> System.out.println(i));

上述代碼中扮惦,原來的stream中有兩個元素,分別是兩個List<Integer>亲桦,執(zhí)行flatMap()之后径缅,將每個List都“攤平”成了一個個的數字掺栅,所以會新產生一個由5個數字組成的Stream。所以最終將輸出1~5這5個數字纳猪。

截止到目前我們感覺良好,已介紹Stream接口函數理解起來并不費勁兒桃笙。如果你就此以為函數式編程不過如此氏堤,恐怕是高興地太早了。下一節(jié)對Stream規(guī)約操作的介紹將刷新你現在的認識搏明。

Streams API(II)

上一節(jié)介紹了部分Stream常見接口方法鼠锈,理解起來并不困難,但Stream的用法不止于此星著,本節(jié)我們將仍然以Stream為例购笆,介紹流的規(guī)約操作。

規(guī)約操作(reduction operation)又被稱作折疊操作(fold)虚循,是通過某個連接動作將所有元素匯總成一個匯總結果的過程同欠。元素求和、求最大值或最小值横缔、求出元素總個數铺遂、將所有元素轉換成一個列表或集合,都屬于規(guī)約操作茎刚。Stream類庫有兩個通用的規(guī)約操作reduce()collect()襟锐,也有一些為簡化書寫而設計的專用規(guī)約操作,比如sum()膛锭、max()粮坞、min()count()等初狰。

最大或最小值這類規(guī)約操作很好理解(至少方法語義上是這樣)莫杈,我們著重介紹reduce()collect(),這是比較有魔法的地方跷究。

多面手reduce()

reduce操作可以實現從一組元素中生成一個值姓迅,sum()max()俊马、min()丁存、count()等都是reduce操作,將他們單獨設為函數只是因為常用柴我。reduce()的方法定義有三種重寫形式:

  • Optional<T> reduce(BinaryOperator<T> accumulator)
  • T reduce(T identity, BinaryOperator<T> accumulator)
  • <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

雖然函數定義越來越長解寝,但語義不曾改變,多的參數只是為了指明初始值(參數identity)艘儒,或者是指定并行執(zhí)行時多個部分結果的合并方式(參數combiner)聋伦。reduce()最常用的場景就是從一堆值中生成一個值夫偶。用這么復雜的函數去求一個最大或最小值,你是不是覺得設計者有病觉增。其實不然兵拢,因為“大”和“小”或者“求和”有時會有不同的語義。

需求:從一組單詞中找出最長的單詞逾礁。這里“大”的含義就是“長”说铃。

// 找出最長的單詞
Stream<String> stream = Stream.of("I", "love", "you", "too");
Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
//Optional<String> longest = stream.max((s1, s2) -> s1.length()-s2.length());
System.out.println(longest.get());

上述代碼會選出最長的單詞love,其中Optional是(一個)值的容器嘹履,使用它可以避免null值的麻煩腻扇。當然可以使用Stream.max(Comparator<? super T> comparator)方法來達到同等效果,但reduce()自有其存在的理由砾嫉。

image

需求:求出一組單詞的長度之和幼苛。這是個“求和”操作,操作對象輸入類型是String焕刮,而結果類型是Integer舶沿。

// 求單詞長度之和
Stream<String> stream = Stream.of("I", "love", "you", "too");
Integer lengthSum = stream.reduce(0, // 初始值 // (1)
        (sum, str) -> sum+str.length(), // 累加器 // (2)
        (a, b) -> a+b); // 部分和拼接器,并行執(zhí)行時才會用到 // (3)
// int lengthSum = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);

上述代碼標號(2)處將i. 字符串映射成長度济锄,ii. 并和當前累加和相加暑椰。這顯然是兩步操作,使用reduce()函數將這兩步合二為一荐绝,更有助于提升性能一汽。如果想要使用map()sum()組合來達到上述目的,也是可以的低滩。

reduce()擅長的是生成一個值召夹,如果想要從Stream生成一個集合或者Map等復雜的對象該怎么辦呢?終極武器collect()橫空出世恕沫!

終極武器collect()

不夸張的講监憎,如果你發(fā)現某個功能在Stream接口中沒找到,十有八九可以通過collect()方法實現婶溯。collect()Stream接口方法中最靈活的一個鲸阔,學會它才算真正入門Java函數式編程。先看幾個熱身的小例子:

// 將Stream轉換成容器或Map
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
// Set<String> set = stream.collect(Collectors.toSet()); // (2)
// Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // (3)

上述代碼分別列舉了如何將Stream轉換成List迄委、SetMap褐筛。雖然代碼語義很明確,可是我們仍然會有幾個疑問:

  1. Function.identity()是干什么的叙身?
  2. String::length是什么意思渔扎?
  3. Collectors是個什么東西?

接口的靜態(tài)方法和默認方法

Function是一個接口信轿,那么Function.identity()是什么意思呢晃痴?這要從兩方面解釋:

  1. Java 8允許在接口中加入具體方法残吩。接口中的具體方法有兩種,default方法和static方法倘核,identity()就是Function接口的一個靜態(tài)方法泣侮。
  2. Function.identity()返回一個輸出跟輸入一樣的Lambda表達式對象,等價于形如t -> t形式的Lambda表達式笤虫。

上面的解釋是不是讓你疑問更多旁瘫?不要問我為什么接口中可以有具體方法,也不要告訴我你覺得t -> tidentity()方法更直觀琼蚯。我會告訴你接口中的default方法是一個無奈之舉,在Java 7及之前要想在定義好的接口中加入新的抽象方法是很困難甚至不可能的惠况,因為所有實現了該接口的類都要重新實現遭庶。試想在Collection接口中加入一個stream()抽象方法會怎樣?default方法就是用來解決這個尷尬問題的稠屠,直接在接口中實現新加入的方法峦睡。既然已經引入了default方法,為何不再加入static方法來避免專門的工具類呢权埠!

方法引用

諸如String::length的語法形式叫做方法引用(method references)榨了,這種語法用來替代某些特定形式Lambda表達式。如果Lambda表達式的全部內容就是調用一個已有的方法攘蔽,那么可以用方法引用來替代Lambda表達式龙屉。方法引用可以細分為四類:

方法引用類別 舉例
引用靜態(tài)方法 Integer::sum
引用某個對象的方法 list::add
引用某個類的方法 String::length
引用構造方法 HashMap::new

我們會在后面的例子中使用方法引用。

收集器

相信前面繁瑣的內容已徹底打消了你學習Java函數式編程的熱情满俗,不過很遺憾转捕,下面的內容更繁瑣。但這不能怪Stream類庫唆垃,因為要實現的功能本身很復雜五芝。

image

收集器(Collector)是為Stream.collect()方法量身打造的工具接口(類)≡颍考慮一下將一個Stream轉換成一個容器(或者Map)需要做哪些工作枢步?我們至少需要兩樣東西:

  1. 目標容器是什么?是ArrayList還是HashSet渐尿,或者是個TreeMap醉途。
  2. 新元素如何添加到容器中?是List.add()還是Map.put()涡戳。

如果并行的進行規(guī)約结蟋,還需要告訴collect() 3. 多個部分結果如何合并成一個。

結合以上分析渔彰,collect()方法定義為<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)嵌屎,三個參數依次對應上述三條分析推正。不過每次調用collect()都要傳入這三個參數太麻煩,收集器Collector就是對這三個參數的簡單封裝,所以collect()的另一定義為<R,A> R collect(Collector<? super T,A,R> collector)宝惰。Collectors工具類可通過靜態(tài)方法生成各種常用的Collector植榕。舉例來說,如果要將Stream規(guī)約成List可以通過如下兩種方式實現:

// 將Stream規(guī)約成List
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
//List<String> list = stream.collect(Collectors.toList());// 方式2
System.out.println(list);

通常情況下我們不需要手動指定collect()的三個參數尼夺,而是調用collect(Collector<? super T,A,R> collector)方法尊残,并且參數中的Collector對象大都是直接通過Collectors工具類獲得。實際上傳入的收集器的行為決定了collect()的行為淤堵。

使用collect()生成Collection

前面已經提到通過collect()方法將Stream轉換成容器的方法寝衫,這里再匯總一下。將Stream轉換成ListSet是比較常見的操作拐邪,所以Collectors工具已經為我們提供了對應的收集器慰毅,通過如下代碼即可完成:

// 將Stream轉換成List或Set
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
Set<String> set = stream.collect(Collectors.toSet()); // (2)

上述代碼能夠滿足大部分需求,但由于返回結果是接口類型扎阶,我們并不知道類庫實際選擇的容器類型是什么汹胃,有時候我們可能會想要人為指定容器的實際類型,這個需求可通過Collectors.toCollection(Supplier<C> collectionFactory)方法完成东臀。

// 使用toCollection()指定規(guī)約容器的類型
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)

上述代碼(3)處指定規(guī)約結果是ArrayList着饥,而(4)處指定規(guī)約結果為HashSet。一切如你所愿惰赋。

使用collect()生成Map

前面已經說過Stream背后依賴于某種數據源宰掉,數據源可以是數組、容器等谤逼,但不能是Map贵扰。反過來從Stream生成Map是可以的,但我們要想清楚Mapkeyvalue分別代表什么流部,根本原因是我們要想清楚要干什么戚绕。通常在三種情況下collect()的結果會是Map

  1. 使用Collectors.toMap()生成的收集器,用戶需要指定如何生成Mapkeyvalue枝冀。
  2. 使用Collectors.partitioningBy()生成的收集器舞丛,對元素進行二分區(qū)操作時用到。
  3. 使用Collectors.groupingBy()生成的收集器果漾,對元素做group操作時用到球切。

情況1:使用toMap()生成的收集器,這種情況是最直接的绒障,前面例子中已提到吨凑,這是和Collectors.toCollection()并列的方法。如下代碼展示將學生列表轉換成由<學生,GPA>組成的Map鸵钝。非常直觀糙臼,無需多言。

// 使用toMap()統(tǒng)計學生GPA
Map<Student, Double> studentToGPA =
     students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成key
                                     student -> computeGPA(student)));// 如何生成value

情況2:使用partitioningBy()生成的收集器恩商,這種情況適用于將Stream中的元素依據某個二值邏輯(滿足條件变逃,或不滿足)分成互補相交的兩部分,比如男女性別怠堪、成績及格與否等揽乱。下列代碼展示將學生分成成績及格或不及格的兩部分。

// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing = students.stream()
         .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));

情況3:使用groupingBy()生成的收集器粟矿,這是比較靈活的一種情況凰棉。跟SQL中的group by語句類似,這里的groupingBy()也是按照某個屬性對數據進行分組陌粹,屬性相同的元素會被對應到Map的同一個key上渊啰。下列代碼展示將員工按照部門進行分組:

// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::getDepartment));

以上只是分組的最基本用法,有些時候僅僅分組是不夠的申屹。在SQL中使用group by是為了協(xié)助其他查詢,比如1. 先將員工按照部門分組隧膏,2. 然后統(tǒng)計每個部門員工的人數哗讥。Java類庫設計者也考慮到了這種情況,增強版的groupingBy()能夠滿足這種需求胞枕。增強版的groupingBy()允許我們對元素分組之后再執(zhí)行某種運算杆煞,比如求和、計數腐泻、平均值决乎、類型轉換等。這種先將元素分組的收集器叫做上游收集器派桩,之后執(zhí)行其他運算的收集器叫做下游收集器(downstream Collector)构诚。

// 使用下游收集器統(tǒng)計每個部門的人數
Map<Department, Integer> totalByDept = employees.stream()
                    .collect(Collectors.groupingBy(Employee::getDepartment,
                                                   Collectors.counting()));// 下游收集器

上面代碼的邏輯是不是越看越像SQL?高度非結構化铆惑。還有更狠的范嘱,下游收集器還可以包含更下游的收集器,這絕不是為了炫技而增加的把戲员魏,而是實際場景需要丑蛤。考慮將員工按照部門分組的場景撕阎,如果我們想得到每個員工的名字(字符串)受裹,而不是一個個Employee對象,可通過如下方式做到:

// 按照部門對員工分布組虏束,并只保留員工的名字
Map<Department, List<String>> byDept = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment,
                        Collectors.mapping(Employee::getName,// 下游收集器
                                Collectors.toList())));// 更下游的收集器

如果看到這里你還沒有對Java函數式編程失去信心棉饶,恭喜你厦章,你已經順利成為Java函數式編程大師了。

使用collect()做字符串join

這個肯定是大家喜聞樂見的功能砰盐,字符串拼接時使用Collectors.joining()生成的收集器闷袒,從此告別for循環(huán)。Collectors.joining()方法有三種重寫形式岩梳,分別對應三種不同的拼接方式囊骤。無需多言,代碼過目難忘冀值。

// 使用Collectors.joining()拼接字符串
Stream<String> stream = Stream.of("I", "love", "you");
//String joined = stream.collect(Collectors.joining());// "Iloveyou"
//String joined = stream.collect(Collectors.joining(","));// "I,love,you"
String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}"

collect()還可以做更多

除了可以使用Collectors工具類已經封裝好的收集器也物,我們還可以自定義收集器,或者直接調用collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)方法列疗,收集任何形式你想要的信息滑蚯。不過Collectors工具類應該能滿足我們的絕大部分需求,手動實現之間請先看看文檔抵栈。

Stream Pipelines

前面我們已經學會如何使用Stream API告材,用起來真的很爽,但簡潔的方法下面似乎隱藏著無盡的秘密古劲,如此強大的API是如何實現的呢斥赋?比如Pipeline是怎么執(zhí)行的满葛,每次方法調用都會導致一次迭代嗎黄虱?自動并行又是怎么做到的,線程個數是多少阳似?本節(jié)我們學習Stream流水線的原理闷堡,這是Stream實現的關鍵所在隘膘。

首先回顧一下容器執(zhí)行Lambda表達式的方式,以ArrayList.forEach()方法為例杠览,具體代碼如下:

// ArrayList.forEach()
public void forEach(Consumer<? super E> action) {
    ...
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);// 回調方法
    }
    ...
}

我們看到ArrayList.forEach()方法的主要邏輯就是一個for循環(huán)弯菊,在該for循環(huán)里不斷調用action.accept()回調方法完成對元素的遍歷。這完全沒有什么新奇之處倦零,回調方法在Java GUI的監(jiān)聽器中廣泛使用误续。Lambda表達式的作用就是相當于一個回調方法,這很好理解扫茅。

Stream API中大量使用Lambda表達式作為回調方法蹋嵌,但這并不是關鍵。理解Stream我們更關心的是另外兩個問題:流水線和自動并行葫隙。使用Stream或許很容易寫入如下形式的代碼:

int longestStringLengthStartingWithA
        = strings.stream()
              .filter(s -> s.startsWith("A"))
              .mapToInt(String::length)
              .max();

上述代碼求出以字母A開頭的字符串的最大長度栽烂,一種直白的方式是為每一次函數調用都執(zhí)一次迭代,這樣做能夠實現功能,但效率上肯定是無法接受的腺办。類庫的實現著使用流水線(Pipeline)的方式巧妙的避免了多次迭代焰手,其基本思想是在一次迭代中盡可能多的執(zhí)行用戶指定的操作。為講解方便我們匯總了Stream的所有操作怀喉。

| Stream操作分類 |
| 中間操作(Intermediate operations) | 無狀態(tài)(Stateless) | unordered() filter() map() mapToInt() mapToLong() mapToDouble() flatMap() flatMapToInt() flatMapToLong() flatMapToDouble() peek() |
| 有狀態(tài)(Stateful) | distinct() sorted() sorted() limit() skip() |
| 結束操作(Terminal operations) | 非短路操作 | forEach() forEachOrdered() toArray() reduce() collect() max() min() count() |
| 短路操作(short-circuiting) | anyMatch() allMatch() noneMatch() findFirst() findAny() |

Stream上的所有操作分為兩類:中間操作和結束操作书妻,中間操作只是一種標記,只有結束操作才會觸發(fā)實際計算躬拢。中間操作又可以分為無狀態(tài)的(Stateless)和有狀態(tài)的(Stateful)躲履,無狀態(tài)中間操作是指元素的處理不受前面元素的影響,而有狀態(tài)的中間操作必須等到所有元素處理之后才知道最終結果聊闯,比如排序是有狀態(tài)操作工猜,在讀取所有元素之前并不能確定排序結果;結束操作又可以分為短路操作和非短路操作菱蔬,短路操作是指不用處理全部元素就可以返回結果篷帅,比如找到第一個滿足條件的元素。之所以要進行如此精細的劃分拴泌,是因為底層對每一種情況的處理方式不同魏身。

一種直白的實現方式

image

仍然考慮上述求最長字符串的程序,一種直白的流水線實現方式是為每一次函數調用都執(zhí)一次迭代蚪腐,并將處理中間結果放到某種數據結構中(比如數組叠骑,容器等)。具體說來削茁,就是調用filter()方法后立即執(zhí)行,選出所有以A開頭的字符串并放到一個列表list1中掉房,之后讓list1傳遞給mapToInt()方法并立即執(zhí)行茧跋,生成的結果放到list2中,最后遍歷list2找出最大的數字作為最終結果卓囚。程序的執(zhí)行流程如如所示:

這樣做實現起來非常簡單直觀瘾杭,但有兩個明顯的弊端:

  1. 迭代次數多。迭代次數跟函數調用的次數相等哪亿。
  2. 頻繁產生中間結果粥烁。每次函數調用都產生一次中間結果,存儲開銷無法接受蝇棉。

這些弊端使得效率底下讨阻,根本無法接受。如果不使用Stream API我們都知道上述代碼該如何在一次迭代中完成篡殷,大致是如下形式:

int longest = 0;
for(String str : strings){
    if(str.startsWith("A")){// 1\. filter(), 保留以A開頭的字符串
        int len = str.length();// 2\. mapToInt(), 轉換成長度
        longest = Math.max(len, longest);// 3\. max(), 保留最長的長度
    }
}

采用這種方式我們不但減少了迭代次數钝吮,也避免了存儲中間結果,顯然這就是流水線,因為我們把三個操作放在了一次迭代當中奇瘦。只要我們事先知道用戶意圖棘催,總是能夠采用上述方式實現跟Stream API等價的功能,但問題是Stream類庫的設計者并不知道用戶的意圖是什么耳标。如何在無法假設用戶行為的前提下實現流水線醇坝,是類庫的設計者要考慮的問題。

Stream流水線解決方案

我們大致能夠想到次坡,應該采用某種方式記錄用戶每一步的操作呼猪,當用戶調用結束操作時將之前記錄的操作疊加到一起在一次迭代中全部執(zhí)行掉。沿著這個思路贸毕,有幾個問題需要解決:

  1. 用戶的操作如何記錄郑叠?
  2. 操作如何疊加?
  3. 疊加之后的操作如何執(zhí)行明棍?
  4. 執(zhí)行后的結果(如果有)在哪里乡革?

操作如何記錄?

image

注意這里使用的是“操作(operation)”一詞摊腋,指的是“Stream中間操作”的操作沸版,很多Stream操作會需要一個回調函數(Lambda表達式),因此一個完整的操作是<數據來源兴蒸,操作视粮,回調函數>構成的三元組。Stream中使用Stage的概念來描述一個完整的操作橙凳,并用某種實例化后的PipelineHelper來代表Stage蕾殴,將具有先后順序的各個Stage連到一起,就構成了整個流水線岛啸。跟Stream相關類和接口的繼承關系圖示钓觉。

還有IntPipeline, LongPipeline, DoublePipeline沒在圖中畫出,這三個類專門為三種基本類型(不是包裝類型)而定制的坚踩,跟ReferencePipeline是并列關系荡灾。圖中Head用于表示第一個Stage,即調用調用諸如Collection.stream()方法產生的Stage瞬铸,很顯然這個Stage里不包含任何操作批幌;StatelessOpStatefulOp分別表示無狀態(tài)和有狀態(tài)的Stage,對應于無狀態(tài)和有狀態(tài)的中間操作嗓节。

Stream流水線組織結構示意圖如下:

image

圖中通過Collection.stream()方法得到Head也就是stage0荧缘,緊接著調用一系列的中間操作,不斷產生新的Stream拦宣。這些Stream對象以雙向鏈表的形式組織在一起胜宇,構成整個流水線耀怜,由于每個Stage都記錄了前一個Stage和本次的操作以及回調函數,依靠這種結構就能建立起對數據源的所有操作桐愉。這就是Stream記錄操作的方式财破。

操作如何疊加?

以上只是解決了操作記錄的問題从诲,要想讓流水線起到應有的作用我們需要一種將所有操作疊加到一起的方案左痢。你可能會覺得這很簡單,只需要從流水線的head開始依次執(zhí)行每一步的操作(包括回調函數)就行了系洛。這聽起來似乎是可行的俊性,但是你忽略了前面的Stage并不知道后面Stage到底執(zhí)行了哪種操作,以及回調函數是哪種形式描扯。換句話說定页,只有當前Stage本身才知道該如何執(zhí)行自己包含的動作。這就需要有某種協(xié)議來協(xié)調相鄰Stage之間的調用關系绽诚。

這種協(xié)議由Sink接口完成典徊,Sink接口包含的方法如下表所示:

| 方法名 | 作用 |
| void begin(long size) | 開始遍歷元素之前調用該方法,通知Sink做好準備恩够。 |
| void end() | 所有元素遍歷完成之后調用卒落,通知Sink沒有更多的元素了。 |
| boolean cancellationRequested() | 是否可以結束操作蜂桶,可以讓短路操作盡早結束儡毕。 |
| void accept(T t) | 遍歷元素時調用,接受一個待處理元素扑媚,并對元素進行處理腰湾。Stage把自己包含的操作和回調方法封裝到該方法里,前一個Stage只需要調用當前Stage.accept(T t)方法就行了疆股。 |

有了上面的協(xié)議檐盟,相鄰Stage之間調用就很方便了,每個Stage都會將自己的操作封裝到一個Sink里押桃,前一個Stage只需調用后一個Stage的accept()方法即可,并不需要知道其內部是如何處理的导犹。當然對于有狀態(tài)的操作唱凯,Sink的begin()end()方法也是必須實現的。比如Stream.sorted()是一個有狀態(tài)的中間操作谎痢,其對應的Sink.begin()方法可能創(chuàng)建一個乘放結果的容器磕昼,而accept()方法負責將元素添加到該容器,最后end()負責對容器進行排序节猿。對于短路操作票从,Sink.cancellationRequested()也是必須實現的漫雕,比如Stream.findFirst()是短路操作,只要找到一個元素峰鄙,cancellationRequested()就應該返回true浸间,以便調用者盡快結束查找。Sink的四個接口方法常常相互協(xié)作吟榴,共同完成計算任務魁蒜。實際上Stream API內部實現的的本質,就是如何重載Sink的這四個接口方法吩翻。

有了Sink對操作的包裝兜看,Stage之間的調用問題就解決了,執(zhí)行時只需要從流水線的head開始對數據源依次調用每個Stage對應的Sink.{begin(), accept(), cancellationRequested(), end()}方法就可以了狭瞎。一種可能的Sink.accept()方法流程是這樣的:

void accept(U u){
    1. 使用當前Sink包裝的回調函數處理u
    2. 將處理結果傳遞給流水線下游的Sink
}

Sink接口的其他幾個方法也是按照這種[處理->轉發(fā)]的模型實現细移。下面我們結合具體例子看看Stream的中間操作是如何將自身的操作包裝成Sink以及Sink是如何將處理結果轉發(fā)給下一個Sink的。先看Stream.map()方法:

// Stream.map()熊锭,調用該方法將產生一個新的Stream
public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
    ...
    return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                 StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
        @Override /*opWripSink()方法返回由回調函數包裝而成Sink*/
        Sink<P_OUT> opWrapSink(int flags, Sink<R> downstream) {
            return new Sink.ChainedReference<P_OUT, R>(downstream) {
                @Override
                public void accept(P_OUT u) {
                    R r = mapper.apply(u);// 1\. 使用當前Sink包裝的回調函數mapper處理u
                    downstream.accept(r);// 2\. 將處理結果傳遞給流水線下游的Sink
                }
            };
        }
    };
}

上述代碼看似復雜弧轧,其實邏輯很簡單,就是將回調函數mapper包裝到一個Sink當中球涛。由于Stream.map()是一個無狀態(tài)的中間操作劣针,所以map()方法返回了一個StatelessOp內部類對象(一個新的Stream),調用這個新Stream的opWripSink()方法將得到一個包裝了當前回調函數的Sink亿扁。

再來看一個復雜一點的例子捺典。Stream.sorted()方法將對Stream中的元素進行排序,顯然這是一個有狀態(tài)的中間操作从祝,因為讀取所有元素之前是沒法得到最終順序的襟己。拋開模板代碼直接進入問題本質,sorted()方法是如何將操作封裝成Sink的呢牍陌?sorted()一種可能封裝的Sink代碼如下:

// Stream.sort()方法用到的Sink實現
class RefSortingSink<T> extends AbstractRefSortingSink<T> {
    private ArrayList<T> list;// 存放用于排序的元素
    RefSortingSink(Sink<? super T> downstream, Comparator<? super T> comparator) {
        super(downstream, comparator);
    }
    @Override
    public void begin(long size) {
        ...
        // 創(chuàng)建一個存放排序元素的列表
        list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
    }
    @Override
    public void end() {
        list.sort(comparator);// 只有元素全部接收之后才能開始排序
        downstream.begin(list.size());
        if (!cancellationWasRequested) {// 下游Sink不包含短路操作
            list.forEach(downstream::accept);// 2\. 將處理結果傳遞給流水線下游的Sink
        }
        else {// 下游Sink包含短路操作
            for (T t : list) {// 每次都調用cancellationRequested()詢問是否可以結束處理擎浴。
                if (downstream.cancellationRequested()) break;
                downstream.accept(t);// 2\. 將處理結果傳遞給流水線下游的Sink
            }
        }
        downstream.end();
        list = null;
    }
    @Override
    public void accept(T t) {
        list.add(t);// 1\. 使用當前Sink包裝動作處理t,只是簡單的將元素添加到中間列表當中
    }
}

上述代碼完美的展現了Sink的四個接口方法是如何協(xié)同工作的:

  1. 首先beging()方法告訴Sink參與排序的元素個數毒涧,方便確定中間結果容器的的大兄ぁ;
  2. 之后通過accept()方法將元素添加到中間結果當中契讲,最終執(zhí)行時調用者會不斷調用該方法仿吞,直到遍歷所有元素;
  3. 最后end()方法告訴Sink所有元素遍歷完畢捡偏,啟動排序步驟唤冈,排序完成后將結果傳遞給下游的Sink;
  4. 如果下游的Sink是短路操作银伟,將結果傳遞給下游時不斷詢問下游cancellationRequested()是否可以結束處理你虹。

疊加之后的操作如何執(zhí)行绘搞?

image

Sink完美封裝了Stream每一步操作,并給出了[處理->轉發(fā)]的模式來疊加操作傅物。這一連串的齒輪已經咬合夯辖,就差最后一步撥動齒輪啟動執(zhí)行。是什么啟動這一連串的操作呢挟伙?也許你已經想到了啟動的原始動力就是結束操作(Terminal Operation)楼雹,一旦調用某個結束操作,就會觸發(fā)整個流水線的執(zhí)行尖阔。

結束操作之后不能再有別的操作贮缅,所以結束操作不會創(chuàng)建新的流水線階段(Stage),直觀的說就是流水線的鏈表不會在往后延伸了介却。結束操作會創(chuàng)建一個包裝了自己操作的Sink谴供,這也是流水線中最后一個Sink,這個Sink只需要處理數據而不需要將結果傳遞給下游的Sink(因為沒有下游)齿坷。對于Sink的[處理->轉發(fā)]模型桂肌,結束操作的Sink就是調用鏈的出口。

我們再來考察一下上游的Sink是如何找到下游Sink的永淌。一種可選的方案是在PipelineHelper中設置一個Sink字段崎场,在流水線中找到下游Stage并訪問Sink字段即可。但Stream類庫的設計者沒有這么做遂蛀,而是設置了一個Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)方法來得到Sink谭跨,該方法的作用是返回一個新的包含了當前Stage代表的操作以及能夠將結果傳遞給downstream的Sink對象。為什么要產生一個新對象而不是返回一個Sink字段李滴?這是因為使用opWrapSink()可以將當前操作與下游Sink(上文中的downstream參數)結合成新Sink螃宙。試想只要從流水線的最后一個Stage開始,不斷調用上一個Stage的opWrapSink()方法直到最開始(不包括stage0所坯,因為stage0代表數據源谆扎,不包含操作),就可以得到一個代表了流水線上所有操作的Sink芹助,用代碼表示就是這樣:

// AbstractPipeline.wrapSink()
// 從下游向上游不斷包裝Sink堂湖。如果最初傳入的sink代表結束操作,
// 函數返回時就可以得到一個代表了流水線上所有操作的Sink状土。
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
    ...
    for (AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
        sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
    }
    return (Sink<P_IN>) sink;
}

現在流水線上從開始到結束的所有的操作都被包裝到了一個Sink里无蜂,執(zhí)行這個Sink就相當于執(zhí)行整個流水線,執(zhí)行Sink的代碼如下:

// AbstractPipeline.copyInto(), 對spliterator代表的數據執(zhí)行wrappedSink代表的操作声诸。
final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
    ...
    if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
        wrappedSink.begin(spliterator.getExactSizeIfKnown());// 通知開始遍歷
        spliterator.forEachRemaining(wrappedSink);// 迭代
        wrappedSink.end();// 通知遍歷結束
    }
    ...
}

上述代碼首先調用wrappedSink.begin()方法告訴Sink數據即將到來,然后調用spliterator.forEachRemaining()方法對數據進行迭代(Spliterator是容器的一種迭代器退盯,參閱)彼乌,最后調用wrappedSink.end()方法通知Sink數據處理結束泻肯。邏輯如此清晰。

執(zhí)行后的結果在哪里慰照?

最后一個問題是流水線上所有操作都執(zhí)行后灶挟,用戶所需要的結果(如果有)在哪里?首先要說明的是不是所有的Stream結束操作都需要返回結果毒租,有些操作只是為了使用其副作用(Side-effects)稚铣,比如使用Stream.forEach()方法將結果打印出來就是常見的使用副作用的場景(事實上,除了打印之外其他場景都應避免使用副作用)墅垮,對于真正需要返回結果的結束操作結果存在哪里呢惕医?

特別說明:副作用不應該被濫用,也許你會覺得在Stream.forEach()里進行元素收集是個不錯的選擇算色,就像下面代碼中那樣抬伺,但遺憾的是這樣使用的正確性和效率都無法保證,因為Stream可能會并行執(zhí)行灾梦。大多數使用副作用的地方都可以使用歸約操作更安全和有效的完成峡钓。

// 錯誤的收集方式
ArrayList<String> results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches())
      .forEach(s -> results.add(s));  // Unnecessary use of side-effects!
// 正確的收集方式
List<String>results =
     stream.filter(s -> pattern.matcher(s).matches())
             .collect(Collectors.toList());  // No side-effects!

回到流水線執(zhí)行結果的問題上來,需要返回結果的流水線結果存在哪里呢若河?這要分不同的情況討論能岩,下表給出了各種有返回結果的Stream結束操作。

返回類型 對應的結束操作
boolean anyMatch() allMatch() noneMatch()
Optional findFirst() findAny()
歸約結果 reduce() collect()
數組 toArray()
  1. 對于表中返回boolean或者Optional的操作(Optional是存放 一個 值的容器)的操作萧福,由于值返回一個值拉鹃,只需要在對應的Sink中記錄這個值,等到執(zhí)行結束時返回就可以了统锤。
  2. 對于歸約操作毛俏,最終結果放在用戶調用時指定的容器中(容器類型通過收集器指定)。collect(), reduce(), max(), min()都是歸約操作饲窿,雖然max()和min()也是返回一個Optional煌寇,但事實上底層是通過調用reduce()方法實現的。
  3. 對于返回是數組的情況逾雄,毫無疑問的結果會放在數組當中阀溶。這么說當然是對的,但在最終返回數組之前鸦泳,結果其實是存儲在一種叫做Node的數據結構中的银锻。Node是一種多叉樹結構,元素存儲在樹的葉子當中做鹰,并且一個葉子節(jié)點可以存放多個元素击纬。這樣做是為了并行執(zhí)行方便。關于Node的具體結構钾麸,我們會在下一節(jié)探究Stream如何并行執(zhí)行時給出詳細說明更振。

本文詳細介紹了Stream流水線的組織方式和執(zhí)行過程炕桨,學習本文將有助于理解原理并寫出正確的Stream代碼,同時打消你對Stream API效率方面的顧慮肯腕。如你所見献宫,Stream API實現如此巧妙,即使我們使用外部迭代手動編寫等價代碼实撒,也未必更加高效姊途。

注:留下本文所用的JDK版本,以便有考究癖的人考證:

$ java -version
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) Server VM (build 25.101-b13, mixed mode)

文章來源:https://github.com/CarpenterLee/JavaLambdaInternals

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末知态,一起剝皮案震驚了整個濱河市捷兰,隨后出現的幾起案子,更是在濱河造成了極大的恐慌肴甸,老刑警劉巖寂殉,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異原在,居然都是意外死亡友扰,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門庶柿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來村怪,“玉大人,你說我怎么就攤上這事浮庐∩醺海” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵审残,是天一觀的道長梭域。 經常有香客問我,道長搅轿,這世上最難降的妖魔是什么病涨? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮璧坟,結果婚禮上既穆,老公的妹妹穿的比我還像新娘。我一直安慰自己雀鹃,他們只是感情好幻工,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著黎茎,像睡著了一般囊颅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天踢代,我揣著相機與錄音先鱼,去河邊找鬼。 笑死奸鬓,一個胖子當著我的面吹牛,可吹牛的內容都是我干的掸读。 我是一名探鬼主播串远,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼儿惫!你這毒婦竟也來了澡罚?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤肾请,失蹤者是張志新(化名)和其女友劉穎留搔,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體铛铁,經...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡隔显,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了饵逐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片括眠。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖倍权,靈堂內的尸體忽然破棺而出掷豺,到底是詐尸還是另有隱情,我是刑警寧澤薄声,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布当船,位于F島的核電站,受9級特大地震影響默辨,放射性物質發(fā)生泄漏德频。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一廓奕、第九天 我趴在偏房一處隱蔽的房頂上張望抱婉。 院中可真熱鬧,春花似錦桌粉、人聲如沸蒸绩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽患亿。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間步藕,已是汗流浹背惦界。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留咙冗,地道東北人沾歪。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像雾消,于是被迫代替她去往敵國和親灾搏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內容