1. 程式人生 > >[JDK] JDK8 Lambda & Stream使用筆記

[JDK] JDK8 Lambda & Stream使用筆記

JDK8 Lambda & Stream使用筆記

簡介

Lambda

一段帶有輸入引數的可執行語句塊。
Java8的lambda表示式給我們提供了建立SAM(Single Abstract Method)介面更加簡單的語法糖
  • 1
  • 2

Stream

Stream是元素的集合,這點讓Stream看起來有些類似Iterator
可以支援順序和並行的對原Stream進行匯聚的操作
高階版本的Iterator
  • 1
  • 2
  • 3

Lambda語法

抽象語法結構

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

簡化Lambda表示式宣告

1. 引數型別省略
  • 1
(param1,param2, ..., paramN) -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

List<String> lowercaseNames = names.stream().map((String name) -> {return name.toLowerCase();}).collect(Collectors.toList());

//編譯器都可以從上下文環境中推斷出lambda表示式的引數型別
List<String> lowercaseNames = names.stream().map((name) -> {return name.toLowerCase();}).collect(Collectors.toList());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
2. 當lambda表示式的引數個數只有一個,可以省略小括號
  • 1
param1 -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

List<String> lowercaseNames = names.stream().map(name -> {return name.toLowerCase();}).collect(Collectors.toList());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
3. 當lambda表示式只包含一條語句時,可以省略大括號、return和語句結尾的分號
  • 1
param1 -> statment

List<String> lowercaseNames = names.stream().map(name -> name.toLowerCase()).collect(Collectors.toList());
  • 1
  • 2
  • 3
  • 4
4. 使用Method Reference
  • 1
//注意,這段程式碼在Idea 13.0.2中顯示有錯誤,但是可以正常執行
List<String> lowercaseNames = names.stream().map(String::toLowerCase).collect(Collectors.toList());
  • 1
  • 2

Lambda表示式眼中的外部世界

1. lambda表示式的三個重要組成部分:

- 輸入引數
- 可執行語句
- 存放外部變數的空間

2. 外部變數被lambda表示式引用,編譯器會隱式的把其當成final來處理
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

String[] array = {"a", "b", "c"};
for(Integer i : Lists.newArrayList(1,2,3)){
  Stream.of(array).map(item -> Strings.padEnd(item, i, '@')).forEach(System.out::println);
}

String[] array = {"a", "b", "c"};
for(int i = 1; i<4; i++){
  Stream.of(array).map(item -> Strings.padEnd(item, i, '@')).forEach(System.out::println);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Lambda表示式眼中的this

不是指向lambda表示式產生的那個SAM物件,而是宣告它的外部物件。[外部類作用域]
  • 1

簡化Lambda表示式中方法和構造器引用

1. 方法引用 
  • 1
//等同於把lambda表示式的引數直接當成instanceMethod|staticMethod的引數來呼叫
objectName::instanceMethod
ClassName::staticMethod
//等同於把lambda表示式的第一個引數當成instanceMethod的目標物件,其他剩餘引數當成該方法的引數
ClassName::instanceMethod
  • 1
  • 2
  • 3
  • 4
  • 5
System.out::println等同於x->System.out.println(x)
Math::max等同於(x, y)->Math.max(x,y)
String::toLowerCase等同於x->x.toLowerCase()


2. 構造器引用    

ClassName::new,把lambda表示式的引數當成ClassName構造器的引數 。例如BigDecimal::new等同於x->new BigDecimal(x)。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Stream語法

通用語法

- 建立Stream
- 轉換Stream: 每次轉換原有Stream物件不改變,返回一個新的Stream物件(**可以有多次轉換**)
- 匯聚(Reduce)Stream
  • 1
  • 2
  • 3

Stream通用語法示意圖

建立Stream

- 通過Stream介面的靜態工廠方法(注意:Java8裡介面可以帶靜態方法)
- 通過Collection介面的預設方法(預設方法:Default method,也是Java8中的一個新特性,就是介面中的一個帶有實現的方法,後續文章會有介紹)–stream(),把一個Collection物件轉換成Stream
  • 1
  • 2

使用Stream靜態方法來建立Stream

  • of方法:有兩個overload方法,一個接受變長引數,一個介面單一值
Stream<Integer> integerStream = Stream.of(1, 2, 3, 5);
Stream<String> stringStream = Stream.of("taobao");
  • 1
  • 2
  • generator方法:生成一個無限長度的Stream,其元素的生成是通過給定的Supplier(這個介面可以看成一個物件的工廠,每次呼叫返回一個給定型別的物件)
Stream.generate(new Supplier<Double>() {
        @Override
        public Double get() {
        return Math.random();
    }   
});
Stream.generate(() -> Math.random());
Stream.generate(Math::random);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
三條語句的作用都是一樣的,只是使用了lambda表示式和方法引用的語法來簡化程式碼。每條語句其實都是生成一個無限長度的Stream,其中值是隨機的。這個無限長度Stream是懶載入,一般這種無限長度的Stream都會配合Stream的limit()方法來用。
  • 1
  • iterate方法:也是生成無限長度的Stream,和generator不同的是,其元素的生成是重複對給定的種子值(seed)呼叫使用者指定函式來生成的。其中包含的元素可以認為是:seed,f(seed),f(f(seed))無限迴圈
//先獲取一個無限長度的正整數集合的Stream,然後取出前10個列印。千萬記住使用limit方法,不然會無限列印下去
Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);
  • 1
  • 2

