day18【死鎖、線程狀態、等待與喚醒、Lambda】

今日內容

  • 死鎖
  • 線程狀態
  • 等待與喚醒

教學目標

  • 能夠描述死鎖產生的原因
  • 能夠說出線程6個狀態的名稱
  • 能夠理解等待喚醒案例

第一章 死鎖

1. 什麼是死鎖

死鎖:是指兩個或者兩個以上的線程在執行的過程中,因爭奪資源產生的一種互相等待的現象。

死鎖在開發中,也會遇到,當線程進入到死鎖狀態時,程序中線程就會一直處於等待狀態。

死鎖的發生:

舉例1:

我請柳巖喫飯(麻辣燙)

我點了一份。上了一雙筷子

我:一支

柳巖:一支

我和柳巖就相當於兩個線程,而麻辣燙相當於共享資源,兩個人都沒辦法喫,都在等待看着麻辣燙,這種現象就是死鎖。

舉例2:有兩個線程(t1線程、t2線程),有兩個對象鎖(lock_a、lock_b)

​ t1線程在執行時,先拿到lock_a對象鎖(此時lock_a對象鎖綁定在t1線程上)。而正在此時CPU切換到t2線程上,t2線程拿到lock_b對象鎖(此時lock_b對象鎖綁定在t2線程上),這時CPU又切換到t1線程上,這時t1線程需要拿lock_b對象鎖,此時t1線程獲取不到lock_b對象鎖(t1線程處於等待)。

當CPU切換到t2線程上,這時t2線程需要拿lock_a對象鎖,此時t2線程獲取不到lock_a對象鎖(B線程處於等待)。

上述案例2簡化如下說法:

​ 有2個線程,需要執行相同的任務,但是需要分別獲取的A鎖和B鎖才能去執行,第一個線程獲取鎖的順序是先A後B。第二個線程獲取鎖的順序是先B後A。

當第一個線程獲取A鎖,CPU切換到第二個線程,此時第二個線程獲取B鎖。而此時第一個線程缺少B鎖,第二個線程缺少A鎖。兩個線程都在等待,發生死鎖現象。

2.產生死鎖的條件

  1. 有多把鎖

  2. 有多個線程

  3. 有同步代碼塊嵌套

3. 代碼演示

分析和步驟:

1)創建一個任務類DeadLockTask 實現Runnable接口,複寫run函數;

2)創建兩個Object類的對象lock_a,lock_b作爲鎖對象;

3)定義一個變量flag,讓不同的線程切換到不同的地方去執行,按照不同的方式來獲取鎖;

4)在run函數中使用if-else結構來控制兩個線程去執行不同的內容,並使用while循環一直讓其執行;

5)在if中嵌套書寫兩個同步代碼塊lock_a和lock_b分別作爲兩個代碼塊的鎖,將if中相同的內容複製一份寫到else中;

6)創建測試類DeadThreadLockDemo,在這個類的主函數中創建任務類的對象;

7)創建兩個線程對象t1和t2;

8)讓主線程休息1毫秒;

9)使用t1對象調用start函數開啓線程,讓下一個線程進入到else中;

10)開啓t2線程;

代碼如下所示:

/*
 * 演示線程死鎖的問題
 */
//定義一個線程任務類
class DeadLockTask implements Runnable
{
	//定義兩個鎖對象
	private Object lock_a=new Object();
	private Object lock_b=new Object();
	//定義一個變量作爲標記,控制取鎖的方式
	 boolean flag=true;
	public void run() {
		//當線程進來之後,一個線程進入到if中,另一個進入到else中
		if(flag)
		{
			while(true)
			{
				synchronized(lock_a)
				{
					System.out.println(Thread.currentThread().getName()+"if.....lock_a");
					synchronized(lock_b)
					{
						System.out.println(Thread.currentThread().getName()+"if.....lock_b");
					}
				}
			}
		}else
		{
			while(true)
			{
				synchronized(lock_b)
				{
					System.out.println(Thread.currentThread().getName()+"else.....lock_b");
					synchronized(lock_a)
					{
						System.out.println(Thread.currentThread().getName()+"else.....lock_a");
					}
				}
			}
		}
	}
}
public class DeadThreadLockDemo {

