Java8特性詳解: lambda表達式與StreamAPI

 原文出處:https://www.cnblogs.com/aoeiuv/p/5911692.html

1.lambda表達式

Java8最值得學習的特性就是Lambda表達式和Stream API,如果有python或者javascript的語言基礎,對理解Lambda表達式有很大幫助,因爲Java正在將自己變的更高(Sha)級(Gua),更人性化。--------可以這麼說lambda表達式其實就是實現SAM接口的語法糖。(SAM接口:https://blog.csdn.net/liuchangjie0112/article/details/90715652)lambda寫的好可以極大的減少代碼冗餘,同時可讀性也好過冗長的內部類,匿名類。

先列舉兩個常見的簡化(簡單的代碼同樣好理解)

  • 創建線程

  • 排序

lambda表達式配合Java8新特性Stream API可以將業務功能通過函數式編程簡潔的實現。(爲後面的例子做鋪墊)

例如:

這段代碼就是對一個字符串的列表,把其中包含的每個字符串都轉換成全小寫的字符串。注意代碼第四行的map方法調用,這裏map方法就是接受了一個lambda表達式。

1.1 lambda表達式語法

1.1.1 lambda表達式的一般語法

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

這是lambda表達式的完全式語法,後面幾種語法是對它的簡化。

1.1.2單參數語法

param1 -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

當lambda表達式的參數個數只有一個,可以省略小括號

例如:將列表中的字符串轉換爲全小寫

List<String> proNames = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
List<String> lowercaseNames1 = proNames.stream().map(name -> {return name.toLowerCase();}).collect(Collectors.toList());

1.1.3 單語句寫法

param1 -> statment

當lambda表達式只包含一條語句時,可以省略大括號、return和語句結尾的分號

例如:將列表中的字符串轉換爲全小寫

List<String> proNames = Arrays.asList(new String[]{"Ni","Hao","Lambda"});

List<String> lowercaseNames2 = proNames.stream().map(name -> name.toLowerCase()).collect(Collectors.toList());

1.1.4 方法引用寫法

(方法引用和lambda一樣是Java8新語言特性,後面會講到)

Class or instance :: method

例如:將列表中的字符串轉換爲全小寫

List<String> proNames = Arrays.asList(new String[]{"Ni","Hao","Lambda"});

List<String> lowercaseNames3 = proNames.stream().map(String::toLowerCase).collect(Collectors.toList());

1.2 lambda表達式可使用的變量

先舉例:

//將爲列表中的字符串添加前綴字符串
String waibu = "lambda :";
List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
List<String>execStrs = proStrs.stream().map(chuandi -> {
Long zidingyi = System.currentTimeMillis();
return waibu + chuandi + " -----:" + zidingyi;
}).collect(Collectors.toList());
execStrs.forEach(System.out::println);

輸出:

lambda :Ni -----:1474622341604
lambda :Hao -----:1474622341604
lambda :Lambda -----:1474622341604

 

變量waibu :外部變量

變量chuandi :傳遞變量

變量zidingyi :內部自定義變量

 

lambda表達式可以訪問給它傳遞的變量,訪問自己內部定義的變量,同時也能訪問它外部的變量。

不過lambda表達式訪問外部變量有一個非常重要的限制:變量不可變(只是引用不可變,而不是真正的不可變)。

當在表達式內部修改waibu = waibu + " ";時,IDE就會提示你:

Local variable waibu defined in an enclosing scope must be final or effectively final

編譯時會報錯。因爲變量waibu被lambda表達式引用,所以編譯器會隱式的把其當成final來處理。

以前Java的匿名內部類在訪問外部變量的時候,外部變量必須用final修飾。現在java8對這個限制做了優化,可以不用顯示使用final修飾,但是編譯器隱式當成final來處理。

1.3 lambda表達式中的this概念

在lambda中,this不是指向lambda表達式產生的那個SAM對象,而是聲明它的外部對象。

例如:

public class WhatThis {

     public void whatThis(){
           //轉全小寫
           List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
           List<String> execStrs = proStrs.stream().map(str -> {
                 System.out.println(this.getClass().getName());
                 return str.toLowerCase();
           }).collect(Collectors.toList());
           execStrs.forEach(System.out::println);
     }

     public static void main(String[] args) {
           WhatThis wt = new WhatThis();
           wt.whatThis();
     }
}

輸出:

com.wzg.test.WhatThis
com.wzg.test.WhatThis
com.wzg.test.WhatThis
ni
hao
lambda

2.方法引用和構造器引用

本人認爲是進一步簡化lambda表達式的聲明的一種語法糖。

前面的例子中已有使用到: execStrs.forEach(System.out::println);

2.1方法引用

objectName::instanceMethod

ClassName::staticMethod

ClassName::instanceMethod

前兩種方式類似,等同於把lambda表達式的參數直接當成instanceMethod|staticMethod的參數來調用。比如System.out::println等同於x->System.out.println(x);Math::max等同於(x, y)->Math.max(x,y)。

最後一種方式,等同於把lambda表達式的第一個參數當成instanceMethod的目標對象,其他剩餘參數當成該方法的參數。比如String::toLowerCase等同於x->x.toLowerCase()。

可以這麼理解,前兩種是將傳入對象當參數執行方法,後一種是調用傳入對象的方法。

2.2構造器引用

構造器引用語法如下:ClassName::new,把lambda表達式的參數當成ClassName構造器的參數 。例如BigDecimal::new等同於x->new BigDecimal(x)。

3.Stream語法

兩句話理解Stream:

1.Stream是元素的集合,這點讓Stream看起來用些類似Iterator;
2.可以支持順序和並行的對原Stream進行匯聚的操作;

大家可以把Stream當成一個裝飾後的Iterator。原始版本的Iterator,用戶只能逐個遍歷元素並對其執行某些操作;包裝後的Stream,用戶只要給出需要對其包含的元素執行什麼操作,比如“過濾掉長度大於10的字符串”、“獲取每個字符串的首字母”等,具體這些操作如何應用到每個元素上,就給Stream就好了!原先是人告訴計算機一步一步怎麼做,現在是告訴計算機做什麼,計算機自己決定怎麼做。當然這個“怎麼做”還是比較弱的。

例子:

//Lists是Guava中的一個工具類
List<Integer> nums = Lists.newArrayList(1,null,3,4,null,6);
nums.stream().filter(num -> num != null).count();

上面這段代碼是獲取一個List中,元素不爲null的個數。這段代碼雖然很簡短,但是卻是一個很好的入門級別的例子來體現如何使用Stream,正所謂“麻雀雖小五臟俱全”。我們現在開始深入解刨這個例子,完成以後你可能可以基本掌握Stream的用法!

圖片就是對於Stream例子的一個解析,可以很清楚的看見:原本一條語句被三種顏色的框分割成了三個部分。紅色框中的語句是一個Stream的生命開始的地方,負責創建一個Stream實例;綠色框中的語句是賦予Stream靈魂的地方,把一個Stream轉換成另外一個Stream,紅框的語句生成的是一個包含所有nums變量的Stream,進過綠框的filter方法以後,重新生成了一個過濾掉原nums列表所有null以後的Stream;藍色框中的語句是豐收的地方,把Stream的裏面包含的內容按照某種算法來匯聚成一個值,例子中是獲取Stream中包含的元素個數。如果這樣解析以後,還不理解,那就只能動用“核武器”–圖形化,一圖抵千言!

使用Stream的基本步驟:

1.創建Stream;
2.轉換Stream,每次轉換原有Stream對象不改變,返回一個新的Stream對象(**可以有多次轉換**);
3.對Stream進行聚合(Reduce)操作,獲取想要的結果;

3.1怎麼得到Stream

最常用的創建Stream有兩種途徑:

1.通過Stream接口的靜態工廠方法(注意:Java8裏接口可以帶靜態方法);
2.通過Collection接口的默認方法(默認方法:Default method,也是Java8中的一個新特性,就是接口中的一個帶有實現的方法)–stream(),把一個Collection對象轉換成Stream

3.1.1 使用Stream靜態方法來創建Stream

1. of方法:有兩個overload方法,一個接受變長參數,一個接口單一值

Stream<Integer> integerStream = Stream.of(1, 2, 3, 5);
Stream<String> stringStream = Stream.of("taobao");


2. generator方法:生成一個無限長度的Stream,其元素的生成是通過給定的Supplier(這個接口可以看成一個對象的工廠,每次調用返回一個給定類型的對象)

Stream.generate(new Supplier<Double>() {
    @Override
    public Double get() {
         return Math.random();
    }
});


Stream.generate(() -> Math.random());
Stream.generate(Math::random);
三條語句的作用都是一樣的,只是使用了lambda表達式和方法引用的語法來簡化代碼。每條語句其實都是生成一個無限長度的Stream,其中值是隨機的。這個無限長度Stream是懶加載,一般這種無限長度的Stream都會配合Stream的limit()方法來用。


3. iterate方法:也是生成無限長度的Stream,和generator不同的是,其元素的生成是重複對給定的種子值(seed)調用用戶指定函數來生成的。其中包含的元素可以認爲是:seed,f(seed),f(f(seed))無限循環

Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);
這段代碼就是先獲取一個無限長度的正整數集合的Stream,然後取出前10個打印。千萬記住使用limit方法,不然會無限打印下去。

