JavaSE第22篇:Lambda表達(dá)式、函數(shù)式接口

核心概述:在開(kāi)發(fā)中,我們經(jīng)常使用匿名內(nèi)部類作為實(shí)參傳遞參數(shù),我們可以發(fā)現(xiàn)匿名內(nèi)部類的格式比較繁瑣,那么如何簡(jiǎn)化呢?本篇我們將會(huì)學(xué)習(xí)到Lambda表達(dá)式來(lái)幫助我們解決問(wèn)題。另外我們也將學(xué)習(xí)與Lambda表達(dá)式相關(guān)的函數(shù)式接口,以及Stream流。

第一章:Lambda表達(dá)式

1.1-函數(shù)式編程介紹(了解)

image

在數(shù)學(xué)中,函數(shù)就是有輸入量、輸出量的一套計(jì)算方案,也就是“拿什么東西做什么事情”。相對(duì)而言,面向?qū)ο筮^(guò)分強(qiáng)調(diào)“必須通過(guò)對(duì)象的形式來(lái)做事情”,而函數(shù)式思想則盡量忽略面向?qū)ο蟮膹?fù)雜語(yǔ)法——強(qiáng)調(diào)做什么,而不是以什么形式做。

面向?qū)ο蟮乃枷? 做一件事情,找一個(gè)能解決這個(gè)事情的對(duì)象,調(diào)用對(duì)象的方法,完成事情。

函數(shù)式編程思想: 只要能獲取到結(jié)果,誰(shuí)去做的,怎么做的都不重要,重視的是結(jié)果,不重視過(guò)程 。

1.2-為什么要用Lambda表達(dá)式(了解)

以Runnable為例

當(dāng)需要啟動(dòng)一個(gè)線程去完成任務(wù)時(shí),通常會(huì)通過(guò)java.lang.Runnable接口來(lái)定義任務(wù)內(nèi)容,并使用java.lang.Thread類來(lái)啟動(dòng)該線程。

傳統(tǒng)寫(xiě)法:

public class Demo01Runnable { 
    public static void main(String[] args) { 
        // 匿名內(nèi)部類 
        Runnable task = new Runnable() { 
            @Override 
            public void run() {
              // 覆蓋重寫(xiě)抽象方法 
              System.out.println("多線程任務(wù)執(zhí)行!");
            } 
        };
        new Thread(task).start(); // 啟動(dòng)線程 
    }
}

本著“一切皆對(duì)象”的思想,這種做法是無(wú)可厚非的:首先創(chuàng)建一個(gè) Runnable 接口的匿名內(nèi)部類對(duì)象來(lái)指定任務(wù)內(nèi) 容,再將其交給一個(gè)線程來(lái)啟動(dòng)。

傳統(tǒng)寫(xiě)法分析:

  1. Thread 類需要 Runnable 接口作為參數(shù),其中的抽象 run 方法是用來(lái)指定線程任務(wù)內(nèi)容的核心;
  2. 為了指定 run 的方法體,不得不需要 Runnable 接口的實(shí)現(xiàn)類;
  3. 為了省去定義一個(gè) RunnableImpl 實(shí)現(xiàn)類的麻煩,不得不使用匿名內(nèi)部類;
  4. 必須覆蓋重寫(xiě)抽象 run 方法,所以方法名稱、方法參數(shù)、方法返回值不得不再寫(xiě)一遍,且不能寫(xiě)錯(cuò);
  5. 而實(shí)際上,似乎只有方法體才是關(guān)鍵所在。

編程思想的轉(zhuǎn)變

我們真的希望創(chuàng)建一個(gè)匿名內(nèi)部類對(duì)象嗎?不。我們只是為了做這件事情而不得不創(chuàng)建一個(gè)對(duì)象。我們真正希望做的事情是:將 run 方法體內(nèi)的代碼傳遞給 Thread 類知曉。

傳遞一段代碼——這才是我們真正的目的。而創(chuàng)建對(duì)象只是受限于面向?qū)ο笳Z(yǔ)法而不得不采取的一種手段方式。

那有沒(méi)有更加簡(jiǎn)單的辦法?如果我們將關(guān)注點(diǎn)從“怎么做”回歸到“做什么”的本質(zhì)上,就會(huì)發(fā)現(xiàn)只要能夠更好地達(dá) 到目的,過(guò)程與形式其實(shí)并不重要

