java8的新特性之lambda表示式和方法引用

ethanSung 發表於 2021-10-10
Java Java 8

1.1. Lambda表示式

通過具體的例項去體會lambda表示式對於我們程式碼的簡化,其實我們不去深究他的底層原理和背景,僅僅從用法上去理解,關注兩方面:

  1. lambda表示式是Java8的一個語法糖,用來簡化了函式式介面(理解什麼是函式式介面)例項的程式碼量;
  2. 什麼是函式式介面,只有在一個介面是函式式介面時候才能使用lambda表示式簡化我們的程式碼;

所以通過以上兩個點,我們需要貫徹始終的觀念有三點

  1. 明確函式式介面定義,就是有且只有一個抽象方法的介面就是函式式介面,當然加上@FunctionalInterface 註解更可以確定這個介面是函式式介面;
  2. lambda表示式只能用在函式式介面的例項中,即lambda表示式的語法本質就是函式式介面中那個唯一抽象方法的實現語句
  3. 因為函式式介面的抽象方法唯一,所以實現(重寫)該方法非常明確,不會造成使用了lambda表示式分不清是該介面的哪個方法被重寫了,於是我們就可以簡化省略各種不必要的語句,比如對資料型別的判斷,返回值的判斷,大括號之類的,這就是lambda表示式必須在函式式介面中才能使用的原因。

下面我們通過例項,對比沒有lambda表示式時候跟有了lambda表示式之後程式碼的語法糖,以下示例程式碼包含了lambda表示式的語法規則


package com.ethan.lambda;

import org.junit.Test;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;

/**
 * Lambda表示式的使用
 *
 * 1.語法格式:
 *  ->:lambda表示式的操作符
 *  ->左邊:lambda的形參列表(其實就是介面中抽象方法的形參列表)
 *  ->右邊:lambda體,(其實就是介面中抽象方法的具體實現)
 * 2.lambda表示式的使用,有六種情況
 *
 * 3.lambda表示式的本質:是作為對應的函式式介面的例項物件!
 * 所以記住:
 *      1)lambda表示式的返回值都是對應介面的例項物件;
 *      2)lambda表示式的語句是對應介面的方法的具體實現;
 */
public class LambdaTest02 {
    //情況1:無參,沒有返回值
    @Test
    public void test01(){
        Runnable run1 = new Runnable() {
            public void run() {
                System.out.println("實現Runnable介面的匿名類物件!");
            }
        };
        run1.run();
        System.out.println("================*==============");
        Runnable run2 = () -> System.out.println("Lambda表示式實現!");
        run2.run();
    }

    //情況2:有一個引數,沒有返回值
    @Test
    public void test02(){
        Consumer<String> con1 = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s+"是什麼呢?");
            }
        };
        con1.accept("eth");

        System.out.println("************************");
        Consumer<String> con2 = (String s) -> System.out.println(s+"是什麼呢?");
        con2.accept("eth和lambda");
    }

    //情況3:有引數,但是引數的資料型別可以省略,因為編譯器可以進行"型別推斷"
    @Test
    public void test03(){

        System.out.println("************************");
        Consumer<String> con2 = (s) -> System.out.println(s+"是什麼呢?");
        con2.accept("eth和lambda");

        System.out.println("************舉例說明型別推斷************");
        List<String> list = new ArrayList<>();//型別推斷ArrayList<這裡無需再寫資料型別>

        int[] arr1 = new int[]{1,2,3};//標準
        int[] arr2 = {1,2,3};//進行了型別推斷,簡化

    }

    //情況4:若是隻有一個引數,引數的小括號可以省略
    @Test
    public void test04(){

        Consumer<String> con1 = (s) -> System.out.println(s+"是什麼呢?");
        con1.accept("此時有形參的小括號");
        System.out.println("***********只有一個引數,可以去掉形參小括號*************");
        Consumer<String> con2 = s -> System.out.println(s+"是什麼呢?");
        con2.accept("此時有形參的小括號");
    }

    //情況5:Lambda 需要兩個或以上的引數,多條執行語句,並且可以有返回值
    @Test
    public void test05(){
        Comparator<String> com1 = new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                System.out.println(o1);
                System.out.println(o2);
                return o1.compareTo(o2);
            }
        };
        System.out.println(com1.compare("a","c"));
        System.out.println("************************");
        Comparator<String> com2 = (o1, o2) -> {
            System.out.println(o1);
            System.out.println(o2);
            return o1.compareTo(o2);
        };
        System.out.println(com1.compare("abc","abcdf"));
    }

    //情況6:當 Lambda 體只有一條語句時,return 與大括號若有,都可以省略
    @Test
    public void test06(){
        Comparator<String> com = (o1, o2) -> o1.compareTo(o2);
    }
}

