深入理解Java中的Lambda表达式
Java8开始出现,带来一个全新特性:使用Lambda表达式(JSR-335)进行函数式编程。今天我们要讨论的是Lambda的其中一部分:虚拟扩展方法,也叫做公共辩护(defender)方法。该特性可以让你在接口定义中提供方法的默认实现。例如你可以为已有的接口(如List和Map)声明一个方法定义,这样其他开发者就无需重新实现这些方法,有点像抽象类,但实际却是接口。当然,Java8理论上还是兼容已有的库。
虚拟扩展方法为Java带来了多重继承的特性,尽管该团队声称与多重继承不同,虚拟扩展方法被限制用于行为继承。或许通过这个特性你可以看到了多重继承的影子。但你还是可以模拟实例状态的继承。我将在接下来的文章详细描述Java8中通过mixin混入实现状态的继承。
什么是混入mixin?
混入是一种组合的抽象类,主要用于多继承上下文中为一个类添加多个服务,多重继承将多个mixin组合成你的类。例如,如果你有一个类表示“马”,你可以实例化这个类来创建一个“马”的实例,然后通过继承像“车库”和“花园”来扩展它,使用Scala的写法就是:
valmyHouse=newHousewithGaragewithGarden
从mixin继承并不是一个特定的规范,这只是用来将各种功能添加到已有类的方法。在OOP中,有了mixin,你就有通过它来提升类的可读性。
例如在Python的 socketserver模块中就有使用mixin的方法,在这里,mixin帮助4个基于不同Socket的服务,包括支持多进程的UDP和TCP服务以及支持多线程的UDP和TCP服务。
classForkingUDPServer(ForkingMixIn,UDPServer):pass classForkingTCPServer(ForkingMixIn,TCPServer):pass classThreadingUDPServer(ThreadingMixIn,UDPServer):pass classThreadingTCPServer(ThreadingMixIn,TCPServer):pass
什么是虚拟扩展方法?
Java8将引入虚拟扩展方法的概念,也叫publicdefendermethod.让我们姑且把这个概念简化为VEM。
VEM旨在为Java接口提供默认的方法定义,你可以用它在已有的接口中添加新的方法定义,例如Java里的集合API。这样类似Hibernate这样的第三方库无需重复实现这些集合API的所有方法,因为已经提供了一些默认方法。
下面是如何在接口中定义方法的示例:
publicinterfaceCollection<T>extendsIterable<T>{ <R>Collection<R>filter(Predicate<T>p) default{returnCollections.<T>filter(this,p);} }
Java8对混入的模拟
现在我们来通过VEM实现一个混入效果,不过事先警告的是:请不要在工作中使用!
下面的实现不是线程安全的,而且还可能存在内存泄露问题,这取决于你在类中定义的hashCode和equals方法,这也是另外一个缺点,我将在后面讨论这个问题。
首先我们定义一个接口(模拟状态Bean)并提供方法的默认定义:
publicinterfaceSwitchableMixin{ booleanisActivated()default{returnSwitchables.isActivated(this);} voidsetActivated(booleanactivated)default{Switchables.setActivated(this,activated);} }
然后我们定义一个工具类,包含一个Map实例来保存实例和状态的关联,状态通过工具类中的私有的嵌套类代表:
publicfinalclassSwitchables{ privatestaticfinalMap<SwitchableMixin,SwitchableDeviceState>SWITCH_STATES=newHashMap<>(); publicstaticbooleanisActivated(SwitchableMixindevice){ SwitchableDeviceStatestate=SWITCH_STATES.get(device); returnstate!=null&&state.activated; } publicstaticvoidsetActivated(SwitchableMixindevice,booleanactivated){ SwitchableDeviceStatestate=SWITCH_STATES.get(device); if(state==null){ state=newSwitchableDeviceState(); SWITCH_STATES.put(device,state); } state.activated=activated; } privatestaticclassSwitchableDeviceState{ privatebooleanactivated; } }
这里是一个使用用例,突出了状态的继承:
privatestaticclassDevice{} privatestaticclassDeviceAextendsDeviceimplementsSwitchableMixin{} privatestaticclassDeviceBextendsDeviceimplementsSwitchableMixin{}
“完全不同的东西”
上面的实现跑起来似乎挺正常的,但Oracle的Java语言架构师BrianGoetz向我提出一个疑问说当前实现是无法工作的(假设线程安全和内存泄露问题已解决)
interfaceFakeBrokenMixin{ staticMap<FakeBrokenMixin,String>backingMap =Collections.synchronizedMap(newWeakHashMap<FakeBrokenMixin,String>()); StringgetName()default{returnbackingMap.get(this);} voidsetName(Stringname)default{backingMap.put(this,name);} } interfaceXextendsRunnable,FakeBrokenMixin{} XmakeX(){return()->{System.out.println("X");};} Xx1=makeX(); Xx2=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那样。因此,你希望这个程序执行后显示的结果是:
x1 x2
如果你删掉getName方法的调用,那么执行结果变成:
MyTest$1@30ae8764 MyTest$1@123acf34
这两行显示出makeX方法的执行来自两个不同的实例,而这时当前OpenJDK8生成的(这里我使用的是OpenJDK824.0-b07).
不管怎样,当前的OpenJDK8并不能反映最终的Java8的行为,为了解决这个问题,你需要使用特殊参数-XDlambdaToMethod来运行javac命令,在使用了这个参数后,运行结果变成:
x2 x2
如果不调用getName方法,则显示:
MyTest$$Lambda$1@5506d4ea MyTest$$Lambda$1@5506d4ea
每个调用makeX方法似乎都是来自相同匿名内部类的一个单例实例,如果观察包含编译后的javaclass文件的目录,会发现并没有一个名为MyTestClass$$Lambda$1.class的文件。
因为在编译时,lambda表达式并没有经过完整的翻译,事实上这个翻译过程是在编译和运行时完成的,javac编译器将lambda表达式变成JVM新增的指令invokedynamic(JSR292)。这个指令包含所有必须的关于在运行时执行lambda表达式的元信息。包括要调用的方法名、输入输出类型以及一个名为bootstrap的方法。bootstrap方法用于定义接收此方法调用的实例,一旦JVM执行了invokedynamic指令,JVM就会在特定的bootstrap上调用lambda元工厂方法(lambdametafactorymethod)。
再回到刚才那个疑问中,lambda表达式转成了一个私有的静态方法,()->{System.out.println("X");}被转到了MyTest:
privatestaticvoidlambda$0(){ System.out.println("X"); }
如果你用javap反编译器并使用-private参数就可以看到这个方法,你也可以使用-c参数来查看更加完整的转换。
当你运行程序时,JVM会调用lambdametafactorymethod来尝试阐释invokedynamic指令。在我们的例子中,首次调用makeX时,lambdametafactorymethod生成一个X的实例并动态链接run方法到lambda$0方法.X的实例接下来被存储在内存中,当第二次调用makeX时就直接从内存中读取这个实例,因此你第二次调用的实例跟第一次是一样的。
修复了吗?有解决办法吗?
目前尚无这个问题直接的修复或者是解决办法。尽管Oracle的Java8计划默认激活-XDlambdaToMethod参数,因为这个参数并不是JVM规范的一部分,因此不同供应商和JVM的实现是不同的。对一个lambda表达式而言,你唯一能期望的就是在类中实现你的接口方法。
其他的方法
到此为止,尽管我们对mixin的模仿并不能兼容Java8,但还是可能通过多继承和委派为已有的类添加多个服务。这个方法就是virtualfieldpattern(虚拟字段模式).
所以来看看我们的Switchable.
interfaceSwitchable{booleanisActive(); voidsetActive(booleanactive); }
我们需要一个基于Switchable的接口,并提供一个附加的抽象方法返回Switchable的实现。集成的方法包含默认的定义,它们使用getter来转换到Switchable实现的调用:
publicinterfaceSwitchableViewextendsSwitchable{ SwitchablegetSwitchable(); booleanisActive()default{returngetSwitchable().isActive();} voidsetActive(booleanactive)default{getSwitchable().setActive(active);} }
接下来,我们创建一个完整的Switchable实现:
publicclassSwitchableImplimplementsSwitchable{ privatebooleanactive; @Override publicbooleanisActive(){ returnactive; } @Override publicvoidsetActive(booleanactive){ this.active=active; } }
这里是我们使用虚拟字段模式的例子:
publicclassDevice{} publicclassDeviceAextendsDeviceimplementsSwitchableView{ privateSwitchableswitchable=newSwitchableImpl(); @Override publicSwitchablegetSwitchable(){ returnswitchable; } } publicclassDeviceBextendsDeviceimplementsSwitchableView{ privateSwitchableswitchable=newSwitchableImpl(); @Override publicSwitchablegetSwitchable(){ returnswitchable; } }
结论
在这篇文章中,我们使用了两种方法通过Java8的虚拟扩展方法为类增加多个服务。第一个方法使用一个Map来存储实例状态,这个方法很危险,因为不是线程安全而且存在内存泄露问题,这完全依赖于不同的JVM对Java语言的实现。另外一个方法是使用虚拟字段模式,通过一个抽象的getter来返回最终的实现实例。第二种方法更加独立而且更加安全。
虚拟扩展方法是Java的新特性,本文主要介绍的是多重继承的实现,详细你会有更深入的研究以及应用于其他方面,别忘了跟大家分享。