Lambda優(yōu)化體驗(yàn)

借助Java 8的全新語(yǔ)法,上述Runnable接口的匿名內(nèi)部類寫(xiě)法可以通過(guò)更簡(jiǎn)單的Lambda表達(dá)式達(dá)到等效:

public class Demo01LambdaRunnable {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("多線程任務(wù)執(zhí)行!")).start(); // 啟動(dòng)線程
    }

這段代碼和剛才的執(zhí)行效果是完全一樣的,可以在1.8或更高的編譯級(jí)別下通過(guò)。從代碼的語(yǔ)義中可以看出:我們啟動(dòng)了一個(gè)線程,而線程任務(wù)的內(nèi)容以一種更加簡(jiǎn)潔的形式被指定。

不再有“不得不創(chuàng)建接口對(duì)象”的束縛,不再有“抽象方法覆蓋重寫(xiě)”的負(fù)擔(dān),就是這么簡(jiǎn)單!

1.3-Lambda格式(重要)

格式

  • Lambda省去面向?qū)ο蟮臈l條框框,格式由3個(gè)部分組成:
    1. 一些參數(shù)
    2. 一個(gè)箭頭
    3. 一段代碼
  • 標(biāo)準(zhǔn)格式: (參數(shù)類型 參數(shù)名稱) ‐> { 代碼語(yǔ)句 }
  • 格式說(shuō)明:
    1. 小括號(hào)內(nèi)的語(yǔ)法與傳統(tǒng)方法參數(shù)列表一致:無(wú)參數(shù)則留空;多個(gè)參數(shù)則用逗號(hào)分隔。
    2. -> 是新引入的語(yǔ)法格式,代表指向動(dòng)作。
    3. 大括號(hào)內(nèi)的語(yǔ)法與傳統(tǒng)方法體要求基本一致。

無(wú)參無(wú)返回值代碼,匿名內(nèi)部類與lambda對(duì)比

代碼:

new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("多線程任務(wù)執(zhí)行!");
            }
}).start();

仔細(xì)分析該代碼中,Runnable接口只有一個(gè)run方法的定義:

  • public abstract void run();

即制定了一種做事情的方案(其實(shí)就是一個(gè)方法):

  • 無(wú)參數(shù):不需要任何條件即可執(zhí)行該方案。
  • 無(wú)返回值:該方案不產(chǎn)生任何結(jié)果。
  • 代碼塊(方法體):該方案的具體執(zhí)行步驟。

同樣的語(yǔ)義體現(xiàn)在Lambda語(yǔ)法中,要更加簡(jiǎn)單:

() -> System.out.println("多線程任務(wù)執(zhí)行!")

代碼說(shuō)明:

  • 前面的一對(duì)小括號(hào)即run方法的參數(shù)(無(wú)),代表不需要任何條件;
  • 中間的一個(gè)箭頭代表將前面的參數(shù)傳遞給后面的代碼;
  • 后面的輸出語(yǔ)句即業(yè)務(wù)邏輯代碼。

有參有返回值代碼,Comparator接口的使用

下面舉例演示java.util.Comparator<T>接口的使用場(chǎng)景代碼,其中的抽象方法定義為:

  • public abstract int compare(T o1, T o2);

當(dāng)需要對(duì)一個(gè)對(duì)象數(shù)組進(jìn)行排序時(shí),Arrays.sort方法需要一個(gè)Comparator接口實(shí)例來(lái)指定排序的規(guī)則。假設(shè)有一個(gè)Person類,含有String nameint age兩個(gè)成員變量:

public class Person { 
    private String name;
    private int age;
    
    // 省略構(gòu)造器、toString方法與Getter Setter 
}

傳統(tǒng)寫(xiě)法

如果使用傳統(tǒng)的代碼對(duì)Person[]數(shù)組進(jìn)行排序,寫(xiě)法如下:

public class Demo05Comparator {
    public static void main(String[] args) {
        // 本來(lái)年齡亂序的對(duì)象數(shù)組
        Person[] array = { new Person("古力娜扎", 19),          new Person("迪麗熱巴", 18),             new Person("馬爾扎哈", 20) };

        // 匿名內(nèi)部類
        Comparator<Person> comp = new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        };
        Arrays.sort(array, comp); // 第二個(gè)參數(shù)為排序規(guī)則,即Comparator接口實(shí)例

