this 與 final


來看一下接下來的程式,先想想看結果會如何顯示?

package cc.openhome;

import static java.lang.System.out;
 
class Hello {
    Runnable r1 = new Runnable() {
        public void run() {
            out.println(this);
        }
    };
    
    Runnable r2 = new Runnable() {
        public void run() {
            out.println(toString());
        }
    };
 
    public String toString() { 
        return "Hello, world!"; 
    }
}

public class Main {
    public static void main(String[] args) {
        new Hello().r1.run();
        new Hello().r2.run();
    }
}

結果會顯示像是cc.openhome.Hello\$1@6d06d69c與cc.openhome.Hello\$2@7852e922之類的訊息,這是因為this以及toString()(也就是this.toString())實際對象,實際上會來自匿名類別對應的實例,也就是Runnable實例,你沒有定義Runnable的toString(),因而顯示結果是Object預設的toString()傳回字串。再來看看接下來的程式,它會顯示什麼?

package cc.openhome;

import static java.lang.System.out;
 
class Hello {
    Runnable r1 = () -> out.println(this);
    Runnable r2 = () -> out.println(toString());
 
    public String toString() { 
        return "Hello, world!"; 
    }
}

public class Main {
    public static void main(String[] args) {
        new Hello().r1.run();
        new Hello().r2.run();
    }
}

如果Lambda表示式只是匿名類別的語法蜜糖,那麼結果也該是顯示cc.openhome.Hello\$1@6d06d69c與cc.openhome.Hello\$2@7852e922之類的訊息,事實上,執行結果會是顯示兩次的"Hello, world!",也就是說,Lambda 表示式本體中的thistoString()(也就是this.toString())實際對象,是來自Lambda的週圍環境(Context),也就是建構出來的Hello實例,因為是Hello類別包圍了Lambda運算式,範例中定義了Hello類別的toString()傳回"Hello, world!"字串,因而執行時才會顯示兩次的"Hello, world!"。

匿名內部類別 中談過,JDK7之前,如果要在匿名內部類別中存取區域變數,則該區域變數必須是final,否則會發生編譯錯誤,而在在JDK8中,如果變數本身等效於final區域變數,也就是說,如果變數不會在匿名類別中有重新指定的動作,就可以不用加上final關鍵字。例如以下在JDK8中不會有錯:

String[] names = {"Justin", "Monica", "Irene"}; // JDK8 前必須加上 final
Runnable runnable = new Runnable() {
    public void run() {
        for(String name : names) {
             System.out.println(name);
        }
    }
};

因為Runnable符合函式介面定義,因此可以改為Lambda運算式:

String[] names = {"Justin", "Monica", "Irene"};
Runnable runnable = () -> {
    for(String name : names) {
         System.out.println(name);
    }
};

如果Lambda表示式中捕獲的區域變數本身等效於final區域變數,可以不用在區域變數上加上final。不過,我們可以在Lambda表示式中改變被捕獲的區域變數值嗎?

不行!JDK8特意禁止你捕獲可變動的區域變數(在JavaScript或Scala等語言中可以做到),因為JDK8會想要採用Lambda的理由之一,是想進一步支援並行程式設計,Lambda表示式中可變動的區域變數,通常也代表著運算式本體中會改變被捕獲變數參考的物件狀態,改變物件狀態在並行程式中代表著可能必須處理同步鎖定問題,JDK8以禁止捕獲可變動的區域變數來避免了這類的問題。