JDK的方法引用(Method Reference)

前言

今天在讀源碼過程中,看到一段比較抽象的代碼,一時理解不了,停下來琢磨了一會兒。查閱相關資料發(fā)現(xiàn)這塊代碼涉及JDK的Method reference(方法引用),特做此記錄,方便日后回顧。下面直接來看該源碼中的寫法:

// 接口定義
public interface NotifyListener {
    void notify(List<String> urls);
}

// 接口使用,當時看到這塊代碼內心有無數(shù)個?
final AtomicReference<List<String>> reference = new AtomicReference<>();
NotifyListener listener = reference::set;
listener.notify(Arrays.asList("hello world"));

對比通常的匿名內部類寫法:

//通常情況下匿名內部類的寫法
NotifyListener listener1 = new NotifyListener() {
     @Override
     public void notify(List<String> urls) {
           reference.set(urls); 
     }
};

明顯源碼中的寫法更為簡潔,易讀性也沒有受影響,那么,這種寫法有依據(jù)嗎?當然有,就是今天我們要說的Method reference,為了更清楚理解方法引用 "::" 的使用,扒了下官網(wǎng)對Method reference的介紹:在lambda表達式的基礎上,方法引用可以幫助我們構建更為簡潔、緊湊、易讀的代碼。下面我們從嵌套類(NestClass)開始,了解如何一步步將代碼變得更為簡潔。

一、嵌套類(NestedClass)

Java語法允許在一個類的內部定義類,這個內部定義類稱之為嵌套類,嵌套類通常分為內部類靜態(tài)嵌套類兩種。例如:

class OuterClass {
    ...
    // 內部類
    class InnerClass {
        ...
    }
    //靜態(tài)嵌套類 
    static class StaticNestedClass {
        ...
    }
}

內部類

與實例方法和變量一樣,內部類與其外部類的實例相關聯(lián),并可以直接訪問外部類實例對象的方法和字段。實例化內部類時,必須先初始化外部類。外部類實例中創(chuàng)建內部類實例的語法如下:

OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();

靜態(tài)嵌套類

與類方法和變量一樣,靜態(tài)嵌套類與其外部類相關聯(lián)。和靜態(tài)類方法一樣,靜態(tài)嵌套類不能直接引用外部類中定義的實例變量或方法:它只能通過對象引用來使用它們。靜態(tài)嵌套類與外部類的交互方式與其他頂級類一樣,事實上,靜態(tài)嵌套類的行為與在同一個包下的其他頂級類一樣。靜態(tài)嵌套類實例化也與頂級類一樣:

StaticNestedClass staticNestedObject = new StaticNestedClass();

屏蔽(shadowing)

特定作用域(內部類或者方法)內的類型聲明(成員變量或者參數(shù)名)與封閉域(通常指代嵌套類所在的類或者方法作用域)內的其他聲明名稱相同,那么前者會屏蔽后者,也就是說此時變量值優(yōu)先從該作用域取,我們來看個例子:

public class ShadowTest {

    public int x = 0;

    class FirstLevel {
        public int x = 1;

        void methodInFirstLevel(int x) {
            // x = 23
            System.out.println("x = " + x);
            // this.x = 1
            System.out.println("this.x = " + this.x);
            // ShadowTest.this.x = 0
            System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
        }
    }