總結:

  1. 對比了前後的程式碼,學會lambda表示式的語法;
  2. 初步知道什麼是函式式介面;

最重要的一點!!!lambda表示式的本質:是作為對應的函式式介面的例項物件!

所以把握住以下兩點進行理解:

  1. lambda表示式的返回值都是對應介面的例項物件;
  2. lambda表示式的語句是對應介面的方法的具體實現;

1.2 函式式(Functional)介面

函式式介面的定義:

  1. 只包含一個抽象方法的介面,稱為函式式介面。
  2. 可以通過 Lambda 表示式來建立該介面的物件。(若 Lambda 表示式丟擲一個受檢異常(即:非執行時異常),那麼該異常需要在目標介面的抽象方法上進行宣告)。
  3. 我們可以在一個介面上使用 @FunctionalInterface 註解,這樣做可以檢查它是否是一個函式式介面。同時 javadoc 也會包含一條宣告,說明這個介面是一個函式式介面。
  4. java.util.function包下定義了Java 8 的豐富的函式式介面

如何理解函式式介面

1. Java從誕生日起就是一直倡導“一切皆物件”,在Java裡面物件導向(OOP)程式設計是一切。但是隨著python、scala等語言的興起和新技術的挑戰,Java不得不做出調整以便支援更加廣泛的技術要求,也即java不但可以支援OOP還可以支援OOF(面向函式程式設計) 
2. 在函數語言程式設計語言當中,函式被當做一等公民對待。在將函式作為一等公民的程式語言中,Lambda表示式的型別是函式。但是在Java8中,有所不同。在Java8中,Lambda表示式是物件,而不是函式,它們必須依附於一類特別的物件型別——函式式介面。 
3. 簡單的說,在Java8中,Lambda表示式就是一個函式式介面的例項。這就是Lambda表示式和函式式介面的關係。也就是說,只要一個物件是函式式介面的例項,那麼該物件就可以用Lambda表示式來表示。
4. 所以以前用匿名實現類表示的現在都可以用Lambda表示式來寫。

1.2.1. Java內建四大核心函式式介面

java8的新特性之lambda表示式和方法引用

通過例子對函式式介面和lambda表示式再進行稍微深入的一點理解,慢慢思考消化。

package com.ethan.lambda;

import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
 *
 * java內建的四大核心函式式介面
 *      介面名                 核心(唯一抽象)方法的作用
 *  1.Consumer<T>       對型別為T的物件進行相關操作,包含方法:void accept(T t)
 *  2.Supplier<T>       返回型別為T的物件,包含方法: T get()
 *  3.Function<T,R>     對型別為T的物件進行操作,並返回結果。結果是R型別的物件。包含方法:R apply(T t)
 *  4.Predicate<T>      確定型別為T的物件是否滿足某約束條件或者說某種判斷規則,並返回boolean值。包含方法: boolean test(T t)
 *
 */
public class LambdaTest03 {

    /**
     * 傳統的方式和lambda表示式的對比,傳遞介面的例項並重寫其方法
     */
    @Test
    public void test01(){
        //1.方式1:例項一個介面
        Consumer<Double> consumer = new Consumer<Double>() {
            @Override
            public void accept(Double aDouble) {
                System.out.println("方式1:花了" + aDouble + "塊錢!");
            }
        };
        happyTime(500,consumer);

        System.out.println("============================");
        //2.方式2:匿名的形式例項化一個介面
        happyTime(500, new Consumer<Double>() {
            @Override
            public void accept(Double aDouble) {
                System.out.println("方式2:花了" + aDouble + "塊錢!");
            }
        });

        System.out.println("============================");
        //3.方式3:lambda表示式的形式例項化一個介面
        happyTime(300,aDouble -> System.out.println("方式3:花了" + aDouble + "塊錢!"));
    }

