帶你解惑大廠必會使用的 Stream流、方法引用🔥

前言:

應廣大讀者的須要,霈哥給你們帶來新一期的乾貨啦!java

帶你解惑大廠必會使用的 Lambda表達式、函數式接口

帶你解惑大廠必會使用的 Stream流、方法引用

若對你和身邊的朋友有幫助, 抓緊關注 IT霈哥 點贊! 點贊! 點贊! 評論!收藏! 分享給更多的朋友共同窗習交流, 天天持續掘金離不開你的點贊支持!

第三章 Stream流

在Java 8中,得益於Lambda所帶來的函數式編程,引入了一個全新的Stream概念,用於解決已有集合類庫既有的弊端。算法

3.1 引言

傳統集合的多步遍歷代碼編程

幾乎全部的集合(如Collection接口或Map接口等)都支持直接或間接的遍歷操做。而當咱們須要對集合中的元素進行操做的時候,除了必需的添加、刪除、獲取外,最典型的就是集合遍歷。例如:後端

public class Demo10ForEach {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("張無忌");
        list.add("周芷若");
        list.add("趙敏");
        list.add("張強");
        list.add("張三丰");
        for (String name : list) {
          	System.out.println(name);
        }
    }  
}
複製代碼

這是一段很是簡單的集合遍歷操做:對集合中的每個字符串都進行打印輸出操做。數組

循環遍歷的弊端markdown

Java 8的Lambda讓咱們能夠更加專一於作什麼(What),而不是怎麼作(How),這點此前已經結合內部類進行了對比說明。如今,咱們仔細體會一下上例代碼,能夠發現:數據結構

  • for循環的語法就是「怎麼作
  • for循環的循環體纔是「作什麼

爲何使用循環?由於要進行遍歷。但循環是遍歷的惟一方式嗎?遍歷是指每個元素逐一進行處理,而並非從第一個到最後一個順次處理的循環。前者是目的,後者是方式。app

試想一下,若是但願對集合中的元素進行篩選過濾:dom

  1. 將集合A根據條件一過濾爲子集B
  2. 而後再根據條件二過濾爲子集C

那怎麼辦?在Java 8以前的作法可能爲:函數式編程

這段代碼中含有三個循環,每個做用不一樣:

  1. 首先篩選全部姓張的人;
  2. 而後篩選名字有三個字的人;
  3. 最後進行對結果進行打印輸出。
public class Demo11NormalFilter {
  	public static void main(String[] args) {
      	List<String> list = new ArrayList<>();
        list.add("張無忌");
        list.add("周芷若");
        list.add("趙敏");
        list.add("張強");
        list.add("張三丰");

        List<String> zhangList = new ArrayList<>();
        for (String name : list) {
            if (name.startsWith("張")) {
              	zhangList.add(name);
            }
        }

        List<String> shortList = new ArrayList<>();
        for (String name : zhangList) {
            if (name.length() == 3) {
              	shortList.add(name);
            }
        }

        for (String name : shortList) {
          	System.out.println(name);
        }
    }
}
複製代碼

每當咱們須要對集合中的元素進行操做的時候,老是須要進行循環、循環、再循環。這是理所固然的麼?不是。循環是作事情的方式,而不是目的。另外一方面,使用線性循環就意味着只能遍歷一次。若是但願再次遍歷,只能再使用另外一個循環從頭開始。

那,Lambda的衍生物Stream能給咱們帶來怎樣更加優雅的寫法呢?

Stream的更優寫法

下面來看一下藉助Java 8的Stream API,什麼才叫優雅:

public class Demo12StreamFilter {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("張無忌");
        list.add("周芷若");
        list.add("趙敏");
        list.add("張強");
        list.add("張三丰");

        list.stream()
          	.filter(s -> s.startsWith("張"))
            .filter(s -> s.length() == 3)
            .forEach(s -> System.out.println(s));
    }
}
複製代碼

直接閱讀代碼的字面意思便可完美展現無關邏輯方式的語義:獲取流、過濾姓張、過濾長度爲三、逐一打印。代碼中並無體現使用線性循環或是其餘任何算法進行遍歷,咱們真正要作的事情內容被更好地體如今代碼中。