        for (Person person : array) {
            System.out.println(person);
        }
    }
}

這種做法在面向?qū)ο蟮乃枷胫?,似乎也是“理所?dāng)然”的。其中Comparator接口的實(shí)例(使用了匿名內(nèi)部類)代表了“按照年齡從小到大”的排序規(guī)則。

代碼分析

下面我們來(lái)搞清楚上述代碼真正要做什么事情。

  • 為了排序,Arrays.sort方法需要排序規(guī)則,即Comparator接口的實(shí)例,抽象方法compare是關(guān)鍵;
  • 為了指定compare的方法體,不得不需要Comparator接口的實(shí)現(xiàn)類;
  • 為了省去定義一個(gè)ComparatorImpl實(shí)現(xiàn)類的麻煩,不得不使用匿名內(nèi)部類;
  • 必須覆蓋重寫(xiě)抽象compare方法,所以方法名稱、方法參數(shù)、方法返回值不得不再寫(xiě)一遍,且不能寫(xiě)錯(cuò);
  • 實(shí)際上,只有參數(shù)和方法體才是關(guān)鍵。

Lambda寫(xiě)法

public class Demo06ComparatorLambda {
    public static void main(String[] args) {
        Person[] array = {
            new Person("古力娜扎", 19),
            new Person("迪麗熱巴", 18),
            new Person("馬爾扎哈", 20) };

        Arrays.sort(array, (Person a, Person b) -> {
            return a.getAge() - b.getAge();
        });

        for (Person person : array) {
            System.out.println(person);
        }
    }
}

省略格式

省略規(guī)則

在Lambda標(biāo)準(zhǔn)格式的基礎(chǔ)上,使用省略寫(xiě)法的規(guī)則為:

  1. 小括號(hào)內(nèi)參數(shù)的類型可以省略;
  2. 如果小括號(hào)內(nèi)有且僅有一個(gè)參,則小括號(hào)可以省略;
  3. 如果大括號(hào)內(nèi)有且僅有一個(gè)語(yǔ)句,則無(wú)論是否有返回值,都可以省略大括號(hào)、return關(guān)鍵字及語(yǔ)句分號(hào)。

1.4-Lambda的前提條件(了解)

Lambda的語(yǔ)法非常簡(jiǎn)潔,完全沒(méi)有面向?qū)ο髲?fù)雜的束縛。但是使用時(shí)有幾個(gè)問(wèn)題需要特別注意:

  1. 使用Lambda必須具有接口,且要求接口中有且僅有一個(gè)抽象方法。
    無(wú)論是JDK內(nèi)置的RunnableComparator接口還是自定義的接口,只有當(dāng)接口中的抽象方法存在且唯一時(shí),才可以使用Lambda。
  2. 使用Lambda必須具有接口作為方法參數(shù)。
    也就是方法的參數(shù)或局部變量類型必須為L(zhǎng)ambda對(duì)應(yīng)的接口類型,才能使用Lambda作為該接口的實(shí)例。

有且僅有一個(gè)抽象方法的接口,稱為“函數(shù)式接口”。

第二章:函數(shù)式接口

2.1-概述(了解)

介紹

函數(shù)式接口在Java中是指:有且僅有一個(gè)抽象方法的接口

函數(shù)式接口,即適用于函數(shù)式編程場(chǎng)景的接口。而Java中的函數(shù)式編程體現(xiàn)就是Lambda,所以函數(shù)式接口就是可以適用于Lambda使用的接口。只有確保接口中有且僅有一個(gè)抽象方法,Java中的Lambda才能順利地進(jìn)行推導(dǎo)。

從應(yīng)用層面來(lái)講,Java中的Lambda可以看做是匿名內(nèi)部類的簡(jiǎn)化格式。

格式

只要確保接口中有且僅有一個(gè)抽象方法即可:

修飾符 interface 接口名稱 {
    public abstract 返回值類型 方法名稱(可選參數(shù)信息);
    // 其他非抽象方法內(nèi)容
}

public abstract 可以省略