    /**
     * 定義一個方法,其中第二個引數需要傳遞為一個介面的例項
     * @param money
     * @param consumer
     */
    public void happyTime(double money, Consumer<Double> consumer){
        consumer.accept(money);
    }
    /**
     * 傳統的方式和lambda表示式的對比,傳遞介面的例項並重寫其方法
     *
     */
    @Test
    public void test02(){

        List<String> destList = Arrays.asList("北京","天津","南京","西京","東京","普京","河南","河北","湖南","廣東","湖北");
        //方式一:仍然以傳統的方式實現,不贅述
        List<String> strList = filterString(destList, new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return s.contains("京");
            }
        });
        System.out.println(strList);

        System.out.println("====================");
        //方式二:以lambda表示式實現
        List<String> strings = filterString(destList, s -> s.contains("河"));
        System.out.println(strings);
    }

    /**
     * 傳遞一個字串集合,根據某種規則過濾字串集合,此規則由Predicate的test方法決定
     * @param strList
     * @param pre
     * @return
     */
    public List<String> filterString(List<String> strList, Predicate<String> pre){
        List<String> targetList = new ArrayList<>();
        for (String s : strList) {
            if(pre.test(s)) {
                targetList.add(s);
            }
        }

        return targetList;
    }
}

1.2.2. Java的其他函式式介面

以下函式式介面是核心四大函式式介面的子介面,其實函數語言程式設計中,函式是一等公民,我們在java中這樣理解:

  1. 函式就是函式式介面中的那個唯一的抽象方法;
  2. 其形參引數,我們理解為我們數學中的多元一次方程中的自變數x,y,z,如果有返回值,那麼將返回值理解為因變數f(x);
  3. 函式式介面中的唯一的那個抽象方法只需要理解為對一個或多個物件進行相關的操作(這些操作就是我們自己要去寫程式碼實現的,但是為了能夠更加簡化語法,此時完全可以將這些操作用lambda表示式去實現,扔掉那些不必要的語句,比如資料型別的判斷,多餘的括號和return關鍵字)

java8的新特性之lambda表示式和方法引用

1.3 方法引用與構造器引用

1.3.1 方法引用

​ 理解方法引用之前,需要注意,其實方法引用需要你對jdk或者你專案中的方法極其熟悉,才能夠熟練使用;

建議:真實開發中其實更建議使用lambda表示式,方法引用理解為主。

方法引用的基本理解


# 1.方法引用的語法格式:類或者物件  :: 方法名
     具體分為以下三種情況:
  	1)物件 :: 非靜態方法
   	2)類 :: 靜態方法
	3)類 :: 非靜態方法(該情況下重點理解非靜態方法的呼叫者(即例項物件)其實是隱藏的形參,或者說非靜態方法的形參中隱藏了一個this引數)

# 2.方法引用的使用情景:
	當發現需要實現Lambda體的操作(即我們要寫的lambda表示式),
	其他的介面(java8開始介面可以有預設實現的方法了)或者類已經有實現相同功能的方法
	此時就可以使用方法引用!!!其他情況下目前不能使用。
	也就是說,使用方法引用的前提條件:
 		1)在能夠使用lambda表示式的前提下
		2)某個方法的引數型別和引數個數,返回值型別以及方法體都跟我們要寫的lambda表示式一致(適用於情況1和2)

# 3.方法引用的理解:
 	本質上就是lambda表示式,而lambda表示式就是作為函式式介面的一個例項物件,所以方法引用,也是函式式介面的例項物件。
 	可以將方法引用理解為在我們需要實現lambda表示式時候,
 	將已經存在的其他類的某個方法(注意這個方法的功能與我們要實現的lambda表示式的功能一致)呼叫過來(拿來主義),替代我們要寫的lambda表示式
 	可以這樣認為:在函式(方法)的層面上消除冗餘重複的方法體

總結:

  •  JDK在Lambda表示式的基礎上提出了方法引用的概念,允許我們複用當前專案(或JDK原始碼)中已經存在的且邏輯相同的方法。
    
  •  即,如果已經存在某個方法能完成你的需求,那麼你連Lambda表示式都別寫了,直接引用這個方法吧。
    

示例中理解方法引用:

package com.ethan.methodReferences;

