當前位置:才華齋>計算機>java語言>

詳解Java中的Lambda表示式

java語言 閱讀(1.55W)

Java 8 開始出現,帶來一個全新特性:使用 Lambda 表示式 (JSR-335) 進行函數語言程式設計。今天我們要討論的是 Lambda 的其中一部分:虛擬擴充套件方法,也叫做公共辯護(defender)方法。該特性可以讓你在介面定義中提供方法的預設實現。例如你可以為已有的介面(如 List 和 Map)宣告一個方法定義,這樣其他開發者就無需重新實現這些方法,有點像抽象類,但實際卻是介面。當然,Java 8 理論上還是相容已有的庫。

詳解Java中的Lambda表示式

虛擬擴充套件方法為 Java 帶來了多重繼承的特性,儘管該團隊聲稱與多重繼承不同,虛擬擴充套件方法被限制用於行為繼承。或許通過這個特性你可以看到了多重繼承的影子。但你還是可以模擬例項狀態的繼承。我將在接下來的文章詳細描述 Java 8 中通過 mixin 混入實現狀態的繼承。

什麼是混入 mixin?

混入是一種組合的抽象類,主要用於多繼承上下文中為一個類新增多個服務,多重繼承將多個 mixin 組合成你的類。例如,如果你有一個類表示“馬”,你可以例項化這個類來建立一個“馬”的例項,然後通過繼承像“車庫”和“花園”來擴充套件它,使用 Scala 的寫法就是:

val myHouse = new House with Garage with Garden

從 mixin 繼承並不是一個特定的規範,這只是用來將各種功能新增到已有類的方法。在 OOP 中,有了 mixin,你就有通過它來提升類的可讀性。

例如在 Python 的 socketserver 模組中就有使用 mixin 的方法,在這裡,mixin 幫助 4 個基於不同 Socket 的 服務,包括支援多程序的 UDP 和 TCP 服務以及支援多執行緒的 UDP 和 TCP 服務。

class ForkingUDPServer(ForkingMixIn, UDPServer): passclass ForkingTCPServer(ForkingMixIn, TCPServer): pass class ThreadingUDPServer(ThreadingMixIn, UDPServer): passclass ThreadingTCPServer(ThreadingMixIn, TCPServer): pass

什麼是虛擬擴充套件方法?

Java 8 將引入虛擬擴充套件方法的概念,也叫 public defender method. 讓我們姑且把這個概念簡化為 VEM。

VEM 旨在為 Java 介面提供預設的方法定義,你可以用它在已有的介面中新增新的方法定義,例如 Java 裡的集合 API。這樣類似 Hibernate 這樣的第三方庫無需重複實現這些集合 API 的所有方法,因為已經提供了一些預設方法。

下面是如何在介面中定義方法的示例:

public interface Collectionextends Iterable{Collectionfilter(Predicatep) default { return Collections.filter(this, p); } }

Java 8 對混入的模擬

現在我們來通過 VEM 實現一個混入效果,不過事先警告的是:請不要在工作中使用!

下面的實現不是執行緒安全的,而且還可能存在記憶體洩露問題,這取決於你在類中定義的 hashCode 和 equals 方法,這也是另外一個缺點,我將在後面討論這個問題。

首先我們定義一個介面(模擬狀態Bean)並提供方法的默認定義:

public interface SwitchableMixin { boolean isActivated() default { return tivated(this); } void setActivated(boolean activated) default { ctivated(this, activated); }}

然後我們定義一個工具類,包含一個 Map 例項來儲存例項和狀態的關聯,狀態通過工具類中的私有的巢狀類代表:

public final class Switchables { private static final MapSWITCH_STATES = new HashMap<>(); public static boolean isActivated(SwitchableMixin device) { SwitchableDeviceState state = SWITCH_(device); return state != null && vated; } public static void setActivated(SwitchableMixin device, boolean activated) { SwitchableDeviceState state = SWITCH_(device); if (state == null) { state = new SwitchableDeviceState(); SWITCH_(device, state); } vated = activated; } private static class SwitchableDeviceState { private boolean activated; } }

這裡是一個使用用例,突出了狀態的繼承:

private static class Device {} private static class DeviceA extends Device implements SwitchableMixin {} private static class DeviceB extends Device implements SwitchableMixin {}

“完全不同的東西”

上面的實現跑起來似乎挺正常的,但 Oracle 的 Java 語言架構師 Brian Goetz 向我提出一個疑問說當前實現是無法工作的(假設執行緒安全和記憶體洩露問題已解決)

interface FakeBrokenMixin { static MapbackingMap = hronizedMap(new WeakHashMap()); String getName() default { return (this); } void setName(String name) default { (this, name); }} interface X extends Runnable, FakeBrokenMixin {} X makeX() { return () -> { tln("X"); }; } X x1 = makeX(); X x2 = makeX(); ame("x1"); ame("x2"); tln(ame()); tln(ame());


你猜這段程式碼執行後會顯示什麼結果呢?

疑問的解決