3.2 流式思想概述

注意:請暫時忘記對傳統IO流的固有印象!

總體來看,流式思想相似於工廠車間的「生產流水線」。

當須要對多個元素進行操做(特別是多步操做)的時候,考慮到性能及便利性,咱們應該首先拼好一個「模型」步驟方案,而後再按照方案去執行它。

這張圖中展現了過濾、映射、跳過、計數等多步操做,這是一種集合元素的處理方案,而方案就是一種「函數模型」。圖中的每個方框都是一個「流」,調用指定的方法,能夠從一個流模型轉換爲另外一個流模型。而最右側的數字3是最終結果。

這裏的filtermapskip都是在對函數模型進行操做,集合元素並無真正被處理。只有當終結方法count執行的時候,整個模型纔會按照指定策略執行操做。而這得益於Lambda的延遲執行特性。

備註:「Stream流」實際上是一個集合元素的函數模型,它並非集合,也不是數據結構,其自己並不存儲任何元素(或其地址值)。

3.3 獲取流方式

java.util.stream.Stream<T>是Java 8新加入的最經常使用的流接口。(這並非一個函數式接口。)

獲取一個流很是簡單,有如下幾種經常使用的方式:

  • 全部的Collection集合均可以經過stream默認方法獲取流;
  • Stream接口的靜態方法of能夠獲取數組對應的流。

方式1 : 根據Collection獲取流

首先,java.util.Collection接口中加入了default方法stream用來獲取流,因此其全部實現類都可獲取流。

import java.util.*;
import java.util.stream.Stream;
/* 獲取Stream流的方式 1.Collection中 方法 Stream stream() 2.Stream接口 中靜態方法 of(T...t) 向Stream中添加多個數據 */
public class Demo13GetStream {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // ...
        Stream<String> stream1 = list.stream();

        Set<String> set = new HashSet<>();
        // ...
        Stream<String> stream2 = set.stream();
    }
}
複製代碼

方式2: 根據數組獲取流

若是使用的不是集合或映射而是數組,因爲數組對象不可能添加默認方法,因此Stream接口中提供了靜態方法of,使用很簡單:

import java.util.stream.Stream;

public class Demo14GetStream {
    public static void main(String[] args) {
        String[] array = { "張無忌", "張翠山", "張三丰", "張一元" };
        Stream<String> stream = Stream.of(array);
    }
}
複製代碼

備註:of方法的參數實際上是一個可變參數,因此支持數組。

3.4 經常使用方法

流模型的操做很豐富,這裏介紹一些經常使用的API。這些方法能夠被分紅兩種:

  • 終結方法:返回值類型再也不是Stream接口自身類型的方法,所以再也不支持相似StringBuilder那樣的鏈式調用。本小節中,終結方法包括countforEach方法。
  • 非終結方法:返回值類型仍然是Stream接口自身類型的方法,所以支持鏈式調用。(除了終結方法外,其他方法均爲非終結方法。)

備註:本小節以外的更多方法,請自行參考API文檔。

forEach : 逐一處理

雖然方法名字叫forEach,可是與for循環中的「for-each」暱稱不一樣,該方法並不保證元素的逐一消費動做在流中是被有序執行的

void forEach(Consumer<? super T> action);
複製代碼

該方法接收一個Consumer接口函數,會將每個流元素交給該函數進行處理。例如:

import java.util.stream.Stream;

public class Demo15StreamForEach {
    public static void main(String[] args) {
        Stream<String> stream =  Stream.of("大娃","二娃","三娃","四娃","五娃","六娃","七娃","爺爺","蛇精","蠍子精");
        //Stream<String> stream = Stream.of("張無忌", "張三丰", "周芷若");
        stream.forEach((String str)->{System.out.println(str);});
    }
}
複製代碼

在這裏,lambda表達式(String str)->{System.out.println(str);}就是一個Consumer函數式接口的示例。

filter:過濾

能夠經過filter方法將一個流轉換成另外一個子集流。方法聲明:

Stream<T> filter(Predicate<? super T> predicate);
複製代碼