3.1.2通過Collection子類獲取Stream

Collection接口有一個stream方法,所以其所有子類都都可以獲取對應的Stream對象。

public interface Collection<E> extends Iterable<E> {
      //其他方法省略
     default Stream<E> stream() {
          return StreamSupport.stream(spliterator(), false);
     }
}

3.2轉換Stream

轉換Stream其實就是把一個Stream通過某些行爲轉換成一個新的Stream。Stream接口中定義了幾個常用的轉換方法,下面我們挑選幾個常用的轉換方法來解釋。
1. distinct: 對於Stream中包含的元素進行去重操作(去重邏輯依賴元素的equals方法),新生成的Stream中沒有重複的元素;

2. filter: 對於Stream中包含的元素使用給定的過濾函數進行過濾操作,新生成的Stream只包含符合條件的元素;

3. map: 對於Stream中包含的元素使用給定的轉換函數進行轉換操作,新生成的Stream只包含轉換生成的元素。這個方法有三個對於原始類型的變種方法,分別是:mapToInt,mapToLong和mapToDouble。這三個方法也比較好理解,比如mapToInt就是把原始Stream轉換成一個新的Stream,這個新生成的Stream中的元素都是int類型。之所以會有這樣三個變種方法,可以免除自動裝箱/拆箱的額外消耗;

