lambda方法引用總結——燒腦喫透

lambda是java8的新特性,基本使用比較容易理解,但有一個環節遇到了坎兒,那就是方法引用,尤其是類的實例方法引用,燒腦之後總結一下。

在需要函數參數的方法中,我們可以把另一個同類型的方法直接傳入,這稱爲方法引用的綁定。類似於C語言中的函數指針。

lambda表達式可以替代方法引用;或者說方法引用是lambda的一種特例,方法引用不可以控制傳遞參數。

4.1) 構造器引用

private Person construntorRef(Supplier<Person> sup){
    Person p=sup.get();
    return p;
}

@Test
public void testConstructorRef(){
    Person p=construntorRef(Person::new);
    System.out.println(p);
}

需要有無參的構造器。

4.2) 靜態方法引用

    private static void print(String s){
        System.out.println(s);
    }

    @Test
    public void testStaticRef(){
        Arrays.asList("aa","bb","cc").forEach(TestMethodReference::print);
    }

so easy,只要靜態方法的參數列表和FI需要的參數一致就可以。

4.3) 成員方法引用

@Test
    public void testMemberMethodRef(){
        Arrays.asList("aa","bb","cc").forEach(System.out::println);
    }

so easy,只要成員方法的參數列表和FI需要的參數一致就可以。

4.4) 類的任意對象的實例方法引用(很怪異)

@Test
public void testClassMemberMethodRef(){
    String[] strs={"zzaa","xxbb","yycc"};
    Arrays.sort(strs,String::compareToIgnoreCase);//OK
    System.out.println(Arrays.asList(strs));
    File[] files = new File("C:").listFiles(File::isHidden); // OK
}

��,前方高能,請關掉耳機的音樂,認真思考,小心行事。

傳統的java開發中,是不允許使用類名去調用成員方法的,這是一個基本原則,那麼這裏的這種寫法就有點不太容易理解了。還是用實例說明:

用到的內部類:

import lombok.Data;

@Data
public static class Person{
    private String name;
    private Integer age;

    public int mycompare(Person p1){
        return p1.getAge()-this.getAge();
    }
    public void print(){
        System.out.println(this);
    }

    public void println(Person p){
        System.out.println(p);
    }

    public int compareByAge(Person a,Person b){
        return a.getAge().compareTo(b.getAge());
    }
}

public static class APerson{
    public void print(){
        System.out.println(this);
    }

    public void println(Person p){
        System.out.println(p);
    }
}

測試代碼:

@Test
    public void testClassMemberMethodRef2() {
        // R apply(T t);//要求一個參數
        Function<String, String> upperfier1 = String::toUpperCase;
        UnaryOperator<String> upperfier2 = (x) -> x.toUpperCase();//這裏沒有參數,即0個

        /*
         * 小結:如果方法引用表達式 "String::toUpperCase" 可以用lambda表達式中參數的指定成員方法(這個成員方法的參數比FI要求的參數少一個改類型的參數)改寫,
         *  那麼就可以使用 "類的實例方法"來表示方法引用。
         *  
         *  或者說:如果lambda表達式的lambda體中使用的方法是參數匹配的方法,那麼方法引用表達式就用"類引用對象的實例方法"。
         *  
         *  lambda的參數是方法的主體。
         */


        class Print {
            public void println(String s) {
                System.out.println(s);
            }
        }
        // void accept(T t);

        Consumer<String> sysout1 = new Print()::println;
        Consumer<String> sysout2 = (x) -> new Print().println(x);

        /*
         * 小結:如果方法引用表達式 "new Print()::println" 可以用lambda表達式中參數的具體對象的參數匹配的成員方法改寫,
         *  那麼就用 "對象的實例方法"來表示方法引用。
         *  
         *  或者說:如果lambda表達式的lambda體中使用的方法來操作lambda的參數,那麼方法引用表達式就用"對象的實例方法"。
         *  
         *  lambda的參數是方法的參數。
         */

        //有一個更讓人易混淆的例子,可以用上面的規則來驗證,Arrays.sort(T t,Comparator<? extends t> c)

        class Person {
            public int com1(Person p) {
                return 1;
            }

            public int com2(Person p1, Person p2) {
                return 1;
            }
        }

        // int compare(T o1, T o2);//需要兩個參數

        Person【】 ps = { new Person(), new Person() };
        Arrays.sort(ps, Person::com1);
        Arrays.sort(ps, (x,y)->x.com1(y));

        Arrays.sort(ps, new Person()::com2);
        Arrays.sort(ps, (x,y)->new Person().com2(x, y));

        //按照以上規則驗證應該能說明清楚。

        /*
         * 但是一個接口爲什麼有兩種寫法?缺省的lambda會匹配FI方法,即"int compare(T o1, T o2);"
         * 從上面的lambda表達式來分析,默認的使用lambda應該是:
         */

        Comparator<Person> comparator1 = new Person()::com2;

        /*
         * 下面的方式又是怎麼回事呢?
         */
        Comparator<Person> comparator2 = Person::com1;
        System.out.println(comparator2);

        /*
        *   任一個兩個參數的FI接口方法(int compare(T o1, T o2)),都可以用引用減少一個參數的方法(int o1<T>.compare(T o2))來代替,而引用對象本身作爲另一個隱含參數,那麼方法引用的對象用類名,表示類的任意對象。

        還是有點亂?我們來換一個角度來看一下:
        首先,我們需要的是int compare(T o1, T o2)是兩個參數;
        其次,先不考慮::前綴是類還是對象,你給了我一個compare(T o2),少一個參數?怎麼辦?
        lambda機制爲了解決這個問題,它使用::前面的類名new一個對象,當做需要的缺少的那個參數,這就是類的實例方法。
        */
    }

