Java-Lambda表達式和“方法引用”的對比和詳解

Lambda表達式


一、Lambda表達式簡介


1.1什麼是Lamdba表達式?


 Lambda表達式是Java 8 添加的一個新特性,可以認爲,Lambda是一個匿名函數(相似於匿名內部類),作用是返回一個實現了接口的對象(這個觀點非常重要,貫穿於Lambda表達式的整個使用過程)。

1.2爲什麼使用Lambada表達式?


 使用Lambda表達式對比於其他接口實現方式顯得非常簡潔。(詳見3種接口實現的方法代碼塊CodeBlock-1)

1.3Lambda對接口的要求?


 雖然Lambda表達式對某些接口進行簡單的實現,但是並不是所有的接口都可以使用Lambda表達式來實現,要求接口種定義的必須要實現的抽象方法只能是一個(注意:具體方法可以多個或者沒有)。
 

在Java 8 中對接口增加了新特性:default,提供了一個默認的抽象方法,但是Lambda對此沒有特殊的
影響,方法可以按Lambda所表達的來。

1.4注意事項:


 @FunctionalInterface
 這個註解用於修飾函數式接口,即意味着接口中的抽象方法只能有一個,否則編譯器會報錯。
我們總是需要對象來實現接口,Lambda表達式就是幫助我們簡化這個過程,而對象中的單獨的方法在對象的創建接口對象的創建過程中並不會執行。4.2小節中構造方法在Lambda表達式中的調用,其更像一種工廠方法返回一個對象的引用,在創建實現接口的對象的時候工廠方法並不被執行。

1.5Lambda表達式的延遲執行原因


(以下來源於Java 核心技術 卷一)
 使用lambda表達式的重點是延遲執行(deffered execution)。畢竟,如果想要立即執行代碼,完全可以直接執行,而無需將它包裝到一個lambda表達式中。之所以希望以後再執行代碼,這有很多原因,比如:

  • 在一個單獨的線程中運行代碼
  • 多次運行代碼、在算法的適當位置運行代碼(例如,排序的比較操作)
  • 發生某種情況時執行代碼(如,點擊了一個按鈕,數據到達,等等)】
  • 只在必要時才運行代碼

