天天看點

線程池、Lambda表達式1、等待喚醒機制2、線程池在這裡插入圖檔描述3、Lambda表達式

主要内容

  • 線程間的通信,用鎖調用方法來實作等待喚醒
  • 線程池
  • Lambda表達式

學習目标

  • 能夠了解線程通信概念
  • 能夠了解等待喚醒機制
  • 能夠描述Java中線程池運作原理
  • 能夠了解函數式程式設計相對于面向對象的優點
  • 能夠掌握Lambda表達式的标準格式
  • 能夠使用Lambda标準格式使用Runnable與Comparator接口
  • 能夠掌握Lambda表達式的省略格式與規則
  • 能夠使用Lambda省略格式使用Runnable與Comparator接口
  • 能夠通過Lambda的标準格式使用自定義的接口(有且僅有一個抽象方法)
  • 能夠通過Lambda的省略格式使用自定義的接口(有且僅有一個抽象方法)
  • 能夠明确Lambda的兩項使用前提

1、等待喚醒機制

1.1 線程間的通信

  • **概念:**多個線程,在處理同一個資源,但是處理的動作(執行線程的任務即run方法)卻不相同

    比如:線程A用來生成包子,線程B用來吃包子,包子可以了解為同一資源,線程A與線程B處理的動作,一個是生産,一個是消費,處理的動作不同,那麼線程A與線程B之間,就存在着線程間的通信問題。

線程池、Lambda表達式1、等待喚醒機制2、線程池在這裡插入圖檔描述3、Lambda表達式
  • 為什麼要處理線程間的通信:

    多個線程并發執行時, 在預設情況下,CPU是随機切換線程的,當我們需要多個線程來共同完成一件任務,并且我們希望他們有規律的執行, 那麼多個線程之間就需要一些協調和通信,幫我們達到多個線程間,共同操作同一份資料!!!

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

    多個線程在處理同一個資源,并且任務不同時,需要線程間通信來幫助解決線程之間對同一個變量的使用或操作。 或者說多個線程在操作同一份資料時, 避免對同一共享變量的争奪。也就是我們需要通過一定的手段,使各個線程能有效的利用資源。

    而這種手段即—— 等待喚醒機制。

1.2 等待喚醒機制

  • 什麼是等待喚醒機制

    這是多個線程間的一種協作機制。談到線程,我們經常想到的是線程間的競争(race),比如去争奪鎖,但這并不是故事的全部,線程間也會有協作機制。就好比在公司裡,你和你的同僚們,你們可能存在在晉升時的競争,但更多時候,你們更多是一起合作,以完成某些任務。

    在一個線程進行了規定操作後,就進入等待狀态(wait()), 等待其他線程執行完他們的指定代碼過後,再将其喚醒(notify());在有多個線程進行等待時, 如果需要,可以使用( notifyAll())來喚醒所有的等待線程。

    wait/notify 就是線程間的一種協作機制

  • 等待喚醒中的方法

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

    1. wait:線程不再活動,不再參與排程,進入 wait set(等待集合) 中,是以不會浪費 CPU 資源,也不會去競争鎖了,這時的線程狀态即是 WAITING。它還要執行一個特别的動作,也即是“通知(notify)”在這個對象上等待的線程從wait set 中釋放出來,重新進入到排程隊列(ready queue)中
    2. **notify:**則選取所通知對象的 wait set 中的一個線程釋放;即随機喚醒其中一個等待的線程
    3. **notifyAll:**則釋放所通知對象的 wait set 上的全部線程。

    注意:

    哪怕隻通知了一個等待的線程,被通知線程也不能立即恢複執行,因為它當國中斷的地方是在同步塊内,而此刻它已經不持有鎖,是以她需要再次嘗試去擷取鎖(很可能面臨其它線程的競争),成功後才能在當初調用 wait 方法之後的地方恢複執行。

    總結如下:

    • 如果能擷取鎖,線程就從 WAITING 狀态變成 RUNNABLE 狀态;
    • 否則,線程就從 WAITING 狀态又變成 BLOCKED 狀态
  • 調用wait和notify方法需要注意的細節(把握一個原則,用鎖等待,用同一個鎖喚醒)
    1. wait方法與notify方法必須由同一個鎖對象調用

      ​ 因為:對應的鎖對象可通過notify喚醒使用同一個鎖對象調用的wait方法後的線程

    2. wait方法與notify方法是屬于Object類的方法的

      ​ 因為:鎖對象可以是任意對象,而任意對象的所屬類都是繼承了Object類的

    3. wait方法與notify方法必須要在同步代碼塊或者是同步函數方法中使用

      ​ 因為:必須要通過鎖對象調用這2個方法