通過Collection子類獲取Stream

檢視Java doc就可以發現Collection介面有一個stream方法,所以其所有子類都都可以獲取對應的Stream物件。
  • 1
public interface Collection<E> extends Iterable<E> {
    //其他方法省略
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

轉換Stream

distinct

對於Stream中包含的元素進行去重操作(去重邏輯依賴元素的equals方法),新生成的Stream中沒有重複的元素
  • 1

distinct方法示意圖

filter

對於Stream中包含的元素使用給定的過濾函式進行過濾操作,新生成的Stream只包含符合條件的元素
  • 1

filter方法示意圖

map

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

map方法示意圖

flatMap

和map類似,不同的是其每個元素轉換得到的是Stream物件,會把子Stream中的元素壓縮到父集合中
  • 1

flatMap方法示意圖

peek

生成一個包含原Stream的所有元素的新Stream,同時會提供一個消費函式(Consumer例項),新Stream每個元素被消費的時候都會執行給定的消費函式
  • 1

peek方法示意圖

limit

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

limit方法示意圖

skip

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

skip方法示意圖

上述方法複合使用

[宣告式程式設計]這段程式碼演示了上面介紹的所有轉換方法(除了flatMap),簡單解釋一下這段程式碼的含義:給定一個Integer型別的List,獲取其對應的Stream物件,然後進行過濾掉null,再去重,再每 個元素乘以2,再每個元素被消費的時候列印自身,在跳過前兩個元素,最後去前四個元素進行加和運算。

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)
                //1,1,2,3,4,5,6,7,8,9,10
                //.peek(x -> System.out.println("peek0: "+x))
                .distinct()
                //1,2,3,4,5,6,7,8,9,10
                .mapToInt(num -> num * 2)
                //2,4,6,8,10,12
                .skip(2)
                //6,8,10,12,14,16,18,20
                .limit(4)
                .peek(System.out::println)
                //6,8,10,12
                .sum());
                //36

//result
6
8
10
12
sum is:36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

peek函式是針對最終stream進行消費自身操作的。

轉換操作都是lazy的,多個轉換操作只會在匯聚操作(見下節)的時候融合起來,一次迴圈完成。我們可以這樣簡單的理解,Stream裡有個操作函式的集合,每次轉換操作就是把轉換函式放入這個集合中,在匯聚操作的時候迴圈Stream對應的集合,然後對每個元素執行所有的函式

匯聚Stream

匯聚操作(也稱為摺疊)接受一個元素序列為輸入,反覆使用某個合併操作,把序列中的元素合併成一個彙總的結果。比如查詢一個數字列表的總和或者最大值,或者把這些數字累積成一個List物件。Stream介面有一些通用的匯聚操作,比如reduce()和collect();也有一些特定用途的匯聚操作,比如sum(),max()和count()。