public class MethodRefTest {
	/**
	 * 情況一:物件 :: 例項方法
	 * 我們知道Consumer是函式式介面,有唯一一個抽象方法void accept(T t)
	 * 我們又發現PrintStream中的void println(T t)方法,不論是返回值型別,形參型別和形參個數
	 * 都跟Consumer介面的void accept(T t)方法一樣,唯一不同的是方法名不一樣,並且假如我們要實現void accept(T t)方法
	 * 如果實現的功能(方法體)又跟PrintStream中的void println(T t)方法實現的一樣,那麼此時我們就可以使用方法引用
	 * 在函式(方法)層面去減少相同功能方法的重複書寫,直接呼叫PrintStream中的void println(T t)方法來代替我們要實現void accept(T t)方法
	 * 對於一個方法的完整定義來說,這兩個方法的方法名不一樣無關大雅,但是其他的定義都完全一致,實現的功能完全一致。所以可以替換。
	 * 這就是方法引用的意義,從函式方法層面去消除重複程式碼,實現相同功能。
	 */
	@Test
	public void test1() {
		//lambda表示式
		Consumer<String> con1 = s -> System.out.println(s);
		con1.accept("北京");
		System.out.println("======================");
		//方法引用
		Consumer<String> con2 = System.out::println;
		con2.accept("beijing");
		
	}

	/**
	 * 	Supplier中的T get()
	 * 	Employee中的String getName()
	 */
	@Test
	public void test2() {
		Employee emp = new Employee(11,"ETHAN",22,30000);
		Supplier<String> sup1 = ()-> emp.getName();
		System.out.println(sup1.get());
		System.out.println("======================");
		//方法引用
		Supplier<String> sup2 = emp::getName;
		System.out.println(sup2.get());

	}

	/**
	 *  情況二:類 :: 靜態方法
	 * 	Comparator中的int compare(T t1,T t2)
	 * 	Integer中的int compare(T t1,T t2)
	 */
	@Test
	public void test3() {

		Comparator<Integer> com1 = (t1, t2)-> Integer.compare(t1,t2);
		System.out.println(com1.compare(12, 22));
		System.out.println("========================");

		/**
		 * 這裡有個疑惑,我們說方法引用的條件是介面的抽象方法的返回值跟形參型別和個數都要一致,
		 * 但是嘗試Comparator介面的int compare(T t1,T t2)方法,引用Integer的compareTo方法仍然成功
		 * 情況3會解惑
		 */
		//Comparator<Integer> com2 = Integer ::compareTo;
		Comparator<Integer> com2 = Integer ::compare;
		System.out.println(com2.compare(13,111));
	}
	


	/**
	 * 	Function中的R apply(T t)
	 * 	Math中的Long round(Double d)
	 */
	@Test
	public void test4() {
		Function<Double,Long> fun1 = aDouble -> Math.round(aDouble);
		System.out.println(fun1.apply(12.3));
		System.out.println("========================");
		Function<Double,Long> fun2 = Math::round;
		System.out.println(fun2.apply(12.6));
	}



	/**
	 *  情況三:類 :: 例項方法
	 *  這種情況比較少見,一般其實自己寫lambda表示式就挺好的。
	 *
	 * 	 Comparator中的int compare(T t1,T t2)
	 * 	 String中的int t1.compareTo(t2)
	 * 	 上面跟我們一開始理解的方法引用的適用條件:
	 * 	 即某個方法的引數型別和引數個數,返回值型別以及方法體都跟我們要寫的lambda表示式一致(適用於以下情況1和2)
	 * 	 不一致!!!如何理解:
	 * 	 首先我們要有一個概念,非靜態方法的形參其實是隱含了一個形參變數,那就是我們這個非靜態方法的例項物件this
	 * 	 所以Comparator中的int comapre(T t1,T t2)可以將String中的int t1.compareTo(t2)進行方法引用,就是因為
	 * 	 String中的int t1.compareTo(t2)方法其實隱含了一個形參this(其實就是t1),
	 * 	 那麼加上的this就符合方法引用的條件,
	 * 	 Comparator中的int compare(T t1,T t2)
	 * 	 String中的int t1.compareTo(this(t1),t2)(this其實也就是Comparator中的int comapre(T t1,T t2)的t1形參。
	 *
	 *
	 * 	 這種情況下,可以這樣理解:
	 * 	 方法引用的方法的呼叫者t1,其實就是函式式介面的抽象方法中的第一個形參。
	 */
	@Test
	public void test5() {


		Comparator<String> c1 = (s1,s2)-> s1.compareTo(s2);
		System.out.println(c1.compare("abr","abc"));

		System.out.println("-==============================");
		Comparator<String> c2 = String::compareTo;
		System.out.println(c2.compare("abr","abc"));
	}



