本文由 简悦SimpRead 转码,原文地址 kerflyn.wordpress.com
Java 8开始出现了。它带来了一个完整的新功能:使用lambda exp00的函数式编程。
Java 8开始崭露头角。它有一个完整的新功能:用lambda表达式进行函数式编程。在这里,我将讨论作为Lambda项目(JSR-335)一部分的一个特性:虚拟扩展方法,也称为公共辩护人方法。这个功能将帮助你为你在接口中声明的方法提供一个默认的实现。例如,这允许Java团队在现有的接口中增加方法的声明,如List或Map。在他们这边,开发者不需要在Java库(如Hibernate)中重新定义他们的不同实现。因此,Java 8在理论上将与现有库兼容。
虚拟扩展方法是一种在Java中提供多重继承的方式。从事Lambda项目的团队认为,用虚拟扩展方法提供的那种多重继承只限于行为继承。作为一个挑战,我展示了用虚拟扩展方法,你也可以模拟状态继承。为此,我在本文中描述了Java 8中mixin的一个实现。
什么是mixin?
mixin是一种可组合的抽象类。它们被用在多继承的环境中,为一个类添加服务。多重继承是用来将你的类与你想要的任意数量的混合器组合在一起。例如,如果你有一个表示房子的类,你可以从这个类中创建你的房子,并通过继承车库和花园等类来扩展它。下面是用Scala写的这个例子。
val myHouse = new House with Garage with Garden
继承于混合器并不是真正的专业化。它是一种收集功能并将其添加到一个类中的方法。因此,通过mixin,你可以在OOP中对代码进行结构性分解,并增强类的可读性。
例如,这里是Python中混合器在模块socketserver中的应用。在这里,混合函数有助于声明四个基于socket的服务器:一个UPD和一个使用多进程的TCP服务,一个UPD和一个使用多线程的TCP服务。
class ForkingUDPServer(ForkingMixIn, UDPServer): pass
class ForkingTCPServer(ForkingMixIn, TCPServer): pass
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
什么是 虚拟扩展方法 ?
Java 8将引入_虚拟扩展方法_的概念,也叫_公共防御方法_。我们把它们称为VEM。
VEM的目的是为Java接口中声明的方法提供一个默认的实现。这有助于在一些广泛使用的接口中增加一个新的方法声明,比如JDK的Collection API的接口。因此,所有像Hibernate这样重新实现Collection API的现有库都不需要重写他们的实现,因为已经提供了一个默认的实现。
下面是一个如何在接口中声明默认实现的例子。
public interface Collection<T> extends Iterable<T> {
<R> Collection<R> filter(Predicate<T> p)
default { return Collections.<T>filter(this, p); }
}
Java 8中对mixin的天真模拟
现在,我们将通过使用VEMs来实现一个mixin。但我要先警告你。请不要在工作中使用它! 真的,不要在工作中使用它!
下面的实现不是线程安全的,会发生内存泄漏,而且完全取决于你在类中定义hashCode和equals的方式。还有一个主要的缺点,我将在后面讨论。
首先,我们定义了一个带有默认实现的接口,似乎是基于有状态的方法。
public interface SwitchableMixin {
boolean isActivated() default { return Switchables.isActivated(this); }
void setActivated(boolean activated) default { Switchables.setActivated(this, activated); }
}
其次,我们定义了一个实用类,它拥有一个地图,用来存储我们之前接口的实例和它们的状态之间的关联。状态由实用类中的一个私有嵌套类表示。
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 {}
DeviceA a = new DeviceA();
DeviceB b = new DeviceB();
a.setActivated(true);
assertThat(a.isActivated()).isTrue();
assertThat(b.isActivated()).isFalse();
"现在是完全不同的东西"
这个实现似乎工作得很好。但是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有一个默认的实现,而不是Runnable的run方法。所以我们可以从一个lambda表达式中生成一个X的实例,它将为方法run提供一个实现,就像方法makeX一样。所以,你期望这个程序显示的是这样的东西。
x1
x2
如果你删除对 "getName "的调用,你期望显示的东西是:
MyTest$1@30ae8764
MyTest$1@123acf34
其中两行表示makeX产生了两个独立的实例。这就是当前OpenJDK 8产生的结果(这里我使用的是OpenJDK 8 24.0-b07)。
然而,当前的OpenJDK 8并没有反映Java 8的最终行为。要做到这一点,你需要在运行javac时加入特殊选项-XDlambdaToMethod。以下是使用特殊选项后得到的输出。
x2
x2
如果你去掉对 "getName "的调用,你会得到这样的结果。
MyTest$$Lambda$1@5506d4ea
MyTest$$Lambda$1@5506d4ea
对makeX的每次调用似乎都给出了一个从相同的匿名内类中实例化的单子。然而,如果你查看存放Java二进制文件的目录(之前已经仔细地删除了所有的*.class文件),你不会发现一个名为MyTestClass$$Lambda$1.class的文件。
lambda表达式的完整翻译其实并不是在编译时完成的。事实上,这种翻译是在编译时和运行时完成的。编译器javac用一条最近加入JVM的指令来代替lambda表达式:指令invokedynamic(JSR292)。这条指令带有所有必要的元信息,以便在运行时翻译lambda表达式。这包括要调用的方法的名称,它的输入和输出类型,以及一个叫做 bootstrap 的方法。bootstrap方法旨在定义接收方法调用的实例,一旦JVM执行了invokedynamic指令。在有lambda表达式的情况下,JVM使用一个特殊的引导方法,叫做 lambda metafactory method 。
回到这个问题,lambda表达式的主体被转换为一个私有静态方法。因此,() -> { System.out.println("X"); }在MyTest中被转换为
private static void lambda$0() {
System.out.println("X")。
}
如果你使用反编译器javap,加上选项-private,就可以看到这一点(你甚至可以使用-c选项来看到更完整的翻译)。
当你运行程序时,一旦JVM第一次尝试解释invokedynamic指令,JVM就会调用上面描述的 lambda metafactory method 。在我们的例子中,在第一次调用makeX时,lambda元工厂方法生成了一个X的实例,并将run方法(来自接口Runnable)动态地链接到lambda$0方法。然后,X的实例被存储在内存中。在第二次调用makeX时,该实例被带回来。在那里你会得到与第一次调用时相同的实例。
修复? 解决方法?
对于这个问题,没有直接的修复或解决方法。即使计划在Oracle的Java 8中默认激活-XDlambdaToMethod,这也不应该出现在JVM规范中。这种行为可能会随着时间的推移和供应商的不同而改变。对于lambda表达式,你只能期望得到实现你的接口的东西。
另一种方法
因此,我们对混杂元素的模拟与Java 8不兼容。但仍有一种可能,即通过使用多重继承和委托,向现有的类添加服务。这种方法被称为虚拟字段模式。
所以让我们从我们的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;
}
}
DeviceA a = new DeviceA();
DeviceB b = new DeviceB();
a.setActive(true);
assertThat(a.isActive()).isTrue();
assertThat(b.isActive()).isFalse();
结论
在这篇文章中,我们看到了在Java 8中借助虚拟扩展方法为类添加服务的两种方法。第一种方法使用Map来存储实例状态。这种方法是危险的,原因不止一个:它可能不是线程安全的,存在内存泄漏的风险,它取决于你的供应商实现语言的方式,等等。另一种方法,基于委托并被称为虚拟字段模式,使用一个抽象的getter,让最终的实现描述它使用的服务的实现。这最后一种方法与语言的实现方式更加独立,也更加安全。
虚拟扩展方法是Java中的新东西。它带来了另一种表达方式,以创造新的模式和最佳实践。本文提供了这样一个例子,由于使用了虚拟扩展方法,在Java 8中启用了这种模式。我相信你可以从中提取其他的新模式。但是,正如我在这里所经历的,你应该毫不犹豫地分享它们,以检查其有效性。
编辑:在lambda项目邮件列表中分享后,需要在本帖中报告一些修改,以反映Java 8的新行为。感谢Oracle的Brian Goetz和Akiban的Yuval Shavit提供的建议。
- [2012-07-11] 更改了标题 + 将 "如何在Java 8中模拟mixin?"更名为 "Java 8中天真的模拟mixin" + 添加了一个谜题
- [2012-07-12] 增加了一个章节 + 删除了结论
- [2012-07-22] 添加了谜题的解答
- [2012-07-24] 增加了修复/解决方法部分+一些修正
- [2012-08-13] 完成了所有章节。