	public static void main(String[] args) {
		// 創建任務類對象
		DeadLockTask dlt = new DeadLockTask();
		//創建線程對象
		Thread t1 = new Thread(dlt);
		Thread t2 = new Thread(dlt);
		//開啓第一個線程
		t1.start();
		//修改標記讓下一個線程進入到else中
		dlt.flag=false;
		t2.start();
	}
}

上述代碼的結果如下圖所示:

在這裏插入圖片描述

通過以上結果發現,線程一直在執行else中的代碼,根本就沒有執行if語句中的代碼。

說明:

出現上述結果是因爲在主線程開啓第一個線程之後,很有可能CPU還在主線程上運行,那麼開啓的線程是不會被CPU立刻去執行,而CPU繼續處理主線程中的代碼, 就會直接去執行d.flag = false; ,這時就已經把標記修改成false,不管線程是否進入到run 方法中,flag都已經變成false,那麼就無法在進入if中,因此我們爲了保證第一個線程一定能夠進入到if中,於是在這裏讓主線程在開啓第一個線程之後,主線程進行休眠1毫秒。

在這裏插入圖片描述

死鎖的結果如下圖所示:

在這裏插入圖片描述

注意:在開發中一旦發生了死鎖現象,不能通過程序自身解決。必須修改程序的源代碼。

​ 在開發中,死鎖現象可以避免,但不能直接解決。當程序中有多個線程時,並且多個線程需要通過嵌套對象鎖(在一個同步代碼塊中包含另一個同步代碼塊)的方式纔可以操作代碼,此時就容易出現死鎖現象。

​ 可以使用一個同步代碼塊解決的問題,不要使用嵌套的同步代碼塊,如果要使用嵌套的同步代碼塊,就要保證同步代碼塊的上的對象鎖使用同一個對象鎖(唯一的對象鎖)

第二章 線程狀態

線程狀態概述

線程由生到死的完整過程:技術素養和麪試的要求。

當線程被創建並啓動以後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態。在線程的生命週期中,有幾種狀態呢?在API中java.lang.Thread.State這個枚舉中給出了六種線程狀態:

線程狀態 導致狀態發生條件
NEW(新建) 線程剛被創建,但是並未啓動。還沒調用start方法。MyThread t = new MyThread()只有線程對象,沒有線程特徵。
Runnable(可運行) 線程可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決於操作系統處理器。調用了t.start()方法 :就緒(經典叫法)
Blocked(鎖阻塞) 當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀態;當該線程持有鎖時,該線程將變成Runnable狀態。
Waiting(無限等待) 一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。進入這個狀態後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。
Timed Waiting(計時等待) 同waiting狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting狀態。這一狀態將一直保持到超時期滿或者接收到喚醒通知。帶有超時參數的常用方法有Thread.sleep 、Object.wait。
Terminated(被終止) 因爲run方法正常退出而死亡,或者因爲沒有捕獲的異常終止了run方法而死亡。

在這裏插入圖片描述

我們不需要去研究這幾種狀態的實現原理,我們只需知道在做線程操作中存在這樣的狀態。那我們怎麼去理解這幾個狀態呢,新建與被終止還是很容易理解的,我們就研究一下線程從Runnable(可運行)狀態與非運行狀態之間的轉換問題。

第三章 等待喚醒機制(包子鋪賣包子)