FunctionalInterface注解

@Override注解的作用類似,Java 8中專門(mén)為函數(shù)式接口引入了一個(gè)新的注解:@FunctionalInterface。該注解可用于一個(gè)接口的定義上:

@FunctionalInterface
public interface MyFunctionalInterface {
    void myMethod();
}

一旦使用該注解來(lái)定義接口,編譯器將會(huì)強(qiáng)制檢查該接口是否確實(shí)有且僅有一個(gè)抽象方法,否則將會(huì)報(bào)錯(cuò)。不過(guò),即使不使用該注解,只要滿足函數(shù)式接口的定義,這仍然是一個(gè)函數(shù)式接口,使用起來(lái)都一樣。

2.2-常用的函數(shù)式接口(重點(diǎn))

JDK提供了大量常用的函數(shù)式接口以豐富Lambda的典型使用場(chǎng)景,它們主要在java.util.function包中被提供。下面是最簡(jiǎn)單的幾個(gè)接口及使用示例。

Supplier接口

java.util.function.Supplier<T>接口,它意味著"供給" , 對(duì)應(yīng)的Lambda表達(dá)式需要“對(duì)外提供”一個(gè)符合泛型類型的對(duì)象數(shù)據(jù)。

抽象方法 : get

僅包含一個(gè)無(wú)參的方法:T get()。用來(lái)獲取一個(gè)泛型參數(shù)指定類型的對(duì)象數(shù)據(jù)。

public class Demo08Supplier {
    private static String getString(Supplier<String> function) {
        return function.get();
    }

    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        System.out.println(getString(() -> msgA + msgB));
    }
}

求數(shù)組元素最大值

使用Supplier接口作為方法參數(shù)類型,通過(guò)Lambda表達(dá)式求出int數(shù)組中的最大值。提示:接口的泛型請(qǐng)使用java.lang.Integer類。

代碼示例:

public class DemoIntArray {
    public static void main(String[] args) {
        int[] array = { 10, 20, 100, 30, 40, 50 };
        printMax(() -> {
            int max = array[0];
            for (int i = 1; i < array.length; i++) {
                if (array[i] > max) {              
                    max = array[i];
                }
            }
            return max;
        });
    }

    private static void printMax(Supplier<Integer> supplier) {
        int max = supplier.get();
        System.out.println(max);
    }
}

Consumer接口

java.util.function.Consumer<T>接口則正好相反,它不是生產(chǎn)一個(gè)數(shù)據(jù),而是消費(fèi)一個(gè)數(shù)據(jù),其數(shù)據(jù)類型由泛型參數(shù)決定。

抽象方法:accept

Consumer接口中包含抽象方法void accept(T t),意為消費(fèi)一個(gè)指定泛型的數(shù)據(jù)?;臼褂萌纾?/p>

import java.util.function.Consumer;

public class Demo09Consumer {
    private static void consumeString(Consumer<String> function , String str) {
        function.accept(str);
    }

    public static void main(String[] args) {
        consumeString(s -> System.out.println(s));
      
    }
}

Function接口

java.util.function.Function<T,R>接口用來(lái)根據(jù)一個(gè)類型的數(shù)據(jù)得到另一個(gè)類型的數(shù)據(jù),前者稱為前置條件,后者稱為后置條件。有進(jìn)有出,所以稱為“函數(shù)Function”。

抽象方法:apply

Function接口中最主要的抽象方法為:R apply(T t),根據(jù)類型T的參數(shù)獲取類型R的結(jié)果。使用的場(chǎng)景例如:將String類型轉(zhuǎn)換為Integer類型。

public class Demo11FunctionApply {
    private static void method(Function<String, Integer> function, Str str) {
        int num = function.apply(str);
        System.out.println(num + 20);
    }

    public static void main(String[] args) {
        method(s -> Integer.parseInt(s) , "10");
    }
}

Predicate接口

有時(shí)候我們需要對(duì)某種類型的數(shù)據(jù)進(jìn)行判斷,從而得到一個(gè)boolean值結(jié)果。這時(shí)可以使用java.util.function.Predicate<T>接口。

抽象方法:test