1.3 生産者與消費者問題(等待喚醒機制)

就拿生産包子消費包子來說,看看等待喚醒機制如何有效利用同一資源:

包子鋪線程生産包子,吃貨線程消費包子。當包子沒有時(包子狀态為false),吃貨線程等待,包子鋪線程生産包子(即包子狀态為true),并通知吃貨線程(解除吃貨的等待狀态),因為已經有包子了,那麼包子鋪線程進入等待狀态。接下來,吃貨線程能否進一步執行,則取決于鎖的擷取情況。如果吃貨擷取到鎖,那麼就執行吃包子動作,包子吃完(包子狀态為false),并通知包子鋪線程(解除包子鋪的等待狀态),吃貨線程進入等待。包子鋪線程能否進一步執行,同樣取決于鎖的擷取情況。

代碼示範:

測試類:
public class Test01 {
    public static void main(String[] args) {
        BaoZi baoZi = new BaoZi();//new包子類對象

        BaoZiPu baoZiPu = new BaoZiPu("雷強包子鋪",baoZi);//new包子鋪類對象,并調用有參構造方法為其指派
        baoZiPu.start();//啟動包子鋪線程,源碼中start方法會調用對應的run方法

        ChiHuo chiHuo = new ChiHuo("波波",baoZi);//new吃貨對象,并調用有參構造方法為其指派
        chiHuo.start();//啟動吃貨線程,源碼中start方法會調用對應的run方法
    }
}

包子資源類:
class BaoZi{//定義包子資源類
    String pi;//包子皮
    String xianer;//包子餡兒
    boolean flag;//包子鋪包子的狀态,true為做好了一個包子,false為包子已經吃了
}

包子鋪線程類:
class BaoZiPu extends Thread{//定義包子鋪線程類
    private BaoZi baoZi;//定義包子對象,不能在run方法裡建立對象
    int count = 0;//利用計數再計算來控制包子的種類

    public BaoZiPu(String name,BaoZi baoZi) {//重載構造方法,2個形參,包子鋪名和包子對象
        super(name);//無參訪無參,有參訪有參,通路Thread類的有參構造方法
        this.baoZi = baoZi;//this關鍵字,誰調用我我就代表誰
    }