​ 我們的賣票案例中,所有的線程都是在做相同的任務:賣票。而我們真實情況中,不同的線程有可能需要去做不同的任務。在程序中,最常見的一種模型,就是生產者和消費者模型。生產者線程和消費者線程之間需要進行通信,我們可以使用等待喚醒機制來實現生產者線程和消費者線程之間的通信。

  • Object類的方法

    wait()		:讓當前線程進入等待狀態
    notify()	:喚醒一個正在等待的線程,喚醒是隨機的
    void notifyAll() 喚醒在此對象監視器上等待的所有線程。 
    
    注意事項: 必須要使用鎖對象來調用的。
    
    • 兩個方法的小疑問

      • 等待和喚醒的方法爲什麼要定義在Object類中?

        因爲需要用鎖對象調用這兩個方法,任意對象都可以作爲鎖對象。
        也就是說任意類型的對象都可以調用的兩個方法,就需要定義在Object類中
        
      • 兩個方法必須寫在同步裏面嗎?

        兩個方法必須要在同步裏面調用,因爲在同步裏面纔有鎖對象。
        
    • 如果一個線程執行了wait()方法,那麼當前線程進入等待狀態,並且會釋放鎖對象,下次即使被喚醒必須獲取到鎖對象纔可以執行。

    代碼演示:

    public class MyRun1 implements Runnable {
        @Override
        public void run() {
            synchronized ("abc"){
                try {
                    //等待
                    "abc".wait();
                } catch (InterruptedException e) {
                }
            }
            System.out.println(1);
            System.out.println(2);
        }
    }
    
    public class MyRun2 implements Runnable {
        @Override
        public void run() {
            synchronized ("abc"){
                //notify()也必須要用鎖對象調用
                "abc".notify();
    
                System.out.println("A");
                System.out.println("B");
            }
        }
    }
    
    public class Test01 {
        public static void main(String[] args) throws Exception{
            //只能用鎖對象調用  別的對象調用就會報錯
    //        "abc".wait();
    
            //開啓線程
            MyRun1 mr = new MyRun1();
            new Thread(mr).start();
    
            //睡一秒鐘
            Thread.sleep(1000);
    
            MyRun2 mr2 = new MyRun2();
            new Thread(mr2).start();
        }
    }
    
  • 包子案例

    說明:

    ​ 1.定義一個包子類,類中成員變量:

     pi //皮兒
     xian //餡兒
     flag://用來表示有沒有包子,true來代表有   用false來代表沒有
    

    ​ 2.定義一個生產包子的任務類即生產者線程類:

    生產者線程思想:如果有包子就不需要製作,讓生產者線程進入等待狀態;如果沒有包子,開始製作包子,並且喚醒消費者線程來喫包子
    

    ​ 3.定義一個消費包子的任務類即消費者線程類:

    消費者線程思想:如果沒有包子就不消費,讓消費者線程進入等待狀態;如果有包子,開始喫包子,並且喚醒生產者線程來生產包子
    
    /*
        包子類需要定義3個成員變量:
            pi
            xian
            flag:表示是否有包子
     */
    //包子類
    public class BaoZi {
        //皮兒
        String pi;
        //餡兒
        String xian;
        //布爾值
        boolean flag=false;  //用來表示有沒有包子,用true來代表有   用false來代表沒有
    }
    
    //生產包子:生產者線程執行的任務
    /*
        生產者線程思想:如果有包子就不需要製作,讓生產者線程進入等待狀態;如果沒有包子,開始製作包子,並且喚醒消費者線程來喫包子
     */
    public class ZhiZuo implements Runnable {
        //成員變量
        BaoZi baoZi;
        //構造方法
        public ZhiZuo(BaoZi baoZi) {
            this.baoZi = baoZi;
        }
    
        @Override
        public void run() {
            //製作包子
            while (true){
                synchronized ("鎖"){//t1
                    if(baoZi.flag == true){
                        //如果有包子就不需要製作
                        //就讓製作的線程進入等待狀態
                        try {
                            "鎖".wait();
                        } catch (InterruptedException e) {
                        }
                    }
                        //表示沒有包子
                        //製作包子
                        baoZi.pi = "白麪";
                        baoZi.xian = "韭菜大蔥";
                        //修改包子狀態
                        baoZi.flag = true;
                        System.out.println("生產出了一個包子!");
    
                        //生產好了包子叫醒喫貨(消費者)來喫
                        "鎖".notify();
                 
                }
            }
        }
    }
    
    //喫包子:消費者線程執行的任務
    /*
        消費者線程思想:如果沒有包子就不消費,讓消費者線程進入等待狀態;如果有包子,開始喫包子,並且喚醒生產者線程來生產包子
     */
    public class ChiHuo implements Runnable {
        //成員變量
        BaoZi baoZi;
        //構造方法
        public ChiHuo(BaoZi baoZi) {
            this.baoZi = baoZi;
        }
    
        @Override
        public void run() {
            //喫包子
            while(true){
                synchronized ("鎖"){
                    if(baoZi.flag == false){
                        //沒包子
                        //讓喫包子的線程進入等待
                        try {
                            "鎖".wait();
                        } catch (InterruptedException e) {
                        }
                    }
                        //表示有包子
                        //開喫
                        System.out.println("喫貨吃了一個" + baoZi.pi+"皮兒," + baoZi.xian + "餡兒的大包子");
                        baoZi.pi = null;
                        baoZi.xian = null;
                        //修改包子狀態
                        baoZi.flag = false;
    
                        //喫完包子叫醒對方(生產者)來做
                        "鎖".notify();
                    
                }
            }
        }
    }
    
    //測試類
    public class Test01 {
        public static void main(String[] args) {
            //創建包子
            BaoZi baoZi = new BaoZi();
            //創建對象
            ZhiZuo zz = new ZhiZuo(baoZi);
            Thread t1 = new Thread(zz);//生產者線程
            t1.start();
            //創建對象:消費者線程
            ChiHuo ch = new ChiHuo(baoZi);
            Thread t2 = new Thread(ch);
            t2.start();
        }
    }
    