    public static void main(String... args) {
        ShadowTest st = new ShadowTest();
        ShadowTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

序列化

強烈建議不要序列化內部類(包括局部類和匿名類)。當Java編譯器編譯某些特定結構,比如內部類時,會創(chuàng)建合成結構;這些合成結構在源代碼中沒有相應結構的類、方法、字段和其他結構。合成構造使Java編譯器能夠在不更改JVM的情況下實現(xiàn)新的Java語言特性。然而,合成結構可能在不同的Java編譯器實現(xiàn)中有所不同,這意味著.class文件也可能在不同的實現(xiàn)中有所不同。因此,如果您先序列化一個內部類,然后再用不同的JRE實現(xiàn)反序列化它,則可能會出現(xiàn)兼容性問題。

二、內部類(InnerClass)

內部類即一般聲明在某個類內部,有明確的名稱和修飾符,除了常見的內部類(這里指非方法、語句、代碼塊中的類,非靜態(tài)嵌套類)以外,還有兩種常見內部類,即局部類(LocalClass)和匿名類(AnonymousClass)。

三、局部類(LocalClass)

局部類一般聲明在方法、語句、代碼塊內部,可以有明確的名稱和修飾符,也可以沒有,此時稱之為匿名類。局部類使用過程中有幾個點需要注意:

1、局部類可以聲明在方法內部、for循環(huán)、if語句等任意代碼塊;
2、從JDK8開始,局部類可以直接訪問所在方法、statement、block的變量和參數(shù)(變量和參數(shù)必須是final或等效于final,即初始化后值不再發(fā)生改變);
3、局部類不能是static(因為局部類需要訪問封閉類中的變量),且局部類內部不能有static變量、方法、static初始化代碼塊,局部類內部不能聲明接口,因為接口天生就是static的。
4、方法(static或者非static)內部的局部類只能訪問封閉類的靜態(tài)變量。
5、靜態(tài)內部類中可以出現(xiàn)static常量,即 static final 修飾。
6、局部類中的變量聲明會屏蔽封閉域內的同名變量。
7、其他類若要實現(xiàn)一個類的某內部接口,可以通過直接實現(xiàn)外部類名.內部接口名的方式實現(xiàn)。

來看官網(wǎng)給的例子:

public class LocalClassExample {
    // 必須為static,否則靜態(tài)方法內的局部類無法訪問
    static String regularExpression = "[^0-9]";

    public static void validatePhoneNumber(String phoneNumber1, String phoneNumber2) {
                //本地變量須為final
        final int numberLength = 10;

        // Valid in JDK 8 and later:
                // 或等效final,值不再發(fā)生改變
        // int numberLength = 10;
        class PhoneNumber {
            String formattedPhoneNumber = null;
            PhoneNumber(String phoneNumber){
                // 必須是final或final等效,不能改變值,編譯報錯
                // numberLength = 7;
                String currentNumber = phoneNumber.replaceAll(regularExpression, "");
                if (currentNumber.length() == numberLength)
                    formattedPhoneNumber = currentNumber;
                else
                    formattedPhoneNumber = null;
            }

            public String getNumber() {
                return formattedPhoneNumber;
            }

//            Valid in JDK 8 and later:可以訪問封閉方法的參數(shù)
//            public void printOriginalNumbers() {
//                System.out.println("Original numbers are " + phoneNumber1 +
//                    " and " + phoneNumber2);
//            }
        }

        PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
        PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);

        // Valid in JDK 8 and later:
//        myNumber1.printOriginalNumbers();

        if (myNumber1.getNumber() == null)
            System.out.println("First number is invalid");
        else
            System.out.println("First number is " + myNumber1.getNumber());
        if (myNumber2.getNumber() == null)
            System.out.println("Second number is invalid");
        else
            System.out.println("Second number is " + myNumber2.getNumber());

    }

    public static void main(String... args) {
        validatePhoneNumber("123-456-7890", "456-7890");
    }

四、匿名類(AnonymousClass)

與局部類相比,匿名類可以讓代碼更簡潔,聲明一個類的同時實例化。與本地類不同的是,匿名類沒有名字,在僅使用一次本地類的場景可以考慮使用匿名類。

匿名類的聲明

如果說本地類是聲明類,那么,匿名類就是表達式,即在表達式內部定義類。來看官網(wǎng)的例子:

public class HelloWorldAnonymousClasses {
    // 局部接口
    interface HelloWorld {
        public void greet();
        public void greetSomeone(String someone);
    }
    
    
    public void sayHello() {
        // 本地類
        class EnglishGreeting implements HelloWorld {
            String name = "world";
            public void greet() {
                greetSomeone("world");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Hello " + name);
            }
        }
        
        HelloWorld englishGreeting = new EnglishGreeting();
        
        // 匿名類1
        HelloWorld frenchGreeting = new HelloWorld() {
            String name = "tout le monde";
            public void greet() {
                greetSomeone("tout le monde");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Salut " + name);
            }
        };
        
        // 匿名類2
        HelloWorld spanishGreeting = new HelloWorld() {
            String name = "mundo";
            public void greet() {
                greetSomeone("mundo");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Hola, " + name);
            }
        };
        englishGreeting.greet();
        frenchGreeting.greetSomeone("Fred");
        spanishGreeting.greet();
    }

    public static void main(String... args) {
        HelloWorldAnonymousClasses myApp =
            new HelloWorldAnonymousClasses();
        myApp.sayHello();
    }            
}
匿名類語法

匿名類表達式的語法類似調用構造方法,不同點在于,表達式中同時有類的定義。匿名類表達式由以下幾部分組成:new操作符、實現(xiàn)或的接口或者繼承的類、括號+構造方法參數(shù)body(允許聲明方法、字段、代碼塊,但是不允許使用語句)

封閉域內局部變量的訪問、匿名類聲明與訪問
1、匿名類可以訪問所在閉包類的成員變量;
2、匿名類不能訪問閉包范圍內的非final(或等效final)本地變量;
3、與嵌套類類似,匿名類內部聲明的變量名會覆蓋閉包類內部的同名變量;
4、匿名類內部不能執(zhí)行靜態(tài)初始化,不能聲明接口;
5、同樣的,匿名類內部可以有靜態(tài)常量;
6、匿名類內部可以聲明Filed、非父類method、實例初始化、本地類

五、lambda表達式

匿名類存在的問題是,當匿名類實現(xiàn)的接口非常簡單,比如只有一個方法時,匿名類的語法會顯得笨拙且混亂。這種情況下,我們往往希望將函數(shù)當作方法參數(shù)傳遞,就像點擊了按鈕就會執(zhí)行某種動作一樣。lambda表達式可以幫助我們實現(xiàn)這個目標,把函數(shù)作為參數(shù),或者把代碼當作數(shù)據(jù)。所以,當類只有一個方法時,匿名類也有點包裝過度又復雜,lambda表達式可以讓你更簡潔的表示一個單方法類的實例。

演進樣例

官網(wǎng)給出了使用lambda表達式的理想場景樣例,可以看到匿名類一步一步到lambda表達式的簡化過程,下面分別來做說明:

// 基礎model類
public class Person {
        public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }
    public void printPerson() {
        // ...
    }
}

1、創(chuàng)建方法,搜索滿足某種特征的成員

比如說,輸出滿足年齡超過35歲的人員信息,通常情況下,我們會創(chuàng)建如下方法:

public static void printPersonsOlderThan(List<Person> roster, int age) {
    for (Person p : roster) {
        if (p.getAge() >= age) {
            p.printPerson();
        }
    }
}

這種方法設計的相對比較脆弱,舉例來說,當需要引入update功能時,就沒法使用了。比如,修改了Person類的結構,修改了age的類型和算法, 你就必須重新設計api。而且這種方法設計本身就有一定局限性,如果我要輸出年齡小于35歲的人員要怎么搞呢。
2、創(chuàng)建更為通用的搜索方法

接著來看一個比printPersonsOlderThan更通用的方法:

public static void printPersonsWithinAgeRange(
    List<Person> roster, int low, int high) {
    for (Person p : roster) {
        if (low <= p.getAge() && p.getAge() < high) {
            p.printPerson();
        }
    }
}

同樣的,這個方法也有局限性。如果我想輸出某個性別或者符合某種性別與年齡組合的成員該怎么搞?如果我要改變Person類的數(shù)據(jù)結構,比如新增、刪除某些屬性,這個時候又該怎么搞?盡管通用性比上一個方法好,但是,嘗試為每種查詢創(chuàng)建單獨的方法這種設計仍然十分脆弱。那么,我們把查詢條件單獨封裝到一個類中是不是會更好一些呢?

3、局部類封裝查詢條件

我們把查詢條件單獨封裝到一個局部類中,把條件組裝和查詢本身分開,代碼如下:

public static void printPersons(
    List<Person> roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

// 定義條件接口
interface CheckPerson {
    boolean test(Person p);
}

//年齡查詢條件封裝
class CheckPersonEligibleForSelectiveService implements CheckPerson {
    public boolean test(Person p) {
        return p.gender == Person.Sex.MALE &&
            p.getAge() >= 18 &&
            p.getAge() <= 25;
    }
}

// 使用的時候調用printPersons方法即可
printPersons(roster, new CheckPersonEligibleForSelectiveService());

雖然設計上來看,相對沒有那么不堪一擊,但是,局部類這種設計,代碼一坨一坨的看起來比較糟心,嘗試用匿名類來代替。

4、在匿名類中指定搜索條件

直接將CheckPersonEligibleForSelectiveService替換為匿名類,只需要調整printPersons方法,代碼如下:

printPersons( roster,new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge() <= 25;
        }
    }
);

