線程池、Lambda表達(dá)式

內(nèi)容預(yù)覽

  • 等待與喚醒案例
  • 線程池
  • Lambda表達(dá)式

等待喚醒機(jī)制

1.1 線程間通信

概念:多個(gè)線程在處理同一個(gè)資源,但是處理的動(dòng)作(線程的任務(wù))卻不相同。

比如:線程A用來生成包子的,線程B用來吃包子的,包子可以理解為同一資源,線程A與線程B處理的動(dòng)作,一個(gè)是生產(chǎn),一個(gè)是消費(fèi),那么線程A與線程B之間就存在線程通信問題。

為什么要處理線程間通信:

多個(gè)線程并發(fā)執(zhí)行時(shí), 在默認(rèn)情況下CPU是隨機(jī)切換線程的,當(dāng)我們需要多個(gè)線程來共同完成一件任務(wù),并且我們希望他們有規(guī)律的執(zhí)行, 那么多線程之間需要一些協(xié)調(diào)通信,以此來幫我們達(dá)到多線程共同操作一份數(shù)據(jù)。

如何保證線程間通信有效利用資源:

多個(gè)線程在處理同一個(gè)資源,并且任務(wù)不同時(shí),需要線程通信來幫助解決線程之間對(duì)同一個(gè)變量的使用或操作。 就是多個(gè)線程在操作同一份數(shù)據(jù)時(shí), 避免對(duì)同一共享變量的爭(zhēng)奪。也就是我們需要通過一定的手段使各個(gè)線程能有效的利用資源。而這種手段即—— 等待喚醒機(jī)制。

1.2 等待喚醒機(jī)制

什么是等待喚醒機(jī)制

這是多個(gè)線程間的一種協(xié)作機(jī)制。談到線程我們經(jīng)常想到的是線程間的競(jìng)爭(zhēng)(race),比如去爭(zhēng)奪鎖,但這并不是故事的全部,線程間也會(huì)有協(xié)作機(jī)制。就好比在公司里你和你的同事們,你們可能存在在晉升時(shí)的競(jìng)爭(zhēng),但更多時(shí)候你們更多是一起合作以完成某些任務(wù)。

就是在一個(gè)線程進(jìn)行了規(guī)定操作后,就進(jìn)入等待狀態(tài)(wait()), 等待其他線程執(zhí)行完他們的指定代碼過后 再將其喚醒(notify());在有多個(gè)線程進(jìn)行等待時(shí), 如果需要,可以使用 notifyAll()來喚醒所有的等待線程。

wait/notify 就是線程間的一種協(xié)作機(jī)制。

等待喚醒中的方法

等待喚醒機(jī)制就是用于解決線程間通信的問題的,使用到的3個(gè)方法的含義如下:

  1. wait:線程不再活動(dòng),不再參與調(diào)度,進(jìn)入 wait set 中,因此不會(huì)浪費(fèi) CPU 資源,也不會(huì)去競(jìng)爭(zhēng)鎖了,這時(shí)的線程狀態(tài)即是 WAITING。它還要等著別的線程執(zhí)行一個(gè)特別的動(dòng)作,也即是“通知(notify)”在這個(gè)對(duì)象上等待的線程從wait set 中釋放出來,重新進(jìn)入到調(diào)度隊(duì)列(ready queue)中
  2. notify:則選取所通知對(duì)象的 wait set 中的一個(gè)線程釋放;例如,餐館有空位置后,等候就餐最久的顧客最先入座。
  3. notifyAll:則釋放所通知對(duì)象的 wait set 上的全部線程。

注意:

哪怕只通知了一個(gè)等待的線程,被通知線程也不能立即恢復(fù)執(zhí)行,因?yàn)樗?dāng)初中斷的地方是在同步塊內(nèi),而此刻它已經(jīng)不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它線程的競(jìng)爭(zhēng)),成功后才能在當(dāng)初調(diào)用 wait 方法之后的地方恢復(fù)執(zhí)行。

總結(jié)如下:

  • 如果能獲取鎖,線程就從 WAITING 狀態(tài)變成 RUNNABLE 狀態(tài);
  • 否則,從 wait set 出來,又進(jìn)入 entry set,線程就從 WAITING 狀態(tài)又變成 BLOCKED 狀態(tài)