Predicate接口中包含一個(gè)抽象方法:boolean test(T t)。用于條件判斷的場(chǎng)景,條件判斷的標(biāo)準(zhǔn)是傳入的Lambda表達(dá)式邏輯,只要字符串長(zhǎng)度大于5則認(rèn)為很長(zhǎng)。

public class Demo15PredicateTest {
    private static void method(Predicate<String> predicate,String str) {
        boolean veryLong = predicate.test(str);
        System.out.println("字符串很長(zhǎng)嗎:" + veryLong);
    }

    public static void main(String[] args) {
        method(s -> s.length() > 5, "HelloWorld");
    }
}

第三章:Stream流

在Java 8中,得益于Lambda所帶來(lái)的函數(shù)式編程,引入了一個(gè)全新的Stream概念,用于解決已有集合類庫(kù)既有的弊端。

3.1-為什么要用Stream流(了解)

傳統(tǒng)集合的多步遍歷代碼

幾乎所有的集合(如Collection接口或Map接口等)都支持直接或間接的遍歷操作。而當(dāng)我們需要對(duì)集合中的元素進(jìn)行操作的時(shí)候,除了必需的添加、刪除、獲取外,最典型的就是集合遍歷。例如:

public class Demo10ForEach {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("張無(wú)忌");
        list.add("周芷若");
        list.add("趙敏");
        list.add("張強(qiáng)");
        list.add("張三豐");
        for (String name : list) {
            System.out.println(name);
        }
    }  
}

這是一段非常簡(jiǎn)單的集合遍歷操作:對(duì)集合中的每一個(gè)字符串都進(jìn)行打印輸出操作。

循環(huán)遍歷的弊端

Java 8的Lambda讓我們可以更加專注于做什么(What),而不是怎么做(How),這點(diǎn)此前已經(jīng)結(jié)合內(nèi)部類進(jìn)行了對(duì)比說(shuō)明?,F(xiàn)在,我們仔細(xì)體會(huì)一下上例代碼,可以發(fā)現(xiàn):

  • for循環(huán)的語(yǔ)法就是“怎么做
  • for循環(huán)的循環(huán)體才是“做什么

為什么使用循環(huán)?因?yàn)橐M(jìn)行遍歷。但循環(huán)是遍歷的唯一方式嗎?遍歷是指每一個(gè)元素逐一進(jìn)行處理,而并不是從第一個(gè)到最后一個(gè)順次處理的循環(huán)。前者是目的,后者是方式。

試想一下,如果希望對(duì)集合中的元素進(jìn)行篩選過(guò)濾:

  1. 將集合A根據(jù)條件一過(guò)濾為子集B
  2. 然后再根據(jù)條件二過(guò)濾為子集C

那怎么辦?在Java 8之前的做法可能為:

這段代碼中含有三個(gè)循環(huán),每一個(gè)作用不同:

  1. 首先篩選所有姓張的人;
  2. 然后篩選名字有三個(gè)字的人;
  3. 最后進(jìn)行對(duì)結(jié)果進(jìn)行打印輸出。
public class Demo11NormalFilter {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("張無(wú)忌");
        list.add("周芷若");
        list.add("趙敏");
        list.add("張強(qiáng)");
        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);
        }
    }
}

每當(dāng)我們需要對(duì)集合中的元素進(jìn)行操作的時(shí)候,總是需要進(jìn)行循環(huán)、循環(huán)、再循環(huán)。這是理所當(dāng)然的么?不是。循環(huán)是做事情的方式,而不是目的。另一方面,使用線性循環(huán)就意味著只能遍歷一次。如果希望再次遍歷,只能再使用另一個(gè)循環(huán)從頭開(kāi)始。

那,Lambda的衍生物Stream能給我們帶來(lái)怎樣更加優(yōu)雅的寫(xiě)法呢?

Stream的更優(yōu)寫(xiě)法

下面來(lái)看一下借助Java 8的Stream API,什么才叫優(yōu)雅:

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

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

直接閱讀代碼的字面意思即可完美展示無(wú)關(guān)邏輯方式的語(yǔ)義:獲取流、過(guò)濾姓張、過(guò)濾長(zhǎng)度為3、逐一打印。代碼中并沒(méi)有體現(xiàn)使用線性循環(huán)或是其他任何算法進(jìn)行遍歷,我們真正要做的事情內(nèi)容被更好地體現(xiàn)在代碼中。