第一眼看去,這個實現的程式碼沒有問題。X 是一個只包含一個方法的介面,因為 getName 和 setName 已經有了預設的定義,但 Runable 介面的 run 方法沒有定義,因此我們可通過 lambda 表示式來生成 X 的例項,然後提供 run 方法的實現,就像 makeX 那樣。因此,你希望這個程式執行後顯示的結果是:

x1x2

如果你刪掉 getName 方法的呼叫,那麼執行結果變成:

MyTest$1@30ae8764MyTest$1@123acf34

這兩行顯示出 makeX 方法的執行來自兩個不同的例項,而這時當前 OpenJDK 8 生成的(這裡我使用的是 OpenJDK 8 24.0-b07).

不管怎樣,當前的 OpenJDK 8 並不能反映最終的 Java 8 的行為,為了解決這個問題,你需要使用特殊引數 -XDlambdaToMethod 來執行 javac 命令,在使用了這個引數後,執行結果變成:

x2x2

如果不呼叫 getName 方法,則顯示:

MyTest$$Lambda$1@5506d4eaMyTest$$Lambda$1@5506d4ea

每個呼叫 makeX 方法似乎都是來自相同匿名內部類的一個單例例項,如果觀察包含編譯後的 java class 檔案的目錄,會發現並沒有一個名為 MyTestClass$$Lambda$s 的檔案。

因為在編譯時,lambda 表示式並沒有經過完整的翻譯,事實上這個翻譯過程是在編譯和執行時完成的,javac 編譯器將 lambda 表示式變成 JVM 新增的指令 invokedynamic (JSR292)。這個指令包含所有必須的關於在執行時執行 lambda 表示式的元資訊。包括要呼叫的方法名、輸入輸出型別以及一個名為 bootstrap 的方法。bootstrap 方法用於定義接收此方法呼叫的例項,一旦 JVM 執行了 invokedynamic 指令,JVM 就會在特定的 bootstrap 上呼叫 lambda 元工廠方法 (lambda metafactory method)。

再回到剛才那個疑問中,lambda 表示式轉成了一個私有的靜態方法,() -> { tln("X"); } 被轉到了 MyTest:

private static void lambda$0() { tln("X");}

如果你用 javap 反編譯器並使用 -private 引數就可以看到這個方法,你也可以使用 -c 引數來檢視更加完整的轉換。

當你執行程式時,JVM 會呼叫 lambda metafactory method 來嘗試闡釋 invokedynamic 指令。在我們的例子中,首次呼叫 makeX 時,lambda metafactory method 生成一個 X 的例項並動態連結 run 方法到 lambda$0 方法. X 的'例項接下來被儲存在記憶體中,當第二次呼叫 makeX 時就直接從記憶體中讀取這個例項,因此你第二次呼叫的例項跟第一次是一樣的。

修復了嗎?有解決辦法嗎?

目前尚無這個問題直接的修復或者是解決辦法。儘管 Oracle 的 Java 8 計劃預設啟用-XDlambdaToMethod 引數,因為這個引數並不是 JVM 規範的一部分,因此不同供應商和 JVM 的實現是不同的。對一個 lambda 表示式而言,你唯一能期望的就是在類中實現你的介面方法。

其他的方法

到此為止,儘管我們對 mixin 的模仿並不能相容 Java 8,但還是可能通過多繼承和委派為已有的類新增多個服務。這個方法就是 virtual field pattern (虛擬欄位模式).

所以來看看我們的 Switchable.

interface Switchable { boolean isActive(); void setActive(boolean active);}

我們需要一個基於 Switchable 的介面,並提供一個附加的抽象方法返回 Switchable 的實現。整合的方法包含預設的定義,它們使用 getter 來轉換到 Switchable 實現的呼叫:

public interface SwitchableView extends Switchable { Switchable getSwitchable(); boolean isActive() default { return getSwitchable()tive(); } void setActive(boolean active) default { getSwitchable()ctive(active); }}

接下來,我們建立一個完整的 Switchable 實現:

public class SwitchableImpl implements Switchable { private boolean active; @Override public boolean isActive() { return active; } @Override public void setActive(boolean active) { ve = active; }}

這裡是我們使用虛擬欄位模式的例子:

public class Device {} public class DeviceA extends Device implements SwitchableView { private Switchable switchable = new SwitchableImpl(); @Override public Switchable getSwitchable() { return switchable; }} public class DeviceB extends Device implements SwitchableView { private Switchable switchable = new SwitchableImpl(); @Override public Switchable getSwitchable() { return switchable; }}

結論

在這篇文章中,我們使用了兩種方法通過 Java 8 的虛擬擴充套件方法為類增加多個服務。第一個方法使用一個 Map 來儲存例項狀態,這個方法很危險,因為不是執行緒安全而且存在記憶體洩露問題,這完全依賴於不同的 JVM 對 Java 語言的實現。另外一個方法是使用虛擬欄位模式,通過一個抽象的 getter 來返回最終的實現例項。第二種方法更加獨立而且更加安全。

虛擬擴充套件方法是 Java 的新特性,本文主要介紹的是多重繼承的實現,詳細你會有更深入的研究以及應用於其他方面,別忘了跟大家分享。