調(diào)用wait和notify方法需要注意的細(xì)節(jié)

  1. wait方法與notify方法必須要由同一個(gè)鎖對(duì)象調(diào)用。因?yàn)椋簩?duì)應(yīng)的鎖對(duì)象可以通過notify喚醒使用同一個(gè)鎖對(duì)象調(diào)用的wait方法后的線程。
  2. wait方法與notify方法是屬于Object類的方法的。因?yàn)椋烘i對(duì)象可以是任意對(duì)象,而任意對(duì)象的所屬類都是繼承了Object類的。
  3. wait方法與notify方法必須要在同步代碼塊或者是同步函數(shù)中使用。因?yàn)椋罕仨氁ㄟ^鎖對(duì)象調(diào)用這2個(gè)方法。

1.3 生產(chǎn)者與消費(fèi)者問題

等待喚醒機(jī)制其實(shí)就是經(jīng)典的“生產(chǎn)者與消費(fèi)者”的問題。

就拿生產(chǎn)包子消費(fèi)包子來說等待喚醒機(jī)制如何有效利用資源:

包子鋪線程生產(chǎn)包子,吃貨線程消費(fèi)包子。當(dāng)包子沒有時(shí)(包子狀態(tài)為false),吃貨線程等待,包子鋪線程生產(chǎn)包子(即包子狀態(tài)為true),并通知吃貨線程(解除吃貨的等待狀態(tài)),因?yàn)橐呀?jīng)有包子了,那么包子鋪線程進(jìn)入等待狀態(tài)。接下來,吃貨線程能否進(jìn)一步執(zhí)行則取決于鎖的獲取情況。如果吃貨獲取到鎖,那么就執(zhí)行吃包子動(dòng)作,包子吃完(包子狀態(tài)為false),并通知包子鋪線程(解除包子鋪的等待狀態(tài)),吃貨線程進(jìn)入等待。包子鋪線程能否進(jìn)一步執(zhí)行則取決于鎖的獲取情況。

代碼演示:

包子資源類:

public class BaoZi {
     String  pier ;
     String  xianer ;
     boolean  flag = false ;//包子資源 是否存在  包子資源狀態(tài)
}

吃貨線程類:

public class ChiHuo extends Thread{
    private BaoZi bz;

    public ChiHuo(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }
    @Override
    public void run() {
        while(true){
            synchronized (bz){
                if(bz.flag == false){//沒包子
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("吃貨正在吃"+bz.pier+bz.xianer+"包子");
                bz.flag = false;
                bz.notify();
            }
        }
    }
}

包子鋪線程類:

public class BaoZiPu extends Thread {

    private BaoZi bz;

    public BaoZiPu(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }

    @Override
    public void run() {
        int count = 0;
        //造包子
        while(true){
            //同步
            synchronized (bz){
                if(bz.flag == true){//包子資源  存在
                    try {

                        bz.wait();

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 沒有包子  造包子
                System.out.println("包子鋪開始做包子");
                if(count%2 == 0){
                    // 冰皮  五仁
                    bz.pier = "冰皮";
                    bz.xianer = "五仁";
                }else{
                    // 薄皮  牛肉大蔥
                    bz.pier = "薄皮";
                    bz.xianer = "牛肉大蔥";
                }
                count++;

                bz.flag=true;
                System.out.println("包子造好了:"+bz.pier+bz.xianer);
                System.out.println("吃貨來吃吧");
                //喚醒等待線程 (吃貨)
                bz.notify();
            }
        }
    }
}

測(cè)試類:

public class Demo {
    public static void main(String[] args) {
        //等待喚醒案例
        BaoZi bz = new BaoZi();

        ChiHuo ch = new ChiHuo("吃貨",bz);
        BaoZiPu bzp = new BaoZiPu("包子鋪",bz);

        ch.start();
        bzp.start();
    }
}

執(zhí)行效果:

包子鋪開始做包子
包子造好了:冰皮五仁
吃貨來吃吧
吃貨正在吃冰皮五仁包子
包子鋪開始做包子
包子造好了:薄皮牛肉大蔥
吃貨來吃吧
吃貨正在吃薄皮牛肉大蔥包子
包子鋪開始做包子
包子造好了:冰皮五仁
吃貨來吃吧
吃貨正在吃冰皮五仁包子

線程池

2.1 線程池思想概述

我們使用線程的時(shí)候就去創(chuàng)建一個(gè)線程,這樣實(shí)現(xiàn)起來非常簡(jiǎn)便,但是就會(huì)有一個(gè)問題:

如果并發(fā)的線程數(shù)量很多,并且每個(gè)線程都是執(zhí)行一個(gè)時(shí)間很短的任務(wù)就結(jié)束了,這樣頻繁創(chuàng)建線程就會(huì)大大降低系統(tǒng)的效率,因?yàn)轭l繁創(chuàng)建線程和銷毀線程需要時(shí)間。

那么有沒有一種辦法使得線程可以復(fù)用,就是執(zhí)行完一個(gè)任務(wù),并不被銷毀,而是可以繼續(xù)執(zhí)行其他的任務(wù)?

在Java中可以通過線程池來達(dá)到這樣的效果。今天我們就來詳細(xì)講解一下Java的線程池。

2.2 線程池概念