3.2-流式思想(了解)

注意:請(qǐng)暫時(shí)忘記對(duì)傳統(tǒng)IO流的固有印象!

整體來(lái)看,流式思想類似于工廠車(chē)間的“生產(chǎn)流水線”。

image

當(dāng)需要對(duì)多個(gè)元素進(jìn)行操作(特別是多步操作)的時(shí)候,考慮到性能及便利性,我們應(yīng)該首先拼好一個(gè)“模型”步驟 方案,然后再按照方案去執(zhí)行它。

image

這張圖中展示了過(guò)濾、映射、跳過(guò)、計(jì)數(shù)等多步操作,這是一種集合元素的處理方案,而方案就是一種“函數(shù)模型”。圖中的每一個(gè)方框都是一個(gè)“流”,調(diào)用指定的方法,可以從一個(gè)流模型轉(zhuǎn)換為另一個(gè)流模型。而最右側(cè)的數(shù)字3是最終結(jié)果。

3.3-獲取流的方式(重點(diǎn))

java.util.stream.Stream<T>是Java 8新加入的最常用的流接口。(這并不是一個(gè)函數(shù)式接口。)

獲取一個(gè)流非常簡(jiǎn)單,有以下幾種常用的方式:

  • 所有的Collection集合都可以通過(guò)stream默認(rèn)方法獲取流;
  • Stream接口的靜態(tài)方法of可以獲取數(shù)組對(duì)應(yīng)的流。

方式1 : 根據(jù)Collection獲取流

首先,java.util.Collection接口中加入了default方法stream用來(lái)獲取流,所以其所有實(shí)現(xiàn)類均可獲取流。

import java.util.*;
import java.util.stream.Stream;
/*
    獲取Stream流的方式

    1.Collection中 方法
        Stream stream()
    2.Stream接口 中靜態(tài)方法
        of(T...t) 向Stream中添加多個(gè)數(shù)據(jù)
 */
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: 根據(jù)數(shù)組獲取流

如果使用的不是集合或映射而是數(shù)組,由于數(shù)組對(duì)象不可能添加默認(rèn)方法,所以Stream接口中提供了靜態(tài)方法of,使用很簡(jiǎn)單:

import java.util.stream.Stream;

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

of方法的參數(shù)其實(shí)是一個(gè)可變參數(shù),所以支持?jǐn)?shù)組。

3.4-常用方法(重點(diǎn))

流模型的操作很豐富,這里介紹一些常用的API。這些方法可以被分成兩種:

  • 終結(jié)方法:返回值類型不再是Stream接口自身類型的方法,因此不再支持類似StringBuilder那樣的鏈?zhǔn)秸{(diào)用。本小節(jié)中,終結(jié)方法包括countforEach方法。
  • 非終結(jié)方法:返回值類型仍然是Stream接口自身類型的方法,因此支持鏈?zhǔn)秸{(diào)用。(除了終結(jié)方法外,其余方法均為非終結(jié)方法。)

更多方法,請(qǐng)自行參考API文檔。

逐一處理方法:forEach

雖然方法名字叫 forEach ,但是與for循環(huán)中的“for-each”昵稱不同。

void forEach(Consumer<? super T> action);

該方法接收一個(gè) Consumer 接口函數(shù),會(huì)將每一個(gè)流元素交給該函數(shù)進(jìn)行處理。

java.util.function.Consumer<T>接口是一個(gè)消費(fèi)型接口。
Consumer接口中包含抽象方法void accept(T t),意為消費(fèi)一個(gè)指定泛型的數(shù)據(jù)。

基本使用

public class Test01 {
  public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("張三");
    list.add("李四");
    list.add("王五");
    list.stream().forEach(name-> System.out.println(name));
  }
}

過(guò)濾方法:filter

image

可以通過(guò) filter 方法將一個(gè)流轉(zhuǎn)換成另一個(gè)子集流。方法簽名:

Stream<T> filter(Predicate<? super T> predicate);

該接口接收一個(gè) Predicate 函數(shù)式接口參數(shù)(可以是一個(gè)Lambda或方法引用)作為篩選條件。

復(fù)習(xí)Predicate接口

java.util.stream.Predicate函數(shù)式接口,其中唯一的抽象方法為:

