Java Lambda表達式知多少

1. 匿名內部類實現

匿名內部類仍然是一個類,只是不須要程序員顯示指定類名,編譯器會自動爲該類取名。所以若是有以下形式的代碼,編譯以後將會產生兩個class文件:java

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文件分別是主類和匿名內部類產生的:git

2-AnonymousClass.png

進一步分析主類MainAnonymousClass.class的字節碼,可發現其建立了匿名內部類的對象:程序員

// 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 /*建立內部類對象*/
       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
}

2. Lambda表達式實現

Lambda表達式經過invokedynamic指令實現,書寫Lambda表達式不會產生新的類。若是有以下代碼,編譯以後只有一個class文件:github

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
}

反編譯以後咱們發現Lambda表達式被封裝成了主類的一個私有方法,並經過invokedynamic指令進行調用。數據結構

3. Streams API(I)

你可能沒意識到Java對函數式編程的重視程度,看看Java 8加入函數式編程擴充多少功能就清楚了。Java 8之因此費這麼大功夫引入函數式編程,緣由有二:app

  1. 代碼簡潔函數式編程寫出的代碼簡潔且意圖明確,使用stream接口讓你今後告別for循環。
  2. 多核友好,Java函數式編程使得編寫並行程序從未如此簡單,你須要的所有就是調用一下parallel()方法。

img

圖中4種stream接口繼承自BaseStream,其中IntStream, LongStream, DoubleStream對應三種基本類型(int, long, double,注意不是包裝類型),Stream對應全部剩餘類型的stream視圖。爲不一樣數據類型設置不一樣stream接口,能夠1.提升性能,2.增長特定接口函數。ide

雖然大部分狀況下stream是容器調用Collection.stream()方法獲得的,但streamcollections有如下不一樣:函數式編程

  • 無存儲stream不是一種數據結構,它只是某種數據源的一個視圖,數據源能夠是一個數組,Java容器或I/O channel等。
  • 爲函數式編程而生。對stream的任何修改都不會修改背後的數據源,好比對stream執行過濾操做並不會刪除被過濾的元素,而是會產生一個不包含被過濾元素的新stream
  • 惰式執行stream上的操做並不會當即執行,只有等到用戶真正須要結果的時候纔會執行。
  • 可消費性stream只能被「消費」一次,一旦遍歷過就會失效,就像容器的迭代器那樣,想要再次遍歷必須從新生成。

stream的操做分爲爲兩類,中間操做(intermediate operations)和結束操做(terminal operations),兩者特色是:

  1. 中間操做老是會惰式執行,調用中間操做只會生成一個標記了該操做的新stream,僅此而已。
  2. 結束操做會觸發實際計算,計算髮生時會把全部中間操做積攢的操做以pipeline的方式執行,這樣能夠減小迭代次數。計算完成以後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()

區分中間操做和結束操做最簡單的方法,就是看方法的返回值,返回值爲stream的大都是中間操做,不然是結束操做。

flatMap()

img

函數原型爲<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper),做用是對每一個元素執行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>,執行flatMap()以後,將每一個List都「攤平」成了一個個的數字,因此會新產生一個由5個數字組成的Stream。因此最終將輸出1~5這5個數字。

截止到目前咱們感受良好,已介紹Stream接口函數理解起來並不費勁兒。若是你就此覺得函數式編程不過如此,恐怕是高興地太早了。下一節對Stream規約操做的介紹將刷新你如今的認識。

多面手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),或者是指定並行執行時多個部分結果的合併方式(參數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()自有其存在的理由。

img

需求:求出一組單詞的長度之和。這是個「求和」操做,操做對象輸入類型是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); // 部分和拼接器,並行執行時纔會用到 // (3)
// int lengthSum = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);

上述代碼標號(2)處將i. 字符串映射成長度,ii. 並和當前累加和相加。這顯然是兩步操做,使用reduce()函數將這兩步合二爲一,更有助於提高性能。若是想要使用map()sum()組合來達到上述目的,也是能夠的。

reduce()擅長的是生成一個值,若是想要從Stream生成一個集合或者Map等複雜的對象該怎麼辦呢?終極武器collect()橫空出世!

終極武器collect()

不誇張的講,若是你發現某個功能在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轉換成ListSetMap。雖然代碼語義很明確,但是咱們仍然會有幾個疑問:

  1. Function.identity()是幹什麼的?
  2. String::length是什麼意思?
  3. Collectors是個什麼東西?
接口的靜態方法和默認方法

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

  1. Java 8容許在接口中加入具體方法。接口中的具體方法有兩種,default方法和static方法,identity()就是Function接口的一個靜態方法。
  2. Function.identity()返回一個輸出跟輸入同樣的Lambda表達式對象,等價於形如t -> t形式的Lambda表達式。

上面的解釋是否是讓你疑問更多?不要問我爲何接口中能夠有具體方法,也不要告訴我你以爲t -> tidentity()方法更直觀。我會告訴你接口中的default方法是一個無奈之舉,在Java 7及以前要想在定義好的接口中加入新的抽象方法是很困難甚至不可能的,由於全部實現了該接口的類都要從新實現。試想在Collection接口中加入一個stream()抽象方法會怎樣?default方法就是用來解決這個尷尬問題的,直接在接口中實現新加入的方法。既然已經引入了default方法,爲什麼再也不加入static方法來避免專門的工具類呢!

方法引用

諸如String::length的語法形式叫作方法引用(method references),這種語法用來替代某些特定形式Lambda表達式。若是Lambda表達式的所有內容就是調用一個已有的方法,那麼能夠用方法引用來替代Lambda表達式。方法引用能夠細分爲四類:

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

咱們會在後面的例子中使用方法引用。

收集器

相信前面繁瑣的內容已完全打消了你學習Java函數式編程的熱情,不過很遺憾,下面的內容更繁瑣。但這不能怪Stream類庫,由於要實現的功能自己很複雜。

img

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

  1. 目標容器是什麼?是ArrayList仍是HashSet,或者是個TreeMap
  2. 新元素如何添加到容器中?是List.add()仍是Map.put()

若是並行的進行規約,還須要告訴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工具類可經過靜態方法生成各類經常使用的Collector。舉例來講,若是要將Stream規約成List能夠經過以下兩種方式實現:

https://objcoding.com/2019/03...


本篇文章由一文多發平臺ArtiPub自動發佈

相關文章
相關標籤/搜索