  • 線程池:其實(shí)就是一個(gè)容納多個(gè)線程的容器,其中的線程可以反復(fù)使用,省去了頻繁創(chuàng)建線程對(duì)象的操作,無(wú)需反復(fù)創(chuàng)建線程而消耗過多資源。

由于線程池中有很多操作都是與優(yōu)化資源相關(guān)的,我們?cè)谶@里就不多贅述。我們通過一張圖來了解線程池的工作原理:

合理利用線程池能夠帶來三個(gè)好處:

  1. 降低資源消耗。減少了創(chuàng)建和銷毀線程的次數(shù),每個(gè)工作線程都可以被重復(fù)利用,可執(zhí)行多個(gè)任務(wù)。
  2. 提高響應(yīng)速度。當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要的等到線程創(chuàng)建就能立即執(zhí)行。
  3. 提高線程的可管理性。可以根據(jù)系統(tǒng)的承受能力,調(diào)整線程池中工作線線程的數(shù)目,防止因?yàn)橄倪^多的內(nèi)存,而把服務(wù)器累趴下(每個(gè)線程需要大約1MB內(nèi)存,線程開的越多,消耗的內(nèi)存也就越大,最后死機(jī))。

2.3 線程池的使用

Java里面線程池的頂級(jí)接口是java.util.concurrent.Executor,但是嚴(yán)格意義上講Executor并不是一個(gè)線程池,而只是一個(gè)執(zhí)行線程的工具。真正的線程池接口是java.util.concurrent.ExecutorService

要配置一個(gè)線程池是比較復(fù)雜的,尤其是對(duì)于線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優(yōu)的,因此在java.util.concurrent.Executors線程工廠類里面提供了一些靜態(tài)工廠,生成一些常用的線程池。官方建議使用Executors工程類來創(chuàng)建線程池對(duì)象。

Executors類中有個(gè)創(chuàng)建線程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回線程池對(duì)象。(創(chuàng)建的是有界線程池,也就是池中的線程個(gè)數(shù)可以指定最大數(shù)量)

獲取到了一個(gè)線程池ExecutorService 對(duì)象,那么怎么使用呢,在這里定義了一個(gè)使用線程池對(duì)象的方法如下:

  • public Future<?> submit(Runnable task):獲取線程池中的某一個(gè)線程對(duì)象,并執(zhí)行

    Future接口:用來記錄線程任務(wù)執(zhí)行完畢后產(chǎn)生的結(jié)果。線程池創(chuàng)建與使用。

使用線程池中線程對(duì)象的步驟:

  1. 創(chuàng)建線程池對(duì)象。
  2. 創(chuàng)建Runnable接口子類對(duì)象。(task)
  3. 提交Runnable接口子類對(duì)象。(take task)
  4. 關(guān)閉線程池(一般不做)。

Runnable實(shí)現(xiàn)類代碼:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我要一個(gè)教練");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教練來了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,交完后,教練回到了游泳池");
    }
}

線程池測(cè)試類:

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 創(chuàng)建線程池對(duì)象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2個(gè)線程對(duì)象
        // 創(chuàng)建Runnable實(shí)例對(duì)象
        MyRunnable r = new MyRunnable();

        //自己創(chuàng)建線程對(duì)象的方式
        // Thread t = new Thread(r);
        // t.start(); ---> 調(diào)用MyRunnable中的run()

        // 從線程池中獲取線程對(duì)象,然后調(diào)用MyRunnable中的run()
        service.submit(r);
        // 再獲取個(gè)線程對(duì)象,調(diào)用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法調(diào)用結(jié)束后,程序并不終止,是因?yàn)榫€程池控制了線程的關(guān)閉。
        // 將使用完的線程又歸還到了線程池中
        // 關(guān)閉線程池
        //service.shutdown();
    }
}

Lambda表達(dá)式

3.1 函數(shù)式編程思想概述