boolean test(T t);

該方法將會(huì)產(chǎn)生一個(gè)boolean值結(jié)果,代表指定的條件是否滿足。如果結(jié)果為true,那么Stream流的 filter 方法 將會(huì)留用元素;如果結(jié)果為false,那么 filter 方法將會(huì)舍棄元素。

基本使用

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

public class Test02 {
  public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("張三");
    list.add("李四");
    list.add("張三豐");
    list.add("王五");
    list.add("張無(wú)忌");
    list.stream()
            .filter(name->name.startsWith("張"))
            .forEach(name-> System.out.println(name));
  }
}

在這里通過(guò)Lambda表達(dá)式來(lái)指定了篩選的條件:必須姓張。

映射方法:map

image

如果需要將流中的元素映射到另一個(gè)流中,可以使用 map 方法。方法簽名:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

該接口需要一個(gè) Function 函數(shù)式接口參數(shù),可以將當(dāng)前流中的T類型數(shù)據(jù)轉(zhuǎn)換為另一種R類型的流。

復(fù)習(xí)Function接口

此前我們已經(jīng)學(xué)習(xí)過(guò) java.util.stream.Function 函數(shù)式接口,其中唯一的抽象方法為:

R apply(T t);

這可以將一種T類型轉(zhuǎn)換成為R類型,而這種轉(zhuǎn)換的動(dòng)作,就稱為“映射”

基本使用

public class Test03 {
  public static void main(String[] args) {
    String[]strs = {"11","22","33","44"};
    Stream.of(strs)
            .map((s -> Integer.parseInt(s)))
            .forEach(i-> System.out.println(i+2));
  }
}

這段代碼中, map 方法的參數(shù)通過(guò)方法引用,將字符串類型轉(zhuǎn)換成為了int類型(并自動(dòng)裝箱為 Integer 類對(duì) 象)。

統(tǒng)計(jì)個(gè)數(shù)方法:count

正如舊集合 Collection 當(dāng)中的 size 方法一樣,流提供 count 方法來(lái)數(shù)一數(shù)其中的元素個(gè)數(shù):

long count();

該方法返回一個(gè)long值代表元素個(gè)數(shù)(不再像舊集合那樣是int值)?;臼褂茫?/p>

  public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("張三");
    list.add("李四");
    list.add("張三豐");
    list.add("王五");
    list.add("張無(wú)忌");
    long i = list.stream()
            .filter(name->name.startsWith("張"))
            .count();
    System.out.println(i);//3
  }

取用前幾個(gè)方法:limit

image

limit 方法可以對(duì)流進(jìn)行截取,只取用前n個(gè)。方法簽名:

Stream<T> limit(long maxSize);

參數(shù)是一個(gè)long型,如果集合當(dāng)前長(zhǎng)度大于參數(shù)則進(jìn)行截??;否則不進(jìn)行操作?;臼褂茫?/p>

public class Test06 {
  public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("張三");
    list.add("李四");
    list.add("張三豐");
    list.add("王五");
    list.add("張無(wú)忌");
    list.stream()
            .filter(name->name.startsWith("張"))
            .limit(2)
            .forEach(name-> System.out.println(name));
  }
}
// 結(jié)果-張三、張三豐

跳過(guò)前幾個(gè)方法:skip

image

如果希望跳過(guò)前幾個(gè)元素,可以使用 skip 方法獲取一個(gè)截取之后的新流:

Stream<T> skip(long n);

如果流的當(dāng)前長(zhǎng)度大于n,則跳過(guò)前n個(gè);否則將會(huì)得到一個(gè)長(zhǎng)度為0的空流。基本使用:

public class Test07 {
  public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("張三");
    list.add("李四");
    list.add("張三豐");
    list.add("王五");
    list.add("張無(wú)忌");
    list.stream()
            .filter(name->name.startsWith("張"))
            .skip(2)
            .forEach(name-> System.out.println(name));
  }
}
// 結(jié)果:張無(wú)忌

組合方法,concat

如果有兩個(gè)流,希望合并成為一個(gè)流,那么可以使用 Stream 接口的靜態(tài)方法 concat :

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

這是一個(gè)靜態(tài)方法,與 java.lang.String 當(dāng)中的 concat 方法是不同的。

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