注意:sum方法不是所有的Stream物件都有的,只有IntStream、LongStream和DoubleStream是例項才有

可變匯聚

把輸入的元素們累積到一個可變的容器中,比如Collection或者StringBuilder
  • 1

Ex1.

<R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);
  • 1
  • 2
  • 3

引數說明

- Supplier supplier是一個工廠函式,用來生成一個新的容器
- BiConsumer accumulator也是一個函式,用來把Stream中的元素新增到結果容器中
- BiConsumer combiner還是一個函式,用來把中間狀態的多個結果容器合併成為一個(併發的時候會用到)
  • 1
  • 2
  • 3
/** 對一個元素是Integer型別的List,先過濾掉全部的null,然後把剩下的元素收集到一個新的List中*/
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));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

步驟分析

- 第一個函式生成一個新的ArrayList例項
- 第二個函式接受兩個引數,第一個是前面生成的ArrayList物件,二個是stream中包含的元素,函式體就是把stream中的元素加入ArrayList物件中。第二個函式被反覆呼叫直到原stream的元素被消費完畢
- 第三個函式也是接受兩個引數,這兩個都是ArrayList型別的,函式體就是把第二個ArrayList全部加入到第一個中
  • 1
  • 2
  • 3

Ex2.

<R, A> R collect(Collector<? super T, A, R> collector);
  • 1
Collectors.toCollection()收集到Collection中
Collectors.toList()收集到List中
Collectors.toSet()收集到Set中
  • 1
  • 2
  • 3
/**Ex1程式碼簡化*/
List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).collect(Collectors.toList());
  • 1
  • 2

其他匯聚

除去可變匯聚剩下的,一般都不是通過反覆修改某個可變物件,而是通過把前一次的匯聚結果當成下一次的入參,反覆如此。比如reduce,count,allMatch
  • 1
  • reduce方法

    reduce方法非常的通用,後面介紹的count,sum等都可以使用其實現。reduce方法有三個override的方法

Ex1.

/**方法定義*/
Optional<T> reduce(BinaryOperator<T> accumulator);

/**示例*/
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) -> sum + item).get());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
接受一個BinaryOperator型別的引數,在使用的時候我們可以用lambda表示式來。
  • 1

程式碼分析

reduce方法接受一個函式,這個函式有兩個引數,第一個引數是上次函式執行的返回值(也稱為中間結果),第二個引數是stream中的元素,這個函式把這兩個值相加,得到的和會被賦值給下次執行這個函式的第一個引數。要注意的是:第一次執行的時候第一個引數的值是Stream的第一個元素,第二個引數是Stream的第二個元素。這個方法返回值型別是Optional,這是Java8防止出現NPE的一種可行方法,可以簡單簡單的認為是一個容器,其中可能會包含0個或者1個物件。 這個過程視覺化的結果如圖:

reduce示意圖

Ex2.

/**方法定義*/
T reduce(T identity, BinaryOperator<T> accumulator);

/**示例*/
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));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
這個定義與上面已經介紹過的基本一致,不同的是:它允許使用者提供一個迴圈計算的初始值,如果Stream為空,就直接返回該值。而且這個方法不會返回Optional
  • 1
  • 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());
  • 1
  • 2
  • allMatch:是不是Stream中的所有元素都滿足給定的匹配條件
  • anyMatch:Stream中是否存在任何一個元素滿足匹配條件
  • findFirst: 返回Stream中的第一個元素,如果Stream為空,返回空Optional
  • noneMatch:是不是Stream中的所有元素都不滿足給定的匹配條件
  • max和min:使用給定的比較器(Operator),返回Stream中的最大|最小值
/** allMatch和max示例*/
List<Integer> 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) > o1.compareTo(o2)).ifPresent(System.out::println);
  • 1
  • 2
  • 3
  • 4

REFRENCES

--------------------- 本文來自 離兮丶斜陽 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/sinat_28690417/article/details/80748930?utm_source=copy