[圖片上傳失敗...(image-237bd1-1593776329824)]

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

面向?qū)ο蟮乃枷?

? 做一件事情,找一個(gè)能解決這個(gè)事情的對(duì)象,調(diào)用對(duì)象的方法,完成事情.

函數(shù)式編程思想:

? 只要能獲取到結(jié)果,誰(shuí)去做的,怎么做的都不重要,重視的是結(jié)果,不重視過程

3.2 冗余的Runnable代碼

傳統(tǒng)寫法

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

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

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

代碼分析

對(duì)于Runnable的匿名內(nèi)部類用法,可以分析出幾點(diǎn)內(nèi)容:

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

3.3 編程思想轉(zhuǎn)換

做什么,而不是怎么做

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

傳遞一段代碼——這才是我們真正的目的。而創(chuàng)建對(duì)象只是受限于面向?qū)ο笳Z(yǔ)法而不得不采取的一種手段方式。那,有沒有更加簡(jiǎn)單的辦法?如果我們將關(guān)注點(diǎn)從“怎么做”回歸到“做什么”的本質(zhì)上,就會(huì)發(fā)現(xiàn)只要能夠更好地達(dá)到目的,過程與形式其實(shí)并不重要。

生活舉例

當(dāng)我們需要從北京到上海時(shí),可以選擇高鐵、汽車、騎行或是徒步。我們的真正目的是到達(dá)上海,而如何才能到達(dá)上海的形式并不重要,所以我們一直在探索有沒有比高鐵更好的方式——搭乘飛機(jī)。

而現(xiàn)在這種飛機(jī)(甚至是飛船)已經(jīng)誕生:2014年3月Oracle所發(fā)布的Java 8(JDK 1.8)中,加入了Lambda表達(dá)式的重量級(jí)新特性,為我們打開了新世界的大門。

3.4 體驗(yàn)Lambda的更優(yōu)寫法

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

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

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

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

3.5 回顧匿名內(nèi)部類

Lambda是怎樣擊敗面向?qū)ο蟮模吭谏侠?,核心代碼其實(shí)只是如下所示的內(nèi)容:

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

為了理解Lambda的語(yǔ)義,我們需要從傳統(tǒng)的代碼起步。

使用實(shí)現(xiàn)類

要啟動(dòng)一個(gè)線程,需要?jiǎng)?chuàng)建一個(gè)Thread類的對(duì)象并調(diào)用start方法。而為了指定線程執(zhí)行的內(nèi)容,需要調(diào)用Thread類的構(gòu)造方法:

  • public Thread(Runnable target)

為了獲取Runnable接口的實(shí)現(xiàn)對(duì)象,可以為該接口定義一個(gè)實(shí)現(xiàn)類RunnableImpl

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

然后創(chuàng)建該實(shí)現(xiàn)類的對(duì)象作為Thread類的構(gòu)造參數(shù):

public class Demo03ThreadInitParam {
    public static void main(String[] args) {
        Runnable task = new RunnableImpl();
        new Thread(task).start();
    }
}

使用匿名內(nèi)部類

這個(gè)RunnableImpl類只是為了實(shí)現(xiàn)Runnable接口而存在的,而且僅被使用了唯一一次,所以使用匿名內(nèi)部類的語(yǔ)法即可省去該類的單獨(dú)定義,即匿名內(nèi)部類:

public class Demo04ThreadNameless {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("多線程任務(wù)執(zhí)行!");
            }
        }).start();
    }
}

匿名內(nèi)部類的好處與弊端

一方面,匿名內(nèi)部類可以幫我們省去實(shí)現(xiàn)類的定義;另一方面,匿名內(nèi)部類的語(yǔ)法——確實(shí)太復(fù)雜了!

語(yǔ)義分析

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

  • public abstract void run();

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

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

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

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

3.6 Lambda標(biāo)準(zhǔn)格式

Lambda省去面向?qū)ο蟮臈l條框框,格式由3個(gè)部分組成:

  • 一些參數(shù)
  • 一個(gè)箭頭
  • 一段代碼

Lambda表達(dá)式的標(biāo)準(zhǔn)格式為:

(參數(shù)類型 參數(shù)名稱) -> { 代碼語(yǔ)句 }

格式說明:

  • 小括號(hào)內(nèi)的語(yǔ)法與傳統(tǒng)方法參數(shù)列表一致:無(wú)參數(shù)則留空;多個(gè)參數(shù)則用逗號(hào)分隔。
  • ->是新引入的語(yǔ)法格式,代表指向動(dòng)作。
  • 大括號(hào)內(nèi)的語(yǔ)法與傳統(tǒng)方法體要求基本一致。