第四章 定時器Timer

  • 功能介紹

    ​ 定時器,可以設置線程在某個時間執行某件事情,或者某個時間開始,每間隔指定的時間反覆的做某件 事情。定時器類是java.util.Timer類

  • 方法介紹

    構造方法:
    	public Timer():構造一個定時器
    	
    常用方法:
    void schedule(TimerTask task, long delay)//在指定的延遲之後安排指定的任務執行。
        參數:
            task - 所要安排的任務。屬於TimerTask類型,java.util.TimerTask是一個實現Runnable接口的抽象類,代表一個可以被Timer執行的任務。我們需要擴展這個類來創建我們自己的TimerTask,它可以使用java Timer類進行調度。
            delay - 執行任務前的延遲時間,單位是毫秒。就是過了多少毫秒之後再執行task任務 
    
    void schedule(TimerTask task, long delay, long period)
    								//在指定 的延遲之後開始,重新執行固定延遲執行的指定任務。
        參數:
            task - 所要安排的任務。
            delay - 執行任務前的延遲時間,單位是毫秒。
            period - 執行各後續任務之間的時間間隔,單位是毫秒。 就是每隔多長時間執行一次task任務
    void schedule(TimerTask task, Date time) //在指定的時間安排指定的任務執行。
        參數:
            task - 所要安排的任務。
            time - 執行任務的時間。 時間一到就會開始執行任務
    
    void schedule(TimerTask task, Date firstTime, long period)
    								//從指定的時間開始,對指定的任務執行重複 的 固定延遲執行 。
         參數:
            task - 所要安排的任務。
            time - 執行任務的時間。 時間一到就會開始執行任務
     		period - 執行各後續任務之間的時間間隔,單位是毫秒。 就是每隔多長時間執行一次task任務
    
  • 代碼演示

public class Test01 {
    public static void main(String[] args) throws ParseException {
        //構造方法:
        //public Timer():構造一個定時器
        Timer t = new Timer();

        //常用方法:
        TimerTask tt = new TimerTask() {
            @Override
            public void run() {
                //這裏面要寫的是執行的代碼
                System.out.println("起牀了");
            }
        };
        //void schedule(TimerTask task, long delay)
        //在3秒鐘之後會自動執行任務
//        t.schedule(tt,3000);

        //void schedule(TimerTask task, long delay, long period)
        //在3秒鐘之後會自動執行任務,每過1秒執行一次
//        t.schedule(tt,3000,1000);

        String s = "2020-4-9 12:06:40";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //解析:能夠包字符串類型變成Date類型
        Date date = sdf.parse(s);
        //void schedule(TimerTask task, Date time)
        //表示時間到2020-4-9 12:06:40的時候就開始執行任務tt
//        t.schedule(tt,date);
//
//        //void schedule(TimerTask task, Date firstTime, long period)
//        //從指定的時間開始,對指定的任務執行重複 的 固定延遲執行 。
        //表示時間到2020-4-9 12:06:40的時候就開始執行任務tt,以後每隔2秒執行一次任務tt
        t.schedule(tt,date,2000);

    }
}

第五章 1.8特性_ Lambda表達式

5.1 函數式編程思想概述

在這裏插入圖片描述

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

做什麼,而不是怎麼做

例如之前的匿名內部類方式實現多線程案例中。

我們真的希望創建一個匿名內部類對象嗎?不。我們只是爲了做這件事情而不得不創建一個對象。我們真正希望做的事情是:將run方法體內的代碼傳遞給Thread類知曉。

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

