Java 8 開始出現,帶來一個全新特性:使用Lambda 表達式(JSR-335) 進行函數式編程。今天我們要討論的是Lambda 的其中一部分:虛擬擴展方法,也叫做公共辯護(defender)方法。該特性可以讓你在接口定義中提供方法的默認實現。例如你可以為已有的接口(如List 和Map)聲明一個方法定義,這樣其他開發者就無需重新實現這些方法,有點像抽像類,但實際卻是接口。當然,Java 8 理論上還是兼容已有的庫。
虛擬擴展方法為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 Collection<T> extends Iterable<T> { <R> Collection<R> filter(Predicate<T> p) default { return Collections.<T>filter(this, p); } }
Java 8 對混入的模擬
現在我們來通過VEM 實現一個混入效果,不過事先警告的是:請不要在工作中使用!
下面的實現不是線程安全的,而且還可能存在內存洩露問題,這取決於你在類中定義的hashCode 和equals 方法,這也是另外一個缺點,我將在後面討論這個問題。
首先我們定義一個接口(模擬狀態Bean)並提供方法的默認定義:
public interface SwitchableMixin { boolean isActivated() default { return Switchables.isActivated(this); } void setActivated(boolean activated) default { Switchables.setActivated(this, activated); }}
然後我們定義一個工具類,包含一個Map 實例來保存實例和狀態的關聯,狀態通過工具類中的私有的嵌套類代表:
public final class Switchables { private static final Map<SwitchableMixin, SwitchableDeviceState> SWITCH_STATES = new HashMap<>(); public static boolean isActivated(SwitchableMixin device) { SwitchableDeviceState state = SWITCH_STATES.get(device); return state != null && state. activated; } public static void setActivated(SwitchableMixin device, boolean activated) { SwitchableDeviceState state = SWITCH_STATES.get(device); if (state == null) { state = new SwitchableDeviceState(); SWITCH_STATES.put(device, state); } state.activated = 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 Map<FakeBrokenMixin, String> backingMap = Collections.synchronizedMap(new WeakHashMap<FakeBrokenMixin, String>()); String getName() default { return backingMap.get(this); } void setName(String name) default { backingMap.put(this, name); }} interface X extends Runnable, FakeBrokenMixin {} X makeX() { return () -> { System.out.println("X"); }; } X x1 = makeX() ; X x2 = makeX(); x1.setName("x1"); x2.setName("x2"); System.out.println(x1.getName()); System.out.println(x2.getName() );
你猜這段代碼執行後會顯示什麼結果呢?
疑問的解決
第一眼看去,這個實現的代碼沒有問題。 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$1.class 的文件。
因為在編譯時,lambda 表達式並沒有經過完整的翻譯,事實上這個翻譯過程是在編譯和運行時完成的,javac 編譯器將lambda 表達式變成JVM 新增的指令invokedynamic (JSR292)。這個指令包含所有必須的關於在運行時執行lambda 表達式的元信息。包括要調用的方法名、輸入輸出類型以及一個名為bootstrap 的方法。 bootstrap 方法用於定義接收此方法調用的實例,一旦JVM 執行了invokedynamic 指令,JVM 就會在特定的bootstrap 上調用lambda 元工廠方法(lambda metafactory method)。
再回到剛才那個疑問中,lambda 表達式轉成了一個私有的靜態方法,() -> { System.out.println("X"); } 被轉到了MyTest:
private static void lambda$0() { System.out.println("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().isActive(); } void setActive(boolean active) default { getSwitchable().setActive(active); }}
接下來,我們創建一個完整的Switchable 實現:
public class SwitchableImpl implements Switchable { private boolean active; @Override public boolean isActive() { return active; } @Override public void setActive(boolean active) { this.active = 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 的新特性,本文主要介紹的是多重繼承的實現,詳細你會有更深入的研究以及應用於其他方面,別忘了跟大家分享。