3.7 練習(xí):使用Lambda標(biāo)準(zhǔn)格式(無(wú)參無(wú)返回)

題目

給定一個(gè)廚子Cook接口,內(nèi)含唯一的抽象方法makeFood,且無(wú)參數(shù)、無(wú)返回值。如下:

public interface Cook {
    void makeFood();
}

在下面的代碼中,請(qǐng)使用Lambda的標(biāo)準(zhǔn)格式調(diào)用invokeCook方法,打印輸出“吃飯啦!”字樣:

public class Demo05InvokeCook {
    public static void main(String[] args) {
        // TODO 請(qǐng)?jiān)诖耸褂肔ambda【標(biāo)準(zhǔn)格式】調(diào)用invokeCook方法
    }

    private static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}

解答

public static void main(String[] args) {
    invokeCook(() -> {
        System.out.println("吃飯啦!");
    });
}

備注:小括號(hào)代表Cook接口makeFood抽象方法的參數(shù)為空,大括號(hào)代表makeFood的方法體。

3.8 Lambda的參數(shù)和返回值

需求:
    使用數(shù)組存儲(chǔ)多個(gè)Person對(duì)象
    對(duì)數(shù)組中的Person對(duì)象使用Arrays的sort方法通過年齡進(jìn)行升序排序

下面舉例演示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í)例來指定排序的規(guī)則。假設(shè)有一個(gè)Person類,含有String nameint age兩個(gè)成員變量:

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

傳統(tǒng)寫法

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

import java.util.Arrays;
import java.util.Comparator;

public class Demo06Comparator {
    public static void main(String[] args) {
        // 本來年齡亂序的對(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ī)則。

代碼分析

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

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

Lambda寫法

import java.util.Arrays;

public class Demo07ComparatorLambda {
    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);
        }
    }
}

3.9 練習(xí):使用Lambda標(biāo)準(zhǔn)格式(有參有返回)

題目

給定一個(gè)計(jì)算器Calculator接口,內(nèi)含抽象方法calc可以將兩個(gè)int數(shù)字相加得到和值:

public interface Calculator {
    int calc(int a, int b);
}

在下面的代碼中,請(qǐng)使用Lambda的標(biāo)準(zhǔn)格式調(diào)用invokeCalc方法,完成120和130的相加計(jì)算:

public class Demo08InvokeCalc {
    public static void main(String[] args) {
        // TODO 請(qǐng)?jiān)诖耸褂肔ambda【標(biāo)準(zhǔn)格式】調(diào)用invokeCalc方法來計(jì)算120+130的結(jié)果?
    }

    private static void invokeCalc(int a, int b, Calculator calculator) {
        int result = calculator.calc(a, b);
        System.out.println("結(jié)果是:" + result);
    }
}

解答

public static void main(String[] args) {
    invokeCalc(120, 130, (int a, int b) -> {
        return a + b;
    });
}

備注:小括號(hào)代表Calculator接口calc抽象方法的參數(shù),大括號(hào)代表calc的方法體。

3.10 Lambda省略格式

可推導(dǎo)即可省略

Lambda強(qiáng)調(diào)的是“做什么”而不是“怎么做”,所以凡是可以根據(jù)上下文推導(dǎo)得知的信息,都可以省略。例如上例還可以使用Lambda的省略寫法:

public static void main(String[] args) {
    invokeCalc(120, 130, (a, b) -> a + b);
}

省略規(guī)則

在Lambda標(biāo)準(zhǔn)格式的基礎(chǔ)上,使用省略寫法的規(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)。

備注:掌握這些省略規(guī)則后,請(qǐng)對(duì)應(yīng)地回顧本章開頭的多線程案例。

3.11 練習(xí):使用Lambda省略格式

題目

仍然使用前文含有唯一makeFood抽象方法的廚子Cook接口,在下面的代碼中,請(qǐng)使用Lambda的省略格式調(diào)用invokeCook方法,打印輸出“吃飯啦!”字樣:

public class Demo09InvokeCook {
    public static void main(String[] args) {
        // TODO 請(qǐng)?jiān)诖耸褂肔ambda【省略格式】調(diào)用invokeCook方法
    }

    private static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}

解答

public static void main(String[] args) {
    invokeCook(() -> System.out.println("吃飯啦!"));
}

3.12 Lambda的使用前提

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

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

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

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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