小結一下:

首先明確此處需要的方法參數列表,此處標記參數個數爲N,那麼:

1. 如果傳入的方法是一個類型的靜態方法,而且參數匹配,使用“類的靜態方法引用”;這應該不難理解。

2. 如果傳入的方法是一個實例的成員方法,而且參數匹配,使用“實例的成員方法”;這也應該不難理解。

3. 如果傳入的方法是一個類型T的實例的成員方法,而且參數爲N-1個,缺少了一個T類型的參數,那麼就使用“T類型的實例方法”。

燒腦分析類的實例方法省略了哪個參數

前面的例子,FI的兩個參數是同一個類型,如果類型不同呢?省略了哪個參數呢?

是按照位置省略了第一個,亦或者是省略了最後一個?

還是按照類型自動去對應,而不關心第幾個呢?

這個時候,我們能想到的辦法可能是去看源碼,但是一般看代碼沒有個把禮拜甚至更長,毛都看不出來。我們還是用一個例子來分析一下吧。

定義一個FI接口:

package com.pollyduan.fi;

public interface TestInterface {
    //隨便什麼名字,lambda並不關心,因爲FI只有一個接口,並且根據參數來匹配
    public void anyStringAsName(TestBean1 bean1,TestBean2 bean2);
}

編寫兩個用於參數的類:

TestBean1.java

package com.pollyduan.fi;

public class TestBean1 {
    public void expect1(TestBean1 bean1){

    }
    public void expect2(TestBean2 bean2){

    }
    public void test1(TestInterface i){

    }
}

TestBean2.java

package com.pollyduan.fi;

public class TestBean2 {
    public void expect1(TestBean1 bean1){

    }
    public void expect2(TestBean2 bean2){

    }
    public void test1(TestInterface i){

    }
}

二者區別不大。

編寫測試類:

package com.pollyduan.fi;

public class TestFIMain {
    public static void main(String[] args) {
        TestBean1 bean1=new TestBean1();
        bean1.test1(TestBean1::expect1);//①
        bean1.test1(TestBean1::expect2);//② ok
        bean1.test1(TestBean2::expect1);//③
        bean1.test1(TestBean2::expect2);//④

        TestBean2 bean2=new TestBean2();
        bean2.test1(TestBean1::expect1);//⑤
        bean2.test1(TestBean1::expect2);//⑥ ok
        bean2.test1(TestBean2::expect1);//⑦
        bean2.test1(TestBean2::expect2);//⑧
    }
}

測試方法中,除了標記OK的行正確,其他都報錯。

分析:

首先我們要明確FI需要的參數列表是:(TestBean1,TestBean2)

  1. 我們先看①行,我們傳入的”::”前導的類是TestBean1,而expect1方法匹配的是TestBean1類型的入參bean1,也就是說省略了TestBean2類型的參數bean2,FI中的最後一個參數。即便我們使用類TestBean1去new一個對象,也找不到TestBean2,因此這個錯誤。

  2. 我們先看②行,我們傳入的”::”前導的類是TestBean1,而expect2方法匹配的是TestBean2類型的入參bean2,也就是說省略了TestBean1類型的參數bean1,那麼lambda就可以使用”::”前導的TestBean1構建一個對象,作爲第一個參數,從而匹配FI的接口方法。ok。

  3. 我們先看③行,我們傳入的”::”前導的類是TestBean2,而expect1方法匹配的是TestBean1類型的入參bean1,也就是說省略了TestBean2類型的參數bean2,FI的最後一個參數。按照第二步的分析,我們用”::”前導的類TestBean2去new一個對象,應該可以湊足兩個參數。實際測試會發現這不靈。這就證明了只能省略第一個參數,而且,用”::”前導的類也必須是第一個參數的類型。

  4. 同第一步類似,第④行代碼,找不到TestBean1的參數,有錯誤可以理解。

  5. 至於⑤~⑧,只是替換了外層的test1的主體,沒有任何區別。這證明了,lambda的匹配與外層是什麼鬼沒有任何關係,它只關心外層需要的FI的參數列表。

  6. 請不要看下一步,在這裏停下來冷靜的思考一下,如果我們把TestInterface中FI方法的參數位置換一下,即public void anyStringAsName(TestBean2 cat,TestBean1 dog);,結果應該是哪兩行正確呢?認真思考一下,實在想不明白跑一下測試用例,也許對理解更有幫助。

  7. 如果想明白了用這個思路驗證一下:參照參數列表(TestBean2,TestBean1),可以確定只可以省略第一個參數即TestBean2,那麼”::”簽到必須是TestBean2,用於自動創建對象;而未省略的參數是TestBean1,那麼方法名爲expect1,結果爲xxx(TestBean2::expect1),即③和⑦,你答對了嗎?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章