該接口接收一個Predicate函數式接口參數(能夠是一個Lambda)做爲篩選條件。

基本使用

Stream流中的filter方法基本使用的代碼如:

public class Demo16StreamFilter {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("張無忌", "張三丰", "周芷若");
        Stream<String> result = original.filter((String s) -> {return s.startsWith("張");});
    }
}
複製代碼

在這裏經過Lambda表達式來指定了篩選的條件:必須姓張。

count:統計個數

正如舊集合Collection當中的size方法同樣,流提供count方法來數一數其中的元素個數:

long count();
複製代碼

該方法返回一個long值表明元素個數(再也不像舊集合那樣是int值)。基本使用:

public class Demo17StreamCount {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("張無忌", "張三丰", "周芷若");
        Stream<String> result = original.filter(s -> s.startsWith("張"));
        System.out.println(result.count()); // 2
    }
}
複製代碼

limit:取用前幾個

limit方法能夠對流進行截取,只取用前n個。方法簽名:

Stream<T> limit(long maxSize):獲取Stream流對象中的前n個元素,返回一個新的Stream流對象 複製代碼

參數是一個long型,若是集合當前長度大於參數則進行截取;不然不進行操做。基本使用:

import java.util.stream.Stream;

public class Demo18StreamLimit {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("張無忌", "張三丰", "周芷若");
        Stream<String> result = original.limit(2);
        System.out.println(result.count()); // 2
    }
}
複製代碼

skip:跳過前幾個

若是但願跳過前幾個元素,可使用skip方法獲取一個截取以後的新流:

Stream<T> skip(long n): 跳過Stream流對象中的前n個元素,返回一個新的Stream流對象 複製代碼

若是流的當前長度大於n,則跳過前n個;不然將會獲得一個長度爲0的空流。基本使用:

import java.util.stream.Stream;

public class Demo19StreamSkip {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("張無忌", "張三丰", "周芷若");
        Stream<String> result = original.skip(2);
        System.out.println(result.count()); // 1
    }
}
複製代碼

concat:組合

若是有兩個流,但願合併成爲一個流,那麼可使用Stream接口的靜態方法concat

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b): 把參數列表中的兩個Stream流對象a和b,合併成一個新的Stream流對象 複製代碼

備註:這是一個靜態方法,與java.lang.String當中的concat方法是不一樣的。

該方法的基本使用代碼如:

import java.util.stream.Stream;

public class Demo20StreamConcat {
    public static void main(String[] args) {
        Stream<String> streamA = Stream.of("張無忌");
        Stream<String> streamB = Stream.of("張翠山");
        Stream<String> result = Stream.concat(streamA, streamB);
    }
}
複製代碼

3.5 Stream綜合案例

如今有兩個ArrayList集合存儲隊伍當中的多個成員姓名,要求使用傳統的for循環(或加強for循環)依次進行如下若干操做步驟:

  1. 第一個隊伍只要名字爲3個字的成員姓名;
  2. 第一個隊伍篩選以後只要前3我的;
  3. 第二個隊伍只要姓張的成員姓名;
  4. 第二個隊伍篩選以後不要前2我的;
  5. 將兩個隊伍合併爲一個隊伍;
  6. 打印整個隊伍的姓名信息。

兩個隊伍(集合)的代碼以下:

public class Demo21ArrayListNames {
    public static void main(String[] args) {
        List<String> one = new ArrayList<>();
        one.add("迪麗熱巴");
        one.add("宋遠橋");
        one.add("蘇星河");
        one.add("老子");
        one.add("莊子");
        one.add("孫子");
        one.add("洪七公");

        List<String> two = new ArrayList<>();
        two.add("古力娜扎");
        two.add("張無忌");
        two.add("張三丰");
        two.add("趙麗穎");
        two.add("張二狗");
        two.add("張天愛");
        two.add("張三");
		// ....
    }
}
複製代碼

傳統方式

使用for循環 , 示例代碼:

public class Demo22ArrayListNames {
    public static void main(String[] args) {
        List<String> one = new ArrayList<>();
        // ...

        List<String> two = new ArrayList<>();
        // ...

        // 第一個隊伍只要名字爲3個字的成員姓名;
        List<String> oneA = new ArrayList<>();
        for (String name : one) {
            if (name.length() == 3) {
                oneA.add(name);
            }
        }

        // 第一個隊伍篩選以後只要前3我的;
        List<String> oneB = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            oneB.add(oneA.get(i));
        }

        // 第二個隊伍只要姓張的成員姓名;
        List<String> twoA = new ArrayList<>();
        for (String name : two) {
            if (name.startsWith("張")) {
                twoA.add(name);
            }
        }

        // 第二個隊伍篩選以後不要前2我的;
        List<String> twoB = new ArrayList<>();
        for (int i = 2; i < twoA.size(); i++) {
            twoB.add(twoA.get(i));
        }

        // 將兩個隊伍合併爲一個隊伍;
        List<String> totalNames = new ArrayList<>();
        totalNames.addAll(oneB);
        totalNames.addAll(twoB);        

        // 打印整個隊伍的姓名信息。
        for (String name : totalNames) {
            System.out.println(name);
        }
    }
}
複製代碼

運行結果爲:

宋遠橋
蘇星河
洪七公
張二狗
張天愛
張三
複製代碼

Stream方式

等效的Stream流式處理代碼爲:

public class Demo23StreamNames {
    public static void main(String[] args) {
        List<String> one = new ArrayList<>();
        // ...

        List<String> two = new ArrayList<>();
        // ...

        // 第一個隊伍只要名字爲3個字的成員姓名;
        // 第一個隊伍篩選以後只要前3我的;
        Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);

        // 第二個隊伍只要姓張的成員姓名;
        // 第二個隊伍篩選以後不要前2我的;
        Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("張")).skip(2);

        // 將兩個隊伍合併爲一個隊伍;
        // 根據姓名建立Person對象;
        // 打印整個隊伍的Person對象信息。
        Stream.concat(streamOne, streamTwo).forEach(s->System.out.println(s));
    }
}
複製代碼

運行效果徹底同樣:

宋遠橋
蘇星河
洪七公
張二狗
張天愛
張三
複製代碼

3.6 函數拼接與終結方法

在上述介紹的各類方法中,凡是返回值仍然爲Stream接口的爲函數拼接方法,它們支持鏈式調用;而返回值再也不爲Stream接口的爲終結方法,再也不支持鏈式調用。以下表所示:

方法名 方法做用 方法種類 是否支持鏈式調用
count 統計個數 終結
forEach 逐一處理 終結
filter 過濾 函數拼接
limit 取用前幾個 函數拼接
skip 跳過前幾個 函數拼接
concat 組合 函數拼接

第四章 方法引用

4.1 概述和方法引用符

來看一個簡單的函數式接口以應用Lambda表達式 , 在accept方法中接收字符串 , 目的就是爲了打印顯示字符串 , 那麼經過Lambda來使用它的代碼很簡單:

public class DemoPrintSimple {
    private static void printString(Consumer<String> data, String str) {
        data.accept(str);
    }
    public static void main(String[] args) {
      	printString(s -> System.out.println(s), "Hello World");
    }
}
複製代碼

因爲lambda表達式中,調用了已經實現的println方法 ,可使用方法引用替代lambda表達式.

符號表示 : ::

符號說明 : 雙冒號爲方法引用運算符,而它所在的表達式被稱爲方法引用

**應用場景 : **若是Lambda要表達的函數方案 , 已經存在於某個方法的實現中,那麼則可使用方法引用。

如上例中,System.out對象中有個println(String)方法 , 剛好就是咱們所須要的 , 那麼對於Consumer接口做爲參數,對比下面兩種寫法,徹底等效:

  • Lambda表達式寫法:s -> System.out.println(s); 拿到參數以後經Lambda之手,繼而傳遞給System.out.println方法去處理。
  • 方法引用寫法:System.out::println 直接讓System.out中的println方法來取代Lambda。

**推導與省略 : ** 若是使用Lambda,那麼根據「可推導就是可省略」的原則,無需指定參數類型,也無需指定的重載形式——它們都將被自動推導。而若是使用方法引用,也是一樣能夠根據上下文進行推導。函數式接口是Lambda的基礎,而方法引用是Lambda的簡化形式。

