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

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內置四大核心函數式介面

通過例子對函數式介面和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關鍵字)

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

	}
}