這種情況減少了很多代碼量,因為你無需為每次查詢都創(chuàng)建一個新類。但是,考慮到CheckPerson這個接口只有一個方法,匿名類的語法又顯得過于笨拙,我們可以嘗試用lambda表達式來進一步簡化。

5、lambda表達式指定搜索條件

首先,CheckPerson這個接口是一個函數(shù)式接口。函數(shù)式接口指一個接口僅包含一個抽象方法(當然,函數(shù)式接口內部也可能有一個或多個default 方法或者static方法)。因為函數(shù)式接口只有一個抽象方法,所以在實現(xiàn)的時候可以直接省略方法名稱,改造后代碼如下:

printPersons(roster,
    (Person p) -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

接下來,JDK已經(jīng)封裝了一批函數(shù)式接口,那么我們是不是可以利用JDK提供的標準函數(shù)式接口來代替CheckPerson,來減少更多的代碼呢。

6、使用lambda表達式的標準函數(shù)式接口

先來回憶一下CheckPerson接口的定義,再對比JDK的標準函數(shù)式接口,可以發(fā)現(xiàn),與Predicate接口非常相像。

interface CheckPerson {
    boolean test(Person p);
}

interface Predicate<Person> {
    boolean test(Person t);
}

我們嘗試用Predicate<Person>來代替CheckPerson接口,代碼如下:

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

// 同樣利用lambda表達式進一步簡化
printPersonsWithPredicate(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

當然這不是可以用lambda表達式的第一個點,你可以擴展到整個應用。

7、整個應用內使用lambda表達式

重新審視printPersonsWithPredicate方法,確認是否有其他地方可以用lambda表達式代替。方法中,輸出符合條件的人員信息,那么人員信息輸出的動作,就符合Consumer這個函數(shù)式接口的語義,我們可以把這個動作用Consumer代替,代碼如下:

public static void processPersons(
    List<Person> roster, Predicate<Person> tester,Consumer<Person> consumer) {
    for (Person p : roster) {
        if (tester.test(p)) {
            consumer.accept(p);
        }
    }
}

// 同樣的,對printPersonsWithPredicate的使用也要做調整
processPersons( roster,
     p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 18
         && p.getAge() <= 25,
     p -> p.printPerson()
);

如果此時不僅僅是輸出符合條件的人員信息,還需要對人員做額外處理,那么這個時候可以引入Function接口,如下:

public static void processPersons(
    List<Person> roster, Predicate<Person> tester,Function<Person,String> applyer,Consumer<String> consumer) {
    for (Person p : roster) {
        if (tester.test(p)) {
            String data = appler.apply(p);
            consumer.accept(data);
        }
    }
}

// 同樣的,對printPersons的使用也要做調整
processPersons( roster,
     p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 18
         && p.getAge() <= 25,
         p -> p.getName(),
             p -> p.printPerson()
);

8、泛型擴展

重新考慮processPersons方法,引入泛型擴展:

public static <S, R> void processElements(
    Iterable<S> source,
    Predicate<S> tester,
    Function <S, R> mapper,
    Consumer<R> block) {
    for (S p : source) {
        if (tester.test(p)) {
            R data = mapper.apply(p);
            block.accept(data);
        }
    }
}

// 同樣的,使用方法調整為processElements
processElements( roster,
             p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 18
         && p.getAge() <= 25,
         p -> p.getName(),
             p -> p.printPerson()
);

方法執(zhí)行以下操作:從容器中取源對象、利用predicate過濾源對象、利用Function 對源對象進行轉換、執(zhí)行cosumer操作。這四個操作完全可以用一個聚合操作來代替,進一步簡化代碼

9、使用接受Lambda表達式作為參數(shù)的聚合操作

這里就需要借助Stream操作了,對此我們非常熟悉。上面的代碼可以改為一行代碼:

// 直接省略了范型方法的定義和調用
roster
    .stream()
    .filter(
        p -> p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25)
    .map(p -> p.getEmailAddress())
    .forEach(email -> System.out.println(email));

以上9個步驟,見證了代碼從普通笨拙到簡單靈活的蛻變,不得不說lambda表達式真的是太好用了。

lambda語法
a) 圓括號內以逗號分隔的形式參數(shù)列表,參數(shù)可以有一個或多個,當只有一個參數(shù)時,括號可以省略。

b) 然后是箭頭 ->