4.2 方法引用簡化

只要「引用」過去就行了:

public class DemoPrintRef {
    private static void printString(Consumer<String> data, String str) {
        data.accept(str);
    }
    public static void main(String[] args) {
      	printString(System.out::println, "HelloWorld");
    }
}
複製代碼

請注意其中的雙冒號::寫法,這被稱爲「方法引用」,而雙冒號是一種新的語法。

4.3 擴展的引用方式

對象名--引用成員方法

這是最多見的一種用法,與上例相同。若是一個類中已經存在了一個成員方法,則能夠經過對象名引用成員方法,代碼爲:

public class DemoMethodRef {
     public static void main(String[] args) {
        String str = "hello";
        printUP(str::toUpperCase);
    }

    public static void printUP(Supplier< String> sup ){
        String apply =sup.get();
        System.out.println(apply);
    }
}
複製代碼

類名--引用靜態方法

因爲在java.lang.Math類中已經存在了靜態方法random,因此當咱們須要經過Lambda來調用該方法時,可使用方法引用 , 寫法是:

public class DemoMethodRef {
  public static void main(String[] args) {
        printRanNum(Math::random);
    }

    public static void printRanNum(Supplier<Double> sup ){
        Double apply =sup.get();
        System.out.println(apply);
    }
}
複製代碼

在這個例子中,下面兩種寫法是等效的:

  • Lambda表達式:n -> Math.abs(n)
  • 方法引用:Math::abs

類--構造引用

因爲構造器的名稱與類名徹底同樣,並不固定。因此構造器引用使用類名稱::new的格式表示。首先是一個簡單的Person類:

public class Person {
    private String name;
    public Person(String name) {
      	this.name = name;
    }
    public String getName() {
      	return name;
    }
}
複製代碼

要使用這個函數式接口,能夠經過方法引用傳遞:

public class Demo09Lambda {
    public static void main(String[] args) {
		String name = "tom";
        Person person = createPerson(Person::new, name);
        System.out.println(person);
        
    }

    public static Person createPerson(Function<String, Person> fun , String name){
        Person p = fun.apply(name);
        return p;

    }
}
複製代碼

在這個例子中,下面兩種寫法是等效的:

  • Lambda表達式:name -> new Person(name)
  • 方法引用:Person::new

數組--構造引用

數組也是Object的子類對象,因此一樣具備構造器,只是語法稍有不一樣。若是對應到Lambda的使用場景中時,須要一個函數式接口:

在應用該接口的時候,能夠經過方法引用傳遞:

public class Demo11ArrayInitRef {   
   public static void main(String[] args) {

        int[] array = createArray(int[]::new, 3);
        System.out.println(array.length);

    }

    public static int[] createArray(Function<Integer , int[]> fun , int n){
        int[] p = fun.apply(n);
        return p;

    }
}
複製代碼

在這個例子中,下面兩種寫法是等效的:

  • Lambda表達式:length -> new int[length]
  • 方法引用:int[]::new

注意 : 方法引用是對Lambda表達式符合特定狀況下的一種縮寫,它使得咱們的Lambda表達式更加的精簡,也能夠理解爲Lambda表達式的縮寫形式 , 同窗們能夠嘗試着 , 將以前使用lambda的地方 , 改寫成方法引用的形式 ,不過要注意的是方法引用只能"引用"已經存在的方法!

後續精彩連載文章, 敬請期待:

  • 歡迎留言,寫下你感興趣的技術話題,霈哥話優先編寫哦~!

觀看更多文章,請移步至 搞後端開發,邊摸魚邊跟他學就夠了,靠譜!🔥


若對你和身邊的朋友有幫助, 抓緊關注 IT霈哥 點贊! 點贊! 點贊! 評論!收藏! 分享給更多的朋友共同窗習交流, 天天持續更新離不開你的支持!

歡迎關注個人B站,未來會發布文章同步視頻~~~ 歡迎關注個人公衆號,獲取更多資料~~~ 歡迎關注個人公衆號,獲取更多資料~~~

相關文章
相關標籤/搜索