1.6接口實現的不同方式

 實現接口的對象創建方式有三種,如CodeBlock-1代碼所示,分爲:

  1. 使用接口實現類(接口對象指向已實現了接口的類對象
  2. 使用匿名內部類實現接口
  3. 使用lambda表達式來實現接口
    CodeBlock-1:三種不同方法來實現接口
public class Program {
    public static void main(String[] args) {
        /***
         * 1.使用接口實現類
         */

        Comparator comparator = new MyComparator();

        /**
         * 2.使用匿名內部類實現接口
         */

        Comparator comparator1 = new Comparator() {
            @Override
            public int compare(int a, int b) {
                return a - b;
            }
        };

        /**
         * 3.使用lambda表達式來實現接口
         *
         */
        Comparator comparator2 = (a, b) -> a - b;


        /**
         * 測試部分 若不出錯,則顯示三個-1
         */

        System.out.println(comparator.compare(1, 2));
        System.out.println(comparator1.compare(1, 2));
        System.out.println(comparator2.compare(1, 2));
    }
}


class MyComparator implements Comparator {
    @Override
    public int compare(int a, int b) {
        return a - b;
    }
}

@FunctionalInterface
interface Comparator {
    int compare(int a, int b);
}

二、Lambda表達式的基礎語法


2.1基本語法注意事項

 

  1. Lambda表達式是一個匿名函數
  2. 關注重點:參數列表 方法體
  3. 小括號():=用來描述一個參數列表(形參)
  4. 大括號{} 來描述一個方法體
  5. ->:即Lambda運算符,讀作goes to ,用於分割參數列表和方法體

2.2代碼實例


 我們用Lambda表達式的多種形式,分爲有無返回值的普通方法(構造方法後面再講),無參、一參、多參方法,總共有6個方法。所以定義了多個擁有不同方法的接口。從接口的命名方式就可以知道其意味着的含義(如果不在同一包種,注意import)。
CodeBlock-2:6種不同的接口定義:
 

//1.無返回值的多參接口
@FunctionalInterface
public interface LambdaNoneReturnMultipleParameter {
    void test(int a, int b);
}
//2.無返回值的無參接口
@FunctionalInterface
public interface LambdaNoneReturnNoneParameter {
    void test();
}
//3.無返回值的一參接口
@FunctionalInterface
public interface LambdaNoneReturnSingleParameter {
    void test(int n );
}

//4.有返回值的多參接口
@FunctionalInterface
public interface LambdaSingleReturnMultipleParameter {
    int test(int a, int b);
}
//5.有返回值的無參接口
@FunctionalInterface
public interface LambdaSingleReturnNoneParameter {
    int test();
}
//6.有返回值的一參接口
@FunctionalInterface
public interface LambdaSingleReturnSingleParameter {
    int test(int n);
}

CodeBlock-3:6種不同的接口的Lambda表達式應用:

/**
 * Lambda表達式的基礎語法
 */
public class Syntax1 {
    public static void main(String[] args) {
        /**
         * 1.無參無返回的Lambda表達式使用樣例
         */
        LambdaNoneReturnNoneParameter lambda1 = () -> {
            System.out.println("lambda1:" + "Hello World!");
        };
        lambda1.test();

        /**
         * 2.無返回值的單參數的Lambda表達式使用樣例
         */

        LambdaNoneReturnSingleParameter lambda2 = (int i) -> {
            System.out.println("lambda2:" + i);
        };
        lambda2.test(1024);


        /**
         * 3.無返回值的多參數的Lambda表達式使用樣例
         */
        LambdaNoneReturnMultipleParameter lambda3 = (int a, int b) ->
        {
            System.out.println("lambda3:" + (a + b));
        };
        lambda3.test(1000, 24);

        /**
         * 4.有返回值的無參數的Lambda表達式使用樣例
         */

        LambdaSingleReturnNoneParameter lambda4 = () -> {
            return 1024;
        };
        int res = lambda4.test();
        System.out.println("lambda4:" + res);

        /**
         * 5.有返回值,單個參數的Lambdad的表達式使用
         */

        LambdaSingleReturnSingleParameter lambda5 = (int a) -> {
            return a;
        };
        int res2 = lambda5.test(1024);
        System.out.println("lambda5:" + res2);

        /**
         * 6.有返回值,多個參數的Lambdad的表達式使用
         */
        LambdaSingleReturnMultipleParameter lambda6 = (int a, int b) -> {
            int sum = a + b;
            return sum;
        };
        int res3 = lambda6.test(1000, 24);
        System.out.println("lambda6:" + res3);


    }


}

三、Lambda表達式語法精簡


 從Lambda表達式的基礎語法樣例中我們幾乎沒有看Lambda語法的優勢,特別是和匿名內部類對比,更是沒發現Lambda帶來的代碼的優雅和簡化。但是,Lambda語法提供了合理的代碼化簡方式,我們在第三章中進行Lambda表達式的簡化的學習。

3.1Lambda表達式精簡的方式:


1.參數類型的精簡:
由於在接口中已經定義了參數,所以在Lambda表達式中參數的類型可以省略;
備註:如果需要進行省略類型,那麼所有參數的類型都必須都得省略,省略部分會報錯;
匿名內部類中省略參數類型是不可取的,這是Lambda表達式的優勢;

2.小括號的精簡:
如果參數列表中,參數的個數有且只有一個(多了少了都不行),那麼小括號可以省略,且仍然可以省略參數的類型

3.方法大括號的精簡:
類似於if,while語句,如果語句塊只有一條語句,那麼此時大括號可以省略

4.return的省略:
如果出現接口只有唯一方法且方法中只有唯一語句,且是返回語句,那麼如果要省略,只能一起省略掉大括號以及return,不能省略其中之一,否則會報錯。


3.2 代碼實例


 此處所使用到的接口仍然是CodeBlock-2代碼塊所定義的。

CodeBlock-4:四種接口精簡的方式代碼案例:

/**
 * 此類用於語法精簡的Lambda表達式演示
 */
public class Syntax2 {
    /**
     * 參數精簡
     * 1.參數的精簡
     * 由於在接口中已經定義了參數,所以在Lambda表達式中參數的類型可以省略
     * 備註:如果需要進行省略類型,但是所有參數的類型必須都得省略,省略部分會報錯
     * 匿名內部類中省略參數類型是不可取的
     */

    LambdaNoneReturnMultipleParameter lambda1 = (a, b) -> {
        System.out.println(a + b);

    };
    /**
     * 2.精簡參數小括號
     * 如果參數列表中,參數的個數有且只有一個(多了少了都不行),那麼小括號可以省略
     * 且仍然可以省略參數的類型
     */
    LambdaNoneReturnSingleParameter lambda2 = a -> {
        System.out.println(a);
    };

    /**
     * 3.方法大括號的省略
     * 類似於if,while語句,如果語句塊只有一條語句,那麼此時大括號可以省略、
     * 前面的省略方式仍然成立
     */
    LambdaNoneReturnSingleParameter lambda3 = a ->
            System.out.println(a);
    /**
     * 4.如果接口的唯一方法只有唯一返回語句,那麼可以省略大括號,但是在省略大號的同時必須省略return
     */
    LambdaSingleReturnNoneParameter lambda4 = () -> 10;

}

四、 Lambda表達式進階之函數引用

方法引用的提出: 由於如果存在一種情況,我們新建了多個接口的實現對象,其方法都是相同的,但是如果方法需要修改,那麼修改的複雜度就隨着對象數量的上升而上升。
方法引用的定義: 快速將一個Lambda表達式的實現指向一個已經寫好的方法方法引用可以看作是lambda表達式的特殊形式,或者稱之爲語法糖。一般方法已經存在纔可以使用方法引用,而方法若未存在,則只能使用lambda表達式。

 我們可以採用兩種方式來在Lambda表達式中調用其他方法,第一種如一般的方法調用,第二種就是方法引用。
方法引用的語法說明:
 即:“方法的隸屬者::方法名”。方法的隸屬者,即靜態方法隸屬者爲類,非靜態方法的隸屬者是對象(隸屬者不是接口,而是定義引用方法的類或者對象)。

注意事項:
 1.被引用的方法的參數數量以及類型一定要和接口中的方法參數數目一致;
 2.被引用的方法的返回值一定要和接口中的方法返回值一致,方法引用這個整體表達式可以返回函數式接口的實現對象,但其調用/引用的方法其返回類型絕不是接口實例對象;
 3.方法名的後面沒有括號“()”;
 4.方法的引用是可以有多個參數入口的,雖然再::表達式中沒有體現(由於沒有小括號),但是接口中對其已有所規定了;

4.1 普通方法在Lambda表達式中的調用

CodeBlock-5:2種不同的普通方法調用的樣例說明:

public class Syntax3 {
    public static void main(String[] args) {
/**
 *方法引用:可以快速將一個Lambda表達式的實現指向一個已經寫好的方法
 *語法:方法的隸屬者,靜態方法隸屬者爲類,非靜態方法的隸屬者是對象
 * 即:“方法的隸屬者:方法名”
 * 
 * 注意事項:
 * 1.被引用的方法的參數數量以及類型一定要和接口中的方法參數數目一致
 * 2.被引用的方法的返回值一定要和接口中的方法返回值一致
 *
 *
 * 假如我們在程序中對於某個接口方法需要調用許多次,那麼用以下的方法創建對象,來調用方法就是不太好的
 * 缺點:如果將來要對方法進行改變,那麼所有用Lambda表達式定義的對象都要更改,這在設計模式上就是有問題的;
 *      */
        LambdaSingleReturnSingleParameter lambda1 = a -> a * 2;
        LambdaSingleReturnSingleParameter lambda2 = a -> a * 2;

        /**
         * 我們一般是寫一個通用的方法,並將其引用至Lambda表達式中
         *
         * */

        LambdaSingleReturnSingleParameter lambda3 = a -> change(a);//在Lambda表達式中使用一般方法的調用方式
        LambdaSingleReturnSingleParameter lambda4 = Syntax3::change;//在Lambda表達式種使用方法引用(方法隸屬於類)
		System.out.println(lambda4.test(2));
		Syntax3 syntax3 = new Syntax3();//非靜態方法需要對象才能被調用
        LambdaSingleReturnSingleParameter lambda5 = syntax3::change2;//在Lambda表達式種使用方法引用(方法隸屬於對象)
		LambdaSingleReturnMultipleParameter lambda6 = syntax3::change3;//多參數的引用方法使用
    }

    private static int change(int a) {
        return a * 2;
    }

    private int change2(int a) {
        return a * 2;
    }
}
    private int change3(int a, int b) {
        return a * 2 + b * 3;
    }

4.2 構造方法在Lambda表達式中的調用

 Person類具有無參和有參構造方法。
CodeBlock-6:定義一個類,構造方法創建的對象

public class Person {
    public String name;
    public int age;

    public Person() {
        System.out.println("Person類的無參構造方法執行了");//語句用於判斷無參構造器是否執行了
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("方法的有參構造方法執行了");//語句用於判斷有參構造器是否執行了
    }
}

CodeBlock-7:Lambda表達式中引用構造方法的樣例:

public class Syntax4 {

    public static void main(String[] args) {

        PersonCreater person = () -> new Person();

        /**構造方法的引用
         * 有參和無參構造器的調用區別在於所定義的接口中構造方法的參數區別
         */

        PersonCreater creater1 = Person::new;
        //無參
        Person person1 = creater1.getPerson();
        //有參
        PersonCreater2 creater2=Person::new;
        Person person2 = creater2.getPerson("Fisherman",18 );

    }

}

//需求爲:一個返回一個Person類的接口

interface PersonCreater {
    Person getPerson();

}

interface PersonCreater2 {
    Person getPerson(String name, int age);

}

數組的構造方法引用:
語法格式:TypeName[]::new等價於lambda表達式:x -> new int[x]

IntFunction<int[]> arrayMaker = int[]::new;//假設有一個返回int類型數組的函數式接口
int[] array = arrayMaker.apply(10) // 創建數組 int[10]

注意事項:

1.在Lambda表達式中,一個接口要麼對應一個無參構造方法,要麼含有一個有參構造方式,其在接口中所定義的抽象返回Person對象的方法已經決定有參還是無參了。
2.構造方法和靜態方法一樣都是隸屬於類的方法
3.構造方法不同於一般靜態方法“類名::方法名一樣調用”,而是採用"類名::new"的方式來進行構造方法的調用。
使用new關鍵字是爲了明確地知道調用的是構造函數。而其並不需要入口參數的原因是因爲編譯器完全可以通過接口的定義推斷出參數類型和個數。構造方法的方法引用和普通方法引用並沒有本質的區別,比如在CodeBlock-5中用change(a)來實現原接口中定義的返回整形數據的test方法,而new關鍵字使用對應形參的構造器來實現接口中定義的返回Person對象的getPerson方法。
4.::在IDE(比如Intllij IDEA)中總是指向當前方法引用實現的函數式接口,以此可以方便地確定方法引用所實現的函數式接口爲哪個。

4.3 方法引用的格式總結:

上述s代表形參,限於篇幅,只象徵性地寫了一個。其數目可以不爲1,爲0,2,3…都可以。

4.4 方法引用和Lambda表達式的對比:

 方法引用比Lambda表達式更加簡潔,但同時也更難理解其語法,所以我們以下用做對比的方法來理解表達式。

4.4.1 靜態方法引用
組成語法格式:ClassName::staticMethodName

  • 靜態方法引用比較容易理解,和靜態方法調用的lambda表達式相比,只是把 .換爲 ::
  • 在目標類型兼容的任何地方,都可以使用靜態方法引用。此時,類是靜態方法動作的發起者。
  • 例子:
  •   String::valueOf等價於lambda表達式 (s) -> String.valueOf(s)
  •   Math::pow等價於lambda表達式 (x, y) -> Math.pow(x, y);
     

4.4.2 特定實例對象的方法引用

實例方法引用又分以下三種類型:

實例上的實例方法引用
這種語法與用於靜態方法的語法類似,只不過這裏使用對象引用而不是類名。此時,對象是方法動作的發起者。
語法格式:instanceReference::methodName
例子:
instanceReference::methodName相當於(無參或有參)->instanceReference.methodName(數目相同的參數)
由於對象需要構造,故在下面給出代碼示例。
 

public class Test {
   public static void main(String[] args) {
   
       Power powerObject = new Power();
       Function<Integer,Integer> function1 = a->powerObject.power(a);
       Function<Integer,Integer> function2 = powerObject::power;
   	/**
   	*不管哪種實現,方法的調用是相同的,都用接口的已實現抽象方法名調用。
   	*/
       System.out.println(function1.apply(2));
       System.out.println(function2.apply(3));
   }

}

class Power {
   public int power (int a ){
       return a*a;
   }
}

2.超類上的實例方法引用
語法格式:super::methodName
方法的名稱由methodName指定,通過使用super,可以引用方法的超類版本。
例子:
還可以捕獲this 指針,this :: equals 等價於lambda表達式 x -> this.equals(x);

3類上的實例方法引用(特定類的任意對象的方法引用)
語法格式:ClassName::methodName
這裏和類調用靜態方法以及對象調用實例都不相同, ::前的類不在是實例方法的發出者,那發出者是誰呢?我們憑藉此方法引用的格式也找不到究竟誰是動作的發起者?實際上真正的發起者是ClassName類鎖創建的任意一個對象,只不過,在方法調用的時候需要將引用對象作爲參數輸入到方法中,並且規定,此對象一定要位於方法參數的第一個。
代碼案例:

注意:
 若類型的實例方法是泛型的,就需要在::分隔符前提供類型參數,或者(多數情況下)利用目標類型推導出其類型,並不需要加類型參數,但是遇到分隔符前使用了強制類型轉換應當看得懂其用意。
例子:
String::toString等價於lambda表達式 (s) -> s.toString()而絕不等於String.toString(),因爲非靜態方法只允許對象調用,而不能是類。但是即使這樣講,你可能還是不明白,那我給以下淺顯易懂的例子,你一定都能夠掌握它。

 我們定義一個計算平方的方法,輸入參數爲”幾“次方,而對於Power接口的不同實現,實現了求冪的底數規定,比如:PowerOfTwo類實現power(i)方法是求2的i次冪,PowerOfThree類實現power(i)方法是求3的i次冪。
類::實例方法的CodeBlock-1:
 

import java.net.InterfaceAddress;
import java.util.function.BiFunction;
import java.util.function.Function;

public interface Power {
    int power(int i);
}

class PowerOfTwo implements Power {
    public int power(int i) {
        return (int) Math.pow(2, i);

    }
}

class PowerOfThree implements Power {
    public int power(int i) {
        return (int) Math.pow(3, i);

    }
}


class Test {
    public static void main(String[] args) {
        /**
         * 之所以使用BiFunction作爲函數式接口,是因爲其爲2輸入參數,1個返回值。
         * 正好符合此例中,類名.實例方法的調用規則
         */
        Power powerObject1 = new PowerOfTwo();
        Power powerObject2 = new PowerOfThree();

        BiFunction<Power, Integer, Integer> function = Power::power;
        
        System.out.println(function.apply(powerObject1, 4));//輸出"2"的4次方:16
        System.out.println(function.apply(powerObject2, 4));//輸出"3"的4次方:81


    }

}

 只需要一個BiFunction接口的實現類,就能實現方法調用的多態(同一的方法名,由於對象名的不同而有不一樣的操作)。這裏多態的形成也是由於父類接口指向不同子類對象實現形成的。
 我們應當將BiFunction<Power, Integer, Integer> function = Power::power;以及function.apply(powerObject1, 4)這兩個步驟結合起來看,而不是孤立的。前者規定了方法輸入參數類型,返回值類型,且規定了方法第一個參數必須得是實際調用該方法的對象,實現了後者的apply方法(代碼實現,並未執行)。後者則輸入了對應參數,執行相關程序。
 如果你問我爲什麼需要這樣的方法引用形式,那麼最大的原因就其在具體方法調用時將對象作爲參數輸入到方法調用中呢,這增加了多態在方法引用中的便利性,如上述例子所示。如果採用其他方法引用方式,將產生多個接口的實例,而此方式只需要一個接口的實例。

 類上實例方法的引用的第二個例子:
類::實例方法的CodeBlock-2:

public class LowercaseToUppercase {

    public static void main(String[] args) {

        List<String> list = Arrays.asList("hello", "world", "hello world");
        
        list.stream().map(String::toUpperCase).forEach(System.out::println);
    }
}

 如果你還沒學過流,不影響這裏的理解。我們Crtl+左鍵+::,可以看到map類所需實現的是一個Function接口,即一個輸入,一個輸出。但是String::toUpperCase作爲一個方法引用,返回值是確定的,因爲toUpperCase方法顯然會返回一個String類型對象:大寫的String對象;但是輸入參數是哪個呢,這似乎難以判斷了,因爲toUpperCase方法是一個無參的函數。但是JVM模型告訴我們:所有實例的實例方法,第一個參數都是一個隱式的this。map方法,對流的元素進行了映射(小寫至大寫),而方法中隱式的參數this即這每一個對象。
 分步驟結合JDK源碼解釋:

  1. map(String::toUpperCase),向map方法內傳入一個方法引用所形成的接口實現對象
  2. <R> Stream<R> map(Function<? super T, ? extends R> mapper);,map是一個返回Stream對象,輸入爲實現Function接口的對象即1中的方法引用
  3. R apply(T t);此是Function接口所要實現的方法,即使用toUpperCase()方法去實現它。
  4. public String toUpperCase() { return toUpperCase(Locale.getDefault());},這是toUpperCase()方法的源碼,其是一個無參返回String類對象的非靜態方法。
  5. 接口所需實現的方法的形參T t則對應任意一個String對象。

如果你有關於“爲何例子1中的apply方法調用中方法的第一個參數爲執行操作對象的引用,而第二個例子中卻從未輸入過對象的引用”的疑惑,那麼說明你理解到關鍵位置了。實際上後者是map調用Function接口對象返回給stream,最後被流中的迭代方法調用了apply方法,其最終也是將對象引用作爲第一個輸入參數調用了apply方法,所以兩個例子通過同一種方式達成了類名::實例方法,只不過一個顯式,一個隱式。


4.4.3 構造方法引用

參看4.2小節內容。

5. 總結

總結: Lambda表達式和方法引用的目的都是使用具體的方法來代替接口中抽象的方法,但是在實際使用中,調用的是接口中被實現的方法名,lambda表達式和方法引用只應用於接口實例的方式實現了的構造過程,例如在CodeBlock-6/7中的代碼:

//1.這是需要被實現的抽象方法,方法名:getPerson
interface PersonCreater {
    Person getPerson();

}

//2.這是使用方法引用實現了抽象方法的對象(返回的是一個被實現了抽象方法的接口的實例)
    PersonCreater creater1 = Person::new;

//3.這是調用接口實例的實現方法,返回一個Person對象,分爲有參和無參。
	Person person1 = creater1.getPerson();

6.補充:常用函數式接口

發佈了58 篇原創文章 · 獲贊 12 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章