public class Test08 {
  public static void main(String[] args) {
    Stream<String>  s1 = Stream.of("張三","李四");
    Stream<String>  s2 = Stream.of("王五","趙六");
    Stream.concat(s1,s2).forEach(name-> System.out.println(name));
  }
}
// 結(jié)果:張三、李四、王五、趙六

流轉(zhuǎn)集合方法,collect

從Stream流對(duì)象轉(zhuǎn)成集合對(duì)象,使用Stream接口方法collect:

public class StreamDemo06 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();

        list.add("張無(wú)忌");
        list.add("周芷若");
        list.add("張三豐");

        Stream<String> stream = list.stream();
        //Stream流對(duì)象collect()傳遞Collectors靜態(tài)方法toList() 流對(duì)象元素轉(zhuǎn)成集合
        List<String> newList =  stream.filter( s->s.startsWith("張")).collect(Collectors.toList());
        System.out.println(newList);
    }
}

3.5-綜合案例(練習(xí))

需求

現(xiàn)在有兩個(gè)ArrayList集合存儲(chǔ)隊(duì)伍當(dāng)中的多個(gè)成員姓名,要求使用傳統(tǒng)的for循環(huán)(或增強(qiáng)for循環(huán))依次進(jìn)行以下若干操作步驟:

  1. 第一個(gè)隊(duì)伍只要名字為3個(gè)字的成員姓名;
  2. 第一個(gè)隊(duì)伍篩選之后只要前3個(gè)人;
  3. 第二個(gè)隊(duì)伍只要姓張的成員姓名;
  4. 第二個(gè)隊(duì)伍篩選之后不要前2個(gè)人;
  5. 將兩個(gè)隊(duì)伍合并為一個(gè)隊(duì)伍;
  6. 打印整個(gè)隊(duì)伍的姓名信息。

兩個(gè)隊(duì)伍(集合)的代碼如下:

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

        List<String> two = new ArrayList<>();
        two.add("古力娜扎");
        two.add("張無(wú)忌");
        two.add("張三豐");
        two.add("趙麗穎");
        two.add("張二狗");
        two.add("張?zhí)鞇?ài)");
        two.add("張三");
        // ....
    }
}

傳統(tǒng)方式

使用for循環(huán) , 示例代碼:

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

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

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

        // 第一個(gè)隊(duì)伍篩選之后只要前3個(gè)人;
        List<String> oneB = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            oneB.add(oneA.get(i));
        }

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

        // 第二個(gè)隊(duì)伍篩選之后不要前2個(gè)人;
        List<String> twoB = new ArrayList<>();
        for (int i = 2; i < twoA.size(); i++) {
            twoB.add(twoA.get(i));
        }

        // 將兩個(gè)隊(duì)伍合并為一個(gè)隊(duì)伍;
        List<String> totalNames = new ArrayList<>();
        totalNames.addAll(oneB);
        totalNames.addAll(twoB);        

        // 打印整個(gè)隊(duì)伍的姓名信息。
        for (String name : totalNames) {
            System.out.println(name);
        }
    }
}

運(yùn)行結(jié)果為:

宋遠(yuǎn)橋
蘇星河
洪七公
張二狗
張?zhí)鞇?ài)
張三

Stream方式

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

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

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

        // 第一個(gè)隊(duì)伍只要名字為3個(gè)字的成員姓名;
        // 第一個(gè)隊(duì)伍篩選之后只要前3個(gè)人;
        Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);

        // 第二個(gè)隊(duì)伍只要姓張的成員姓名;
        // 第二個(gè)隊(duì)伍篩選之后不要前2個(gè)人;
        Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("張")).skip(2);

        // 將兩個(gè)隊(duì)伍合并為一個(gè)隊(duì)伍;
        // 根據(jù)姓名創(chuàng)建Person對(duì)象;
        // 打印整個(gè)隊(duì)伍的Person對(duì)象信息。
        Stream.concat(streamOne, streamTwo).forEach(s->System.out.println(s));
    }
}

運(yùn)行效果完全一樣:

宋遠(yuǎn)橋
蘇星河
洪七公
張二狗
張?zhí)鞇?ài)
張三

3.6-函數(shù)拼接與終結(jié)方法(了解)

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容