c)單個表達式或語句塊組成的語句體,如果指定單個表達式,那么Java運行時將計算該表達式,然后返回其值;你可以用return語句代替,如下:
p -> {
    return p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25;
}

返回語句不是一個表達式;lambda表達式中,所有聲明必須放在"{}"中,但是void方法的調用可以不用 "{}",如下:

email -> System.out.println(email)
封閉作用域內局部變量的訪問

與局部類和匿名類類似,lambda表達式可以捕獲變量,對封閉作用域內局部變量的訪問權限也一樣。不同之處在于,lambda不存沒有變量覆蓋的問題,這是因為,lambda是詞法范圍內的應用,也就是說lambda不會即成任何父類型的名稱或者引入新的作用域級別。lambda表達式中對聲明的解釋與封閉作用域內的聲明一致。同樣的,lambda表達式只能訪問封閉作用域內final(或者等效final)類型的參數(shù)或者變量。

目標類型推定(Target Typing)

如何推定lambda表達式的類型呢?仍以上面的代碼為例

p -> p.getGender() == Person.Sex.MALE
    && p.getAge() >= 18
    && p.getAge() <= 25;

// 下面兩個方法都會用到該lambda表達式
public static void printPersons(List<Person> roster, CheckPerson tester);
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) 

上面的代碼中,當JVM調用printPersons時,期望的數(shù)據(jù)類型是CheckPerson,那么lambda表達式的類型就是CheckPerson;當調用printPersonsWithPredicate時,期望的數(shù)據(jù)類型是Predicate<Person> tester,那么lambda表達式的類型就是Predicate<Person> tester。我們把方法期望的類型稱之為目標類型,Java編譯器會根據(jù)lambda表達式所在的上下文或者環(huán)境的目標類型來確定lambda表達式的類型。而且,只有在Java編譯器可以確定目標類型的場景,才可以使用lambda表達式,這是基本規(guī)則。這些場景包括:

  • Variable declarations 變量聲明
  • Assignments 賦值
  • Return statements 返回語句
  • Array initializers 數(shù)組初始化
  • Method or constructor arguments 方法或者構造器參數(shù)
  • Lambda expression bodies lambda表達式主體
  • Conditional expressions, ?: 條件表達式
  • Cast expressions 轉型表達式