5.2 Lambda的優化

當需要啓動一個線程去完成任務時,通常會通過java.lang.Runnable接口來定義任務內容,並使用java.lang.Thread類來啓動該線程。

傳統寫法,代碼如下:

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

本着“一切皆對象”的思想,這種做法是無可厚非的:首先創建一個Runnable接口的匿名內部類對象來指定任務內容,再將其交給一個線程來啓動。

代碼分析:

對於Runnable的匿名內部類用法,可以分析出幾點內容:

  • Thread類需要Runnable接口作爲參數,其中的抽象run方法是用來指定線程任務內容的核心;
  • 爲了指定run的方法體,不得不需要Runnable接口的實現類;
  • 爲了省去定義一個RunnableImpl實現類的麻煩,不得不使用匿名內部類;
  • 必須覆蓋重寫抽象run方法,所以方法名稱、方法參數、方法返回值不得不再寫一遍,且不能寫錯;
  • 而實際上,似乎只有方法體纔是關鍵所在

Lambda表達式寫法,代碼如下:

藉助Java 8的全新語法,上述Runnable接口的匿名內部類寫法可以通過更簡單的Lambda表達式達到等效:

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

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

不再有“不得不創建接口對象”的束縛,不再有“抽象方法覆蓋重寫”的負擔,就是這麼簡單!

5.3 Lambda的格式

標準格式:

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

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

Lambda表達式的標準格式爲:

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

格式說明:

  • 小括號內的語法與傳統方法參數列表一致:無參數則留空;多個參數則用逗號分隔。
  • ->是新引入的語法格式,代表指向動作。
  • 大括號內的語法與傳統方法體要求基本一致。

匿名內部類與lambda對比:

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

仔細分析該代碼中,Runnable接口只有一個run方法的定義:

  • public abstract void run();

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

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

同樣的語義體現在Lambda語法中,要更加簡單:

() -> System.out.println("多線程任務執行!")
  • 前面的一對小括號即run方法的參數(無),代表不需要任何條件;
  • 中間的一個箭頭代表將前面的參數傳遞給後面的代碼;
  • 後面的輸出語句即業務邏輯代碼。

參數和返回值:

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

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

當需要對一個對象數組進行排序時,Arrays.sort方法需要一個Comparator接口實例來指定排序的規則。假設有一個Person類,含有String nameint age兩個成員變量:

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

傳統寫法

如果使用傳統的代碼對Person[]數組進行排序,寫法如下:

public class Demo05Comparator {
    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寫法

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);
        }
    }
}

省略格式:

省略規則

在Lambda標準格式的基礎上,使用省略寫法的規則爲:

  1. 小括號內參數的類型可以省略;
  2. 如果小括號內有且僅有一個參數,則小括號可以省略;
  3. 如果大括號內有且僅有一個語句,則無論是否有返回值,都可以省略大括號、return關鍵字及語句分號。

備註:如果省略大括號、return關鍵字及語句分號的原則要省略都省略,要麼都不能省略。

可推導即可省略

Lambda強調的是“做什麼”而不是“怎麼做”,所以凡是可以推導得知的信息,都可以省略。例如上例還可以使用Lambda的省略寫法:

Runnable接口簡化:
1. () -> System.out.println("多線程任務執行!")
Comparator接口簡化:
2. Arrays.sort(array, (a, b) -> a.getAge() - b.getAge());

5.4 Lambda的前提條件

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

  1. 使用Lambda必須具有接口,且要求接口中有且僅有一個抽象方法
    無論是JDK內置的RunnableComparator接口還是自定義的接口,只有當接口中的抽象方法存在且唯一時,纔可以使用Lambda。
  2. 使用Lambda必須具有接口作爲方法參數。
    也就是方法的參數必須爲Lambda對應的接口類型,才能使用Lambda作爲該接口的實例。

備註:有且僅有一個抽象方法的接口,稱爲“函數式接口”。

5.5使用lambda總結

匿名內部類:
	可以用於類也可以用於接口,對類和接口中的方法的個數沒有要求。
	
Lambda表達式:
	只能用於接口,接口中抽象方法只能有一個。
	
Lambda表達式的要求更嚴格,並不是所有的匿名內部類都能改成Lambda表達式。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章