	/**
	 *
	 * 	BiPredicate中的boolean test(T t1, T t2);
	 * 	String中的boolean t1.equals(t2)
	 */
	@Test
	public void test6() {
		BiPredicate<String,String> bp1 = (s1,s2)->s1.equals(s2);
		System.out.println(bp1.test("aka","aba"));
		System.out.println("==============================");
		BiPredicate<String,String> bp2 = String::equals;
		System.out.println(bp2.test("aka","aka"));
	}
	


	/**
	 * 	 Function中的R apply(T t)
	 * 	 Employee中的String getName();
	 *
	 * 	 變形理解:
	 * 	 Function中的R apply(T t)		================================> 	Function中的R apply(T t);
	 * 	 Employee中的emp.getName() 	===getName隱藏了一個形參this(emp)==> 	Employee中的String getName(Employee emp);
	 */
	@Test
	public void test7() {
		Employee emp = new Employee(11,"ETHAN",22,30000);
		Function<Employee,String> fun1 = employee -> employee.getName();
		System.out.println(fun1.apply(emp));
		System.out.println("==============================");
		Function<Employee,String> fun2 = Employee::getName;
		System.out.println(fun2.apply(emp));
	}

}


//省略 get/set 構造器
class Employee {

	private int id;
	private String name;
	private int age;
	private double salary;
}

1.3.2 構造器引用和陣列引用

一、構造器引用

格式: ClassName::new

與函式式介面相結合,自動與函式式介面中方法相容。

  • 和方法引用類似,函式式介面的抽象方法的形參列表和構造器的形參列表一致。即對應抽象方法形參列表的構造器必須存在。

  • 抽象方法的返回值型別即為構造器所屬類的型別

二、陣列引用

格式: type[] :: new

  • 將陣列看作一個特殊的類,即與構造器引用型別了。
package com.ethan.methodReferences;

import org.junit.Test;

import java.util.Arrays;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;


public class ConstructorRefTest {


    /**
     * 構造器引用
     * Supplier中的T get()
     * Employee中的 new Employee();
     */
    @Test
    public void test1(){
        //最基礎的方式
        Supplier<Employee> sup1 = new Supplier<Employee>() {
            @Override
            public Employee get() {
                return new Employee();
            }
        };
        System.out.println(sup1.get());

        System.out.println("========================");
        //lambda表示式的方式
        Supplier<Employee> sup2 = ()-> new Employee();
        System.out.println(sup2.get());

        System.out.println("========================");

        //構造器引用的方式
        Supplier<Employee> sup3 = Employee::new;
        System.out.println(sup3.get());

	}

	//Function中的R apply(T t)
    @Test
    public void test2(){
        Function<Integer,Employee> fun1 = id-> new Employee(id);
        System.out.println(fun1.apply(1001));
        System.out.println("========================");
        Function<Integer,Employee> fun2 = Employee::new;
        System.out.println(fun1.apply(1002));

	}

	//BiFunction中的R apply(T t,U u)
    @Test
    public void test3(){
        BiFunction<Integer,String,Employee> bf1 = (id,name)->new Employee(id,name);
        System.out.println(bf1.apply(1001,"ethan"));

        System.out.println("=====================");
        BiFunction<Integer,String,Employee> bf2 = Employee::new;
        System.out.println(bf2.apply(1002,"ethal"));
	}

	//陣列引用
    //Function中的R apply(T t)
    @Test
    public void test4(){
        Function<Integer,String[]> fun1 = len -> new String[len];
        String[] strArr1 = fun1.apply(5);
        System.out.println(Arrays.toString(strArr1));
        System.out.println("=====================");
        Function<Integer,String[]> fun2 = String[] ::new;
        String[] strArr2 = fun2.apply(10);
        System.out.println(Arrays.toString(strArr2));

	}
}