目標類型和方法參數(shù)

對于方法參數(shù)來說,Java編譯器主要根據(jù)兩個語言特性來完成類型的推定,分別是重載解析(overload resolution )類型參數(shù)推斷(type argument inference),以Callable和Runnable接口為例

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call();
}

// 有兩個重載方法
void invoke(Runnable r) {
    r.run();
}

<T> T invoke(Callable<T> c) {
    return c.call();
}

// 調用的將會是 invoke(Callable<T> c)
String s = invoke(() -> "done");

上面的例子中,() -> "done"就是Callable

序列化

當lambda表達式的參數(shù)或者目標類型實現(xiàn)了序列化接口,那么lambda也支持序列化,但是,與內部類一樣,強烈建議不要使用lambda的序列化。

六、方法引用(MethodReference)

通常情況下,使用lambda表達式來創(chuàng)建匿名方法,但是,大多數(shù)情況下lambda表達式除了調用現(xiàn)有方法以外什么也不做。這種情況下,通過名稱引用現(xiàn)有的方法通常更清晰,方法引用可以做到這一點。方法引用是用于已經(jīng)有名稱的方法的緊湊、易于閱讀的lambda表達式

JDK官網(wǎng)中介紹的Method reference(方法引用)有四種類型:

類型 語法 樣例
Reference to a static method(靜態(tài)方法引用 *ContainingClass*::*staticMethodName* Person::compareByAge MethodReferencesExamples::appendStrings
Reference to an instance method of a particular object(指定實例方法引用 *containingObject*::*instanceMethodName* myComparisonProvider::compareByName myApp::appendStrings2
Reference to an instance method of an arbitrary object of a particular type(特定類型的任意實例對象方法引用) *ContainingType*::*methodName* String::compareToIgnoreCase String::concat
Reference to a constructor(構造方法引用) *ClassName*::new HashSet::new

這是官網(wǎng)給的例子,有興趣的同學可以對照四種類型自己實踐一下,輔助類Person,

public class Person {
    LocalDate birthday;
    public int getAge() {}
    public String getName() {}
    public LocalDate getBirthday() { return birthday;} 
    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }
}
靜態(tài)方法引用

靜態(tài)方法引用比較簡單,常見如Collections::sort的用法,這里就不再做過多敘述。

指定對象的實例方法引用
class ComparisonProvider {
    public int compareByName(Person a, Person b) {
        return a.getName().compareTo(b.getName());
    }
        
    public int compareByAge(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
// 引用myComparisonProvider對象的實例方法
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);

rosterAsArray是一個Person數(shù)組,compareByName實例方法是參數(shù)是<Person,Person>,可以看到用法也比較簡單。

特定類型任意對象的實例方法的引用
String[] stringArray = { "Barbara", "James", "Mary", "John","Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

樣例也比較簡單,不做過多說明

構造方法引用
public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>> DEST transferElements(SOURCE    sourceCollection,Supplier<DEST> collectionFactory) {
    DEST result = collectionFactory.get();
    for (T t : sourceCollection) {
        result.add(t);
    }
    return result;
}

上面的樣例中,Supplier內部是一個get方法,沒有參數(shù),只有一個返回值。通常情況下,采用lambda表達式調用transferElements的方式如下:

// 初始狀態(tài)
Set<Person> rosterSetLambda = transferElements(roster, () -> { return new HashSet<>(); });
// 進一步簡化
Set<Person> rosterSet = transferElements(roster, HashSet::new);
// 編譯器發(fā)現(xiàn)你希望創(chuàng)建一個元素類型是Person的容器,指定元素類型
Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);

七、使用嵌套類(NestedClass)、內部類(InnerClass)、匿名類(AnonymousClass)還是lambda表達式?

應該怎么確定在哪些場景使用那種類呢,比較簡單的辦法是根據(jù)各種類型類的可用場景來選擇,官網(wǎng)同樣給出了建議:

LocalClass當需要創(chuàng)建一個類的多個實例、訪問其構造方法或者引入一個新的命名類型(比如,稍后你需要調用其他方法)時,可以使用。

AnonymousClass需要聲明單獨field或者方法時

Lambda表達式當需要傳遞單個行為單元給某代碼時;或者需要使用函數(shù)式接口的簡單實例而且以上均不滿足的情況下。

NestedClass類似于LocalClass的需求,但是希望該類型更廣泛地可用,且不需要訪問局部變量或方法參數(shù)時。注意:如果需要訪問封閉實例的非公共字段和方法,請使用非靜態(tài)嵌套類(或內部類)。如果不需要,請使用靜態(tài)嵌套類。

總結

回顧完以上知識點,我們再回到本文開頭提到的問題。四種方法引用都介紹完了,你可能想問,并沒有看到這四種方法引用跟開頭講的例子有什么關系啊,別急,慢慢來看。先把接口定義拎出來:

// 接口定義,函數(shù)式接口
public interface NotifyListener {
    void notify(List<String> urls);
}

我們從常用的匿名類寫法開始:

public static void main(String[] args) {
    final AtomicReference<List<String>> reference = new AtomicReference<>();
    NotifyListener listener = new NotifyListener() {
        @Override
        public void notify(List<String> urls) {
            reference.set(urls);
        }
    };
    listener.notify(urls);

    List<String> result = reference.get();
    result.forEach(System.out::println);
}

NotifyListener是函數(shù)式接口(僅定義了一個抽象方法),可以利用lambda表達式進行簡化:

public static void main(String[] args) {
    final AtomicReference<List<String>> reference = new AtomicReference<>();
    // urls是函數(shù)式接口中抽象方法的參數(shù)
    NotifyListener listener = urls -> reference.set(urls);
    listener.notify(urls);

    List<String> result = reference.get();
    result.forEach(System.out::println);
}

然后在利用方法引用(實例方法引用)進一步簡化:

public static void main(String[] args) {
    final AtomicReference<List<String>> reference = new AtomicReference<>();
    NotifyListener listener = reference::set;
    listener.notify(urls);

    List<String> result = reference.get();
    result.forEach(System.out::println);
}

最終得到源碼中的寫法。在閱讀源碼過程中,如果遇到類似難以理解的代碼,可以參考進行反向推斷。不正之處,多多指教。

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容