    @Override
    public void run() {//包子鋪子類重寫父類Thread的run方法
        while(true){//循環來控制做包子吃包子的整個流程不中斷
            synchronized ("hanhan"){//synchronized同步鎖,baozi鎖對象唯一,可以自定義任何對象
                if(baoZi.flag == false){//包子狀态為false,包子鋪就開始做包子
                    count++;
                    if(count%2 == 0){
                        baoZi.pi = "西瓜";
                        baoZi.xianer = "桂圓";
                    }else if(count%2 == 1){
                        baoZi.pi = "紅棗";
                        baoZi.xianer = "蓮子";
                    }

                    System.out.println(getName()+"做了"+baoZi.pi+baoZi.xianer+"包子");
                    System.out.println("雷強:包子做好啦,波波你來吃吧!");
                    baoZi.flag = true;//做好了包子,更改包子狀态false為true
                    "hanhan".notify();//選取所通知對象的 wait set 中的一個線程釋放,喚醒
                }else{//包子狀态為true
                    try{
                        "hanhan".wait();//線程不再活動,進入wait set(等待集合)中,不會浪費 CPU 資源,也不會去競争鎖了
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

吃貨線程類:
class ChiHuo extends Thread{//定義吃貨線程類
    private BaoZi baoZi;//定義包子對象
    public ChiHuo(String name,BaoZi baoZi) {
        super(name);
        this.baoZi = baoZi;
    }

    @Override
    public void run() {
        while(true){
            synchronized ("hanhan"){//和包子鋪線程同用一把鎖,鎖對象調用wait和notify方法
                if(baoZi.flag == true){
                    System.out.println(getName()+"吃了"+baoZi.pi+baoZi.xianer+"包子");
                    System.out.println("波波:包子吃完啦,強哥你繼續做吧!");
                    System.out.println("=========================================");
                    baoZi.flag = false;
                    "hanhan".notify();
                }else{
                    try{
                        "hanhan".wait();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

執行效果:
雷強包子鋪做了西瓜桂圓包子
雷強:包子做好啦,波波你來吃吧!
波波吃了西瓜桂圓包子
波波:包子吃完啦,強哥你繼續做吧!
=========================================
雷強包子鋪做了紅棗蓮子包子
雷強:包子做好啦,波波你來吃吧!
波波吃了紅棗蓮子包子
波波:包子吃完啦,強哥你繼續做吧!
=========================================
......
           

2、線程池在這裡插入圖檔描述

2.1 線程池思想概述

線程池、Lambda表達式1、等待喚醒機制2、線程池在這裡插入圖檔描述3、Lambda表達式

我們使用線程的時候,就去建立一個線程,這樣實作起來非常簡便,但是就會有一個問題:如果并發的線程數量很多,并且每個線程都是執行一個時間很短的任務(run方法)就結束了,這樣頻繁建立線程就會大大降低系統的效率,因為頻繁建立線程和銷毀線程是需要時間。那麼有沒有一種辦法,使得線程可以複用,就是執行完一個任務,線程沒有被銷毀,可以繼續執行其他的任務?

在Java中可以通過線程池,來達到這樣的效果。

2.2 線程池概念

  • **線程池:**其實就是一個容納多個線程的容器,其中的線程可以反複使用,省去了頻繁建立線程對象的操作,進而無需反複建立線程而消耗過多資源。

    由于線程池中有很多操作都是與優化資源相關的,不多贅述。通過一張圖來了解線程池的工作原理:

線程池、Lambda表達式1、等待喚醒機制2、線程池在這裡插入圖檔描述3、Lambda表達式
  • 合理利用線程池能夠帶來三個好處://記憶,資源,速度,管理,選擇題
  1. 降低資源消耗。 減少了建立和銷毀線程的次數,每個工作線程都可以被重複利用,可執行多個任務。
  2. 提高響應速度。 當任務到達時,任務可以不需要等到線程建立,就能立即執行。
  3. 提高線程的可管理性。 可以根據系統的承受能力,調整線程池中,工作線線程的數目,防止因為消耗過多的記憶體,而把伺服器累趴下(每個線程需要大約1MB記憶體,線程開的越多,消耗的記憶體也就越大,最後當機)。

2.3 線程池的使用(工具類Executors)

Java裡面線程池的頂級接口是

java.util.concurrent.Executor

,但是嚴格意義上講

Executor

并不是一個線程池,而隻是一個執行線程的工具。真正的線程池接口是

java.util.concurrent.ExecutorService

要配置一個線程池是比較複雜的,尤其是對于線程池的原理,不是很清楚的情況下,很有可能配置的線程池不是較優的,是以在

java.util.concurrent.Executors

類裡面提供了一些靜态方法,來生成一些常用的線程池。
  • 官方建議使用Executors類,來建立線程池對象。
  • Executors類建立線程池的靜态方法如下:
    • public static ExecutorService newFixedThreadPool(int nThreads)

      : 傳回線程池對象

      (建立的是有界線程池,也就是池中的線程個數可以指定最大數量)

  • 擷取到了一個線程池ExecutorService 對象,那麼怎麼使用呢,在這裡定義了一個使用線程池對象的方法如下:
    • public Future<?> submit(Runnable task或者Callable tast)

      : 擷取線程池中的某一個線程對象,并執行送出的任務
  • 上面submit送出方法,如果送出的是Callable接口實作類對象,重寫的call方法可以産生傳回值,通過Future未來對象的get方法來擷取
    Future接口:未來接口,其接口實作類對象,用來記錄線程任務執行完畢後産生的結果
  • 使用線程池中線程對象的步驟:
    1. 建立線程池對象。
    2. 建立Runnable接口子類對象。(task)
    3. 送出Runnable接口子類對象。(take task)
    4. 關閉線程池(一般不做)。

Runnable實作類代碼:

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

線程池測試類:

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 建立線程池對象
        ExecutorService pool = Executors.newFixedThreadPool(2);//包含2個線程對象
        // 建立Runnable執行個體對象
        MyRunnable r = new MyRunnable();
        // 從線程池中擷取線程對象,然後調用MyRunnable中的run()
        pool.submit(r);
        // 再擷取個線程對象,調用MyRunnable中的run()
        pool.submit(r);
        pool.submit(r);
        // 注意:submit方法調用結束後,程式并不終止,是因為線程池,控制了線程的關閉。
        // 将使用完的線程又歸還到了線程池中
        // 關閉線程池
        //pool.shutdown();
    }
}
           

線程池的使用,簡單,記産生線程池的工具類Executors,類名點調用方法,産生線程池對象送出任務執行即可,代碼如下所示:

import java.util.concurrent.*;

public class Test02 {
    public static void main(String[] args) {
        //線程池的使用,簡單,記産生線程池的工具類Executors,然後線程池對象送出任務執行即可
        //姨,cs
//        ExecutorService pool = Executors.newFixedThreadPool(2);//pool線程池對象,2表示池裡面有2個線程
//        pool.submit(new Runnable() {
//            @Override
//            public void run() {
//                System.out.println("剪頭發啦");
//            }
//        });
//
//        pool.submit(new Runnable() {
//            @Override
//            public void run() {
//                System.out.println("剪頭發啦2");
//            }
//        });
//
//        pool.submit(new Runnable() {
//            @Override
//            public void run() {
//                System.out.println("剪頭發啦3");
//            }
//        });

        //線程池裡面線程執行完任務,程式沒有停止,線程沒有銷毀而是回到線程裡面,繼續等待執行任務,線程可以重複利用的
//        Future<?> f = pool.submit(new Runnable() {
//            @Override
//            public void run() {
//                System.out.println("剪頭發啦2");
//            }
//        });//f是Future未來對象對象名
//
//        try {
            System.out.println(f);//[email protected]
//            System.out.println(f.get());//送出任務對應重寫方法的傳回值,沒有傳回值傳回null表示
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        } catch (ExecutionException e) {
//            e.printStackTrace();
//        }

        ExecutorService pool = Executors.newFixedThreadPool(2);//pool線程池對象,2表示池裡面有2個線程
//        pool.submit(new Callable<Object>() {
//            @Override
//            public Object call() {
//                System.out.println("剪完頭發");
//                return 888;
//            }
//        });

        Future<Object> f2 = pool.submit(new Callable<Object>() {
            @Override
            public Object call() {
                System.out.println("剪完頭發");
                return 888;
            }
        });
        try {
            System.out.println(f2.get());//888,未來對象的得到方法對應的是送出任務重寫方法傳回值,call方法
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        //程式沒有停止,線程池的線程沒有銷毀,可以重複利用
        //把線程池關了
        pool.shutdown();//關了程式停止了,建議不要這麼做,不能做事情了,否則報錯:RejectedExecutionException,拒絕執行異常

        pool.submit(new Callable<Object>() {//報錯,線程池已經關了,不能執行任務了
            @Override
            public Object call() {
                System.out.println("理發店關門了還想剪頭發");
                return 888;
            }
        });
        
        //可以傳入入Thread類的方式!!!
        MyThread mt = new MyThread();
        pool.submit(mt);//"你放進來做事情啦"
    }
}

class MyThread extends Thread {// Thread implements Runnable
    @Override
    public void run() {
        System.out.println("你放進來做事情啦");
    }
}
           

3、Lambda表達式

簡單記,接口引用調用自己唯一的一個抽象方法時,以前用的是匿名内部類,現在可以用lambda表達式來代替,做的事情無法是:參數傳遞,執行方法體做事情,()->{}

所有總結:以前用匿名内部類重寫一個接口唯一的抽象方法的地方,現在用lambda表達式來代替,無法做的是參數傳遞執行方法體标準格式是小括号箭頭大括号,()->{}
省略格式是如果參數可以推導類型可以省略,如果小括号隻有一個參數,小括号可以省略,如果大括号裡面隻有一句話,大括号和return和分号可以一起省略
           

3.1 函數式程式設計思想概述

線程池、Lambda表達式1、等待喚醒機制2、線程池在這裡插入圖檔描述3、Lambda表達式

在數學中,函數就是有輸入量()、輸出量return的一套計算方案,也就是“拿什麼東西做什麼事情”。相對而言,面向對象過分強調“必須通過對象的形式來做事情”,而函數式思想則盡量忽略面向對象的複雜文法——強調做什麼{},而不是以什麼形式做。

3.2 備援的Runnable代碼

傳統寫法

當需要啟動一個線程去完成任務時,通常會通過

java.lang.Runnable

接口來定義任務内容,并使用

java.lang.Thread

類來啟動該線程。代碼如下:

public class Demo01Runnable {
	public static void main(String[] args) {
    	// 匿名内部類//多态
		Runnable task = new Runnable() {
			@Override
			public void run() { // 覆寫重寫抽象方法
				System.out.println("多線程任務執行!");
			}
		};
        
		new Thread(task).start(); // 啟動線程,執行任務
	}
}
           

本着“一切皆對象”的思想,這種做法是無可厚非的:首先建立一個

Runnable

接口的匿名内部類對象來指定任務内容,再将其交給一個線程來啟動。

代碼分析,簡單了解

對于

Runnable

的匿名内部類用法,可以分析出幾點内容:

  • Thread

    類需要

    Runnable

    接口作為參數,其中的抽象

    run

    方法是用來指定線程任務内容的核心;
  • 為了指定

    run

    的方法體,不得不需要

    Runnable

    接口的實作類;
  • 為了省去定義一個

    RunnableImpl

    實作類的麻煩,不得不使用匿名内部類;
  • 必須覆寫重寫抽象

    run

    方法,是以方法名稱、方法參數、方法傳回值不得不再寫一遍,且不能寫錯;
  • 而實際上,似乎隻有方法體{}才是關鍵所在。

3.3 程式設計思想轉換

做什麼,而不是怎麼做

我們真的希望建立一個匿名内部類對象嗎?不。我們隻是為了做這件事情而不得不建立一個對象。我們真正希望做的事情是:将

run

方法體内的代碼傳遞給

Thread

類知曉。

傳遞一段代碼——這才是我們真正的目的。而建立對象隻是受限于面向對象文法而不得不采取的一種手段方式。那,有沒有更加簡單的辦法?如果我們将關注點從“怎麼做”回歸到“做什麼”的本質上,就會發現隻要能夠更好地達到目的,過程與形式,其實并不重要。

生活舉例

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

而現在這種飛機(甚至是飛船)已經誕生:2014年3月Oracle所釋出的Java 8(JDK 1.8)中,加入了Lambda表達式的這個新特性,為我們打開了更快通往新世界的大門。()->{}

3.4 體驗Lambda的更優寫法

借助Java 8的這個新特性,上述

Runnable

接口的匿名内部類寫法,可以通過更簡單的Lambda表達式來達到等效:

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

這段代碼和上面的執行效果是完全一樣的,可以在1.8或更高的編譯級别下通過。從代碼的語義中可以看出:我們啟動了一個線程,而線程任務的内容,以一種更加簡潔的形式來指定。

這時,便不再有“不得不建立接口對象”的束縛,不再有“抽象方法覆寫重寫”的負擔,就是這麼簡單!新特性就是爽!!!

3.5 回顧匿名内部類

Lambda表達式,是怎樣擊敗面向對象的?在上例中,核心代碼,其實隻是如下所示的内容:

為了了解Lambda的語義,我們需要從傳統的代碼起步。

使用實作類

要啟動一個線程,需要建立一個

Thread

類的對象并調用

start

方法。而為了指定線程執行任務的内容,需要調用

Thread

類的構造方法:

  • public Thread(Runnable target)

為了擷取

Runnable

接口的實作對象,可以為該接口定義一個實作類

RunnableImpl

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

然後建立該實作類的對象作為

Thread

類的構造參數:

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

使用匿名内部類

這個

RunnableImpl

類隻是為了實作

Runnable

接口而存在的,而且僅被使用了唯一一次,是以使用匿名内部類的文法,即可省去該類的單獨定義,即匿名内部類:

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

匿名内部類的好處與弊端

一方面,匿名内部類可以幫我們省去實作類的定義;另一方面,匿名内部類的文法——确實太複雜了!

語義分析

仔細分析該代碼中的語義,

Runnable

接口隻有一個

run

方法的定義:

  • public abstract void run();

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

  • 無參數:不需要任何條件即可執行該方案。
  • 無傳回值:該方案不産生任何結果。
  • 代碼塊(方法體):該方案的具體執行步驟。

同樣的語義展現在

Lambda

文法中,要更加簡單:

  • 前面的一對小括号即

    run

    方法的參數(無),代表不需要任何條件;
  • 中間的一個箭頭代表将前面的參數傳遞給後面的代碼;
  • 後面的輸出語句即業務邏輯代碼。

3.6 Lambda标準格式,()->{},參數傳遞,執行方法體,做事情

Lambda省去面向對象的條條框框,格式由3個部分組成:

  • 一些參數
  • 一個箭頭
  • 一段代碼

Lambda表達式的标準格式為:

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

格式說明:

  • 小括号内的文法與傳統方法參數清單()一緻:無參數則留白;多個參數則用逗号分隔。
  • ->

    是新引入的文法格式,代表指向動作,傳遞
  • 大括号内的文法與傳統方法體要求基本一緻。

3.7 練習:使用Lambda标準格式(無參無傳回)//以前用匿名内部類,現在用Lambda表達式,參數傳遞,執行方法體:()->{}

  • 題目

給定一個廚子

Cook

接口,内含唯一的抽象方法

makeFood

,且無參數、無傳回值。如下:

public interface Cook {//有且隻有一個抽象方法的接口,函數式接口
    void makeFood();
}
           

在下面的代碼中,請使用Lambda的标準格式調用

invokeCook

方法,列印輸出“吃飯啦!”字樣:

public class Demo05InvokeCook {
    public static void main(String[] args) {
        // TODO 請在此使用Lambda【标準格式】調用invokeCook方法
    }

    private static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}
           
  • 解答
public static void main(String[] args) {
    invokeCook(() -> {
      	System.out.println("吃飯啦!");
    });
}
           
備注:小括号代表

Cook

接口

makeFood

抽象方法的參數為空,大括号代表

makeFood

的方法體。

3.8 Lambda的參數和傳回值

下面舉例示範

java.util.Comparator<T>

接口的使用場景代碼,其中的抽象方法定義為:

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

當需要對一個對象數組進行排序時,

Arrays.sort

方法需要一個

Comparator

接口執行個體來指定排序的規則。假設有一個

Person

類,含有

String name

int age

兩個成員變量:

class Person { 
    private String name;
    private int age;
    
    // 省略構造器、toString方法與Getter Setter 
}
           

傳統寫法

如果使用傳統的代碼對

Person[]

數組進行排序,寫法如下:

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

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

      	// 匿名内部類
        Comparator<Person> comp = new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        };
        Arrays.sort(array, comp); // 第二個參數為排序規則,即Comparator接口執行個體

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

這種做法在面向對象的思想中,似乎也是“理所當然”的。其中

Comparator

接口的執行個體(使用了匿名内部類)代表了“按照年齡從小到大”的排序規則。

代碼分析

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

  • 為了排序,

    Arrays.sort

    方法需要排序規則,即

    Comparator

    接口的執行個體,抽象方法

    compare

    是關鍵;
  • 為了指定

    compare

    的方法體,不得不需要

    Comparator

    接口的實作類;
  • 為了省去定義一個

    ComparatorImpl

    實作類的麻煩,不得不使用匿名内部類;
  • 必須覆寫重寫抽象

    compare

    方法,是以方法名稱、方法參數、方法傳回值不得不再寫一遍,且不能寫錯;
  • 實際上,隻有參數和方法體才是關鍵。

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 練習:使用Lambda标準格式(有參有傳回)

題目

給定一個電腦

Calculator

接口,内含抽象方法

calc

可以将兩個int數字相加得到和值:

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

在下面的代碼中,請使用Lambda的标準格式調用

invokeCalc

方法,完成120和130的相加計算:

public class Demo08InvokeCalc {
    public static void main(String[] args) {
        // TODO 請在此使用Lambda【标準格式】調用invokeCalc方法來計算120+130的結果ß
    }

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

解答

public static void main(String[] args) {
    invokeCalc(120, 130, (int a, int b) -> {
      	return a + b;
    });
}
           
備注:小括号代表

Calculator

接口

calc

抽象方法的參數,大括号代表

calc

的方法體。

3.10 Lambda省略格式

  • 可推導即可省略

Lambda強調的是“做什麼”而不是“怎麼做”,是以凡是可以根據上下文推導,得知的資訊,都可以省略。例如上例還可以使用Lambda的省略寫法:

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

在Lambda标準格式()->{}的基礎上,使用省略寫法的規則為://記憶

  1. 小括号内參數的類型可以省略;
  2. 如果小括号内有且僅有一個參數,則小括号可以省略;
  3. 如果大括号内有且僅有一個語句,則無論是否有傳回值,都可以省略大括号、return關鍵字及語句分号。
備注:掌握這些省略規則後,請對應地回顧本章開頭的多線程案例。
  • 3.11 練習:使用Lambda省略格式
  • 題目

仍然使用前文含有唯一

makeFood

抽象方法的廚子

Cook

接口,在下面的代碼中,請使用Lambda的省略格式調用

invokeCook

方法,列印輸出“吃飯啦!”字樣:

public class Demo09InvokeCook {
    public static void main(String[] args) {
        // TODO 請在此使用Lambda【省略格式】調用invokeCook方法
    }

    private static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}
           
  • 解答
public static void main(String[] args) {
  	invokeCook(() -> System.out.println("吃飯啦!"));
}
           

3.12 Lambda的使用前提//函數式程式設計,用到函數式接口

Lambda的文法非常簡潔,完全沒有面向對象複雜的束縛。但是使用時有幾個問題需要特别注意://記憶

  1. 使用Lambda必須具有接口,且要求接口中有且僅有一個抽象方法(函數式接口)。
    無論是JDK内置的

    Runnable

    Comparator

    接口還是自定義的接口,隻有當接口中的抽象方法存在且唯一時,才可以使用Lambda。
  2. 使用Lambda必須具有上下文推斷。
    也就是方法的參數或局部變量類型,必須為Lambda對應的接口類型,才能使用Lambda作為該接口的執行個體。
備注:有且僅有一個抽象方法的接口,稱為“函數式接口”,這個後面還會學習。