4. flatMap:和map類似,不同的是其每個元素轉換得到的是Stream對象,會把子Stream中的元素壓縮到父集合中;

flatMap給一段代碼理解:

Stream<List<Integer>> inputStream = Stream.of(
 Arrays.asList(1),
 Arrays.asList(2, 3),
 Arrays.asList(4, 5, 6)
 );
Stream<Integer> outputStream = inputStream.
flatMap((childList) -> childList.stream());

flatMap 把 input Stream 中的層級結構扁平化,就是將最底層元素抽出來放到一起,最終 output 的新 Stream 裏面已經沒有 List 了,都是直接的數字。

 

5. peek: 生成一個包含原Stream的所有元素的新Stream,同時會提供一個消費函數(Consumer實例),新Stream每個元素被消費的時候都會執行給定的消費函數;

6. limit: 對一個Stream進行截斷操作,獲取其前N個元素,如果原Stream中包含的元素個數小於N,那就獲取其所有的元素;

7. skip: 返回一個丟棄原Stream的前N個元素後剩下元素組成的新Stream,如果原Stream中包含的元素個數小於N,那麼返回空Stream;

 

整體調用例子:

 

List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
System.out.println(“sum is:”+nums.stream().filter(num -> num != null).distinct().mapToInt(num -> num * 2).peek(System.out::println).skip(2).limit(4).sum());

這段代碼演示了上面介紹的所有轉換方法(除了flatMap),簡單解釋一下這段代碼的含義:給定一個Integer類型的List,獲取其對應的Stream對象,然後進行過濾掉null,再去重,再每個元素乘以2,再每個元素被消費的時候打印自身,在跳過前兩個元素,最後去前四個元素進行加和運算(解釋一大堆,很像廢話,因爲基本看了方法名就知道要做什麼了。這個就是聲明式編程的一大好處!)。大家可以參考上面對於每個方法的解釋,看看最終的輸出是什麼。

 

2
4
6
8
10
12
sum is:36

可能會有這樣的疑問:在對於一個Stream進行多次轉換操作,每次都對Stream的每個元素進行轉換,而且是執行多次,這樣時間複雜度就是一個for循環裏把所有操作都做掉的N(轉換的次數)倍啊。其實不是這樣的,轉換操作都是lazy的,多個轉換操作只會在匯聚操作(見下節)的時候融合起來,一次循環完成。我們可以這樣簡單的理解,Stream裏有個操作函數的集合,每次轉換操作就是把轉換函數放入這個集合中,在匯聚操作的時候循環Stream對應的集合,然後對每個元素執行所有的函數。

 

3.3匯聚(Reduce)Stream

匯聚操作(也稱爲摺疊)接受一個元素序列爲輸入,反覆使用某個合併操作,把序列中的元素合併成一個彙總的結果。比如查找一個數字列表的總和或者最大值,或者把這些數字累積成一個List對象。Stream接口有一些通用的匯聚操作,比如reduce()和collect();也有一些特定用途的匯聚操作,比如sum(),max()和count()。注意:sum方法不是所有的Stream對象都有的,只有IntStream、LongStream和DoubleStream是實例纔有。

下面會分兩部分來介紹匯聚操作:

可變匯聚:把輸入的元素們累積到一個可變的容器中,比如Collection或者StringBuilder;
其他匯聚:除去可變匯聚剩下的,一般都不是通過反覆修改某個可變對象,而是通過把前一次的匯聚結果當成下一次的入參,反覆如此。比如reduce,count,allMatch;

3.3.1可變匯聚

可變匯聚對應的只有一個方法:collect,正如其名字顯示的,它可以把Stream中的要有元素收集到一個結果容器中(比如Collection)。先看一下最通用的collect方法的定義(還有其他override方法):

<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
先來看看這三個參數的含義:Supplier supplier是一個工廠函數,用來生成一個新的容器;BiConsumer accumulator也是一個函數,用來把Stream中的元素添加到結果容器中;BiConsumer combiner還是一個函數,用來把中間狀態的多個結果容器合併成爲一個(併發的時候會用到)。看暈了?來段代碼!

List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
collect(() -> new ArrayList<Integer>(),
(list, item) -> list.add(item),
(list1, list2) -> list1.addAll(list2));

上面這段代碼就是對一個元素是Integer類型的List,先過濾掉全部的null,然後把剩下的元素收集到一個新的List中。進一步看一下collect方法的三個參數,都是lambda形式的函數。

第一個函數生成一個新的ArrayList實例;
第二個函數接受兩個參數,第一個是前面生成的ArrayList對象,二個是stream中包含的元素,函數體就是把stream中的元素加入ArrayList對象中。第二個函數被反覆調用直到原stream的元素被消費完畢;
第三個函數也是接受兩個參數,這兩個都是ArrayList類型的,函數體就是把第二個ArrayList全部加入到第一個中;
但是上面的collect方法調用也有點太複雜了,沒關係!我們來看一下collect方法另外一個override的版本,其依賴[Collector](http://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html)。

<R, A> R collect(Collector<? super T, A, R> collector);
這樣清爽多了!Java8還給我們提供了Collector的工具類–[Collectors](http://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html),其中已經定義了一些靜態工廠方法,比如:Collectors.toCollection()收集到Collection中, Collectors.toList()收集到List中和Collectors.toSet()收集到Set中。這樣的靜態方法還有很多,這裏就不一一介紹了,大家可以直接去看JavaDoc。下面看看使用Collectors對於代碼的簡化:

List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
collect(Collectors.toList());

3.3.2其他匯聚

– reduce方法:reduce方法非常的通用,後面介紹的count,sum等都可以使用其實現。reduce方法有三個override的方法,本文介紹兩個最常用的。先來看reduce方法的第一種形式,其方法定義如下:

Optional<T> reduce(BinaryOperator<T> accumulator);
接受一個BinaryOperator類型的參數,在使用的時候我們可以用lambda表達式來。

List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println("ints sum is:" + ints.stream().reduce((sum, item) -&gt; sum + item).get());
可以看到reduce方法接受一個函數,這個函數有兩個參數,第一個參數是上次函數執行的返回值(也稱爲中間結果),第二個參數是stream中的元素,這個函數把這兩個值相加,得到的和會被賦值給下次執行這個函數的第一個參數。要注意的是:**第一次執行的時候第一個參數的值是Stream的第一個元素,第二個參數是Stream的第二個元素**。這個方法返回值類型是Optional,這是Java8防止出現NPE的一種可行方法,後面的文章會詳細介紹,這裏就簡單的認爲是一個容器,其中可能會包含0個或者1個對象。
這個過程可視化的結果如圖:

reduce方法還有一個很常用的變種:

T reduce(T identity, BinaryOperator<T> accumulator);
這個定義上上面已經介紹過的基本一致,不同的是:它允許用戶提供一個循環計算的初始值,如果Stream爲空,就直接返回該值。而且這個方法不會返回Optional,因爲其不會出現null值。下面直接給出例子,就不再做說明了。

List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println("ints sum is:" + ints.stream().reduce(0, (sum, item) -> sum + item));
– count方法:獲取Stream中元素的個數。比較簡單,這裏就直接給出例子,不做解釋了。

List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println("ints sum is:" + ints.stream().count());

– 搜索相關
– allMatch:是不是Stream中的所有元素都滿足給定的匹配條件
– anyMatch:Stream中是否存在任何一個元素滿足匹配條件
– findFirst: 返回Stream中的第一個元素,如果Stream爲空,返回空Optional
– noneMatch:是不是Stream中的所有元素都不滿足給定的匹配條件
– max和min:使用給定的比較器(Operator),返回Stream中的最大|最小值
下面給出allMatch和max的例子,剩下的方法讀者當成練習。

查看源代碼打印幫助
List<Integer&gt; ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println(ints.stream().allMatch(item -> item < 100));
ints.stream().max((o1, o2) -&gt; o1.compareTo(o2)).ifPresent(System.out::println);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章