深入学习java中的Groovy 和 Scala 类
前言
Java传承的是平台,而不是语言。有超过200种语言可以在JVM上运行,它们之中不可避免地会有一种语言最终将取代Java语言,成为编写JVM程序的最佳方式。本系列将探讨三种下一代JVM语言:Groovy、Scala和Clojure,比较并对比新的功能和范例,让Java开发人员对自己近期的未来发展有大体的认识。
Java语言的开发人员精通C++和其他语言,包括多继承(multipleinheritance),使得类可以继承自任意数量的父类。多继承带来的一个问题是,不可能确定所继承的功能来自哪个父类。这个问题被称为钻石问题(请参阅参考资料)。钻石问题和多继承中固有的其他复杂性启发了Java语言设计者选择“单继承加接口”的方法。
接口定义了语义,但没有定义行为。它们非常适合用来定义方法签名和数据抽象,所有Java下一代语言都支持Java接口,并且无需进行重大的修改。不过,有些交叉问题不适合使用“单继承加接口”模型。这种错位导致必须提供适合Java语言的外部机制,比如面向方面的编程。两种Java下一代语言(Groovy和Scala)通过使用一种被称为混入或特征的语言结构在另一个层次的扩展上处理这类问题。本文介绍了Groovy中的混入和Scala中的特征,并演示了如何使用它们。(Clojure通过协议处理大致相同的功能,我在Java下一代:没有继承性的扩展,第2部分中已经介绍过这一点。)
混入
混入的概念起源于Flavors语言(请参阅参考资料)。这个概念的灵感来自于开发该语言的办公室附近的一家冰淇淋店。这家冰淇淋店提供了纯口味的冰淇淋,以及客户想要的其他任何的“混合物”(糖果碎、糖屑、坚果,等等)。
早期的一些面向对象语言在单个代码块中共同定义某个类的属性和方法,所有类定义是完整的。在其他语言中,开发人员可以在一个地方定义属性,但推迟方法的定义,并在适当的时候将它们“混合”到类中。随着面向对象语言的演变,混入与现代语言的配合方式的细节也在演变。
在Ruby、Groovy和类似的语言中,作为一个接口和父类之间的交叉,混入可以扩充现有的类层次结构。像接口一样,混入可以充当instanceof检查的类型,同时也要遵循相同的扩展规则。您可以将无限数量的混入应用于某一个类。与接口不同的是,混入不仅指定了方法签名,也可以实现签名的行为。
在包括混入的第一种语言中,混入只包含方法,不包含状态,例如,成员变量。现在很多语言(Groovy也在其中)都包括有状态的混入。Scala的特征也以有状态的方式进行操作。
Groovy的混入
Groovy通过metaClass.mixin()方法或@Mixin注解来实现混入。(@Mixin注解依次使用GroovyAbstractSyntaxTree(AST)转换,以支持所需的元编程管道。)清单1中的示例使用metaClass.mixin()让File类能够创建ZIP压缩文件:
清单1.将zip()方法混合到File类中
classZipper{ defzip(dest){ newZipOutputStream(newFileOutputStream(dest)) .withStream{ZipOutputStreamzos-> eachFileRecurse{f-> if(!f.isDirectory()){ zos.putNextEntry(newZipEntry(f.getPath())) newFileInputStream(f).withStream{s-> zos<在清单1中,我创建了一个Zipper类,它包含新的zip()方法,以及将该方法添加到现有File类的连接。zip()方法的(不起眼的)Groovy代码以递归方式创建了一个ZIP文件。清单的最后一部分通过使用静态的初始化程序,将新方法添加到现有的File类。在Java语言中,类的静态初始化程序在加载类的时候运行。静态初始化程序是扩充代码的理想位置,因为在运行依赖于增强的任何代码之前,应确保先运行初始化程序。在清单1中,mixin()方法将zip()方法添加到File。
在"没有继承性的扩展,第1部分"中,我介绍了两种Groovy机制:ExpandoMetaClass和类别类,您可以使用它们在现有类上添加、更改或删除方法。使用mixin()添加方法的最终结果与使用ExpandoMetaClass或类别类添加方法的最终结果相同,但它们的实现是不一样的。请考虑清单2中混入示例:
清单2.混入操纵继承层次结构
importgroovy.transform.ToString classDebugInfo{ defgetWhoAmI(){ println"${this.class}<-${super.class.name} <<--${this.getClass().getSuperclass().name}" } } @ToStringclassPerson{ defname,age } @ToStringclassEmployeeextendsPerson{ defid,role } @ToStringclassManagerextendsEmployee{ defsuiteNo } Person.mixin(DebugInfo) defp=newPerson(name:"Pete",age:33) defe=newEmployee(name:"Fred",age:25,id:"FRE",role:"Manager") defm=newManager(name:"Burns",id:"001",suiteNo:"1A") p.whoAmI e.whoAmI m.whoAmI在清单2中,我创建了一个名为DebugInfo的类,其中包含一个getWhoAmI属性定义。在该属性内,我打印出类的一些详细信息,比如当前类以及super和getClass().getSuperClass()属性的父子关系说明。接下来,我创建一个简单的类层次结构,包括Person、Employee和Manager。
然后我将DebugInfo类混合到驻留在层次结构顶部的Person类。由于Person具有whoAmI属性,所以其子类也具有该属性。
在输出中,可以看到(并且可能会感到惊讶),DebugInfo类将自己插入到继承层次结构中:
classPerson<-DebugInfo<<--java.lang.Object classEmployee<-DebugInfo<<--Person classManager<-DebugInfo<<--Employee混入方法必须适应Groovy中现有的复杂关系,以便进行方法解析。清单2中的父类的不同返回值反映了这些关系。方法解析的细节不属于本文的讨论范围。但请小心处理对混入方法中的this和super值(及其各种形式)的依赖。
使用类别类或ExpandoMetaClass不影响继承,因为您只是对类进行修改,而不是混入不同的新行为中。这样做的一个缺点是,无法将这些更改识别为一个不同类别的构件。如果我使用类别类或ExpandoMetaClass将相同的三个方法添加到多个类中,那么没有特定的代码构件(比如接口或类签名)可以识别目前存在的共性。混入的优点是,Groovy将使用混入的一切都视为一个类别。
类别类实现的一个麻烦之处在于严格的类结构。您必须完全使用静态方法,每个方法至少需要接受一个参数,以代表正在进行扩充的类型。元编程是最有用的,它可以消除这样的样板代码。@Mixin注释的出现使得创建类别并将它们混合到类中变得更容易。清单3(摘自Groovy文档)说明了类别和混入之间的协同效应:
清单3.结合类别和混入
interfaceVehicle{ StringgetName() } @Category(Vehicle)classFlying{ deffly(){"I'mthe${name}andIfly!"} } @Category(Vehicle)classDiving{ defdive(){"I'mthe${name}andIdive!"} } @Mixin([Diving,Flying]) classJamesBondVehicleimplementsVehicle{ StringgetName(){"JamesBond'svehicle"} } assertnewJamesBondVehicle().fly()== "I'mtheJamesBond'svehicleandIfly!" assertnewJamesBondVehicle().dive()== "I'mtheJamesBond'svehicleandIdive!"在清单3中,我创建了一个简单的Vehicle接口和两个类别类(Flying和Diving)。@Category注释关注样板代码的要求。在定义了类别之后,我将它们混合成一个JamesBondVehicle,以便连接两个行为。
类别、ExpandoMetaClass和混入在Groovy中的交集是积极的语言进化的必然结果。三种技术明显有重叠之处,但每种技术都有它们自身才能处理得最好的强项。如果从头重新设计Groovy,那么作者可能会将三种技术的多个特性整合在一个机制中。
Scala的特征
Scala通过特征实现了代码重用,这是类似于混入的一个核心语言特性。Scala中的特征是有状态的(它们可以同时包括方法和字段),它们与Java语言中的接口扮演相同的instanceof角色。特征和混入解决了多个相同的问题,但特征在语言严谨性方面获得了更多的支持。
In"Groovy、Scala和Clojure中的共同点,第1部分"中,我使用了一个复数类来说明Scala中的操作符重载。我没有在该类中实现布尔比较操作符,因为Scala内置的Ordered特征使得实现变得微不足道。清单4显示了改进的复数类,它利用了Ordered特征的优势:
清单4.比较复数
finalclassComplex(valreal:Int,valimaginary:Int)extendsOrdered[Complex]{ require(real!=0||imaginary!=0) def+(operand:Complex)= newComplex(real+operand.real,imaginary+operand.imaginary) def+(operand:Int)= newComplex(real+operand,imaginary) def-(operand:Complex)= newComplex(real-operand.real,imaginary-operand.imaginary) def-(operand:Int)= newComplex(real-operand,imaginary) def*(operand:Complex)= newComplex(real*operand.real-imaginary*operand.imaginary, real*operand.imaginary+imaginary*operand.real) overridedeftoString()= real+(if(imaginary<0)""else"+")+imaginary+"i" overridedefequals(that:Any)=thatmatch{ caseother:Complex=>(real==other.real)&&(imaginary==other.imaginary) case_=>false } overridedefhashCode():Int= 41*((41+real)+imaginary) defcompare(that:Complex):Int={ defmyMagnitude=Math.sqrt(this.real^2+this.imaginary^2) defthatMagnitude=Math.sqrt(that.real^2+that.imaginary^2) (myMagnitude-thatMagnitude).round.toInt } }我在清单4中没有实现>、<、<=和>=运算符,但我可以在复数实例中调用它们,如清单5所示:
清单5.测试比较
classComplexTestextendsFunSuite{ test("comparison"){ assert(newComplex(1,2)>=newComplex(3,4)) assert(newComplex(1,1)newComplex(1,1)) assert(newComplex(1,2)>=newComplex(1,2)) assert(newComplex(1,2)<=newComplex(1,2)) } } 因为不需要采用数学上定义的技术来比较复数,所以在清单4中,我使用了一个被人们普遍接受的算法来比较数字的大小。我使用Ordered[Complex]特征来extend类定义,它混入了参数化的类的布尔运算符。为了让特征可以正常工作,注入的运算符必须比较两个复数,这是compare()方法的目的。如果您尝试extendOrdered特征,但不提供所需的方法,那么编译器消息会通知您,因为缺少所需的方法,所以必须将您的类声明为abstract。
在Scala中,特征有两个明确定义的作用:丰富接口和执行可堆叠的修改。
丰富接口
在设计接口时,Java开发人员面临着一个取决于便利性的难题:应该创建包含很多方法的富接口,还是创建只有几个方法的瘦接口?富接口对于其消费者更方便一些,因为它提供了广泛的方法调色板,但方法的绝对数量使得接口更加难以实现。瘦接口的问题刚好相反。
特征可以解决使用富接口还是薄接口的这种两难问题。您可以在瘦接口中创建核心功能,然后使用特征扩充它,以提供更丰富的功能。例如,在Scala中,Set特征实现了一个设置好的共享功能,您选择的子特征(mutable或immutable)已经决定了设置是否可变。
可堆叠的修改
Scala中的特征的另一个常见用途是可堆叠的修改。利用特征,您可以修改现有的方法并添加新的方法,super提供了对可以链接回以前的特征实现的访问。
清单6通过一些队列说明了可堆叠的修改:
清单6.构建可堆叠的修改
abstractclassIntQueue{ defget():Int defput(x:Int) } importscala.collection.mutable.ArrayBuffer classBasicIntQueueextendsIntQueue{ privatevalbuf=newArrayBuffer[Int] defget()=buf.remove(0) defput(x:Int){buf+=x} } traitSquaringextendsIntQueue{ abstractoverridedefput(x:Int){super.put(x*x)} }在清单6中,我创建一个简单的IntQueue类。然后,我构建一个可变的版本,该版本中包括ArrayBuffer。Squaring特征扩展了所有IntQueue,并在值被插入队列中时自动对其进行平方计算。在Squaring特征内对super的调用提供对堆栈中前面的特性的访问。除了第一个方法之外,只要每个被重写的方法调用super,修改堆栈就会一个一个地堆叠上去,如清单7所示:
清单7.构建堆叠的实例
objectTest{ defmain(args:Array[String]){ valqueue=(newBasicIntQueuewithSquaring) queue.put(10) queue.put(20) println(queue.get())//100 println(queue.get())//400 } }super清单6中对super的使用说明了特征和混入之间的重要区别。因为您在创建原始的类后(确实)混入了它们,所以混入必须解决类层次结构中的当前位置上的潜在不确定性。特征在创建类的时候已被线性化;编译器解决了什么是super的问题,没有不确定性。严格定义的复杂规则(这超出了本文的范围)控制了线性化在Scala中的工作方式。特征还为Scala解决了钻石问题。当Scala跟踪方法的源和解析时,不可能出现不确定性,因为该语言定义了明确的规则来处理解析。
结束语
在本期文章中,我探讨了混入(在Groovy中)和特征(在Scala中)之间的异同。混入和特征提供了许多相似的特性,但在实现细节方面有所不同,在某些重要方面,说明了不同的语言哲学。在Groovy中,混入是以注释的形式存在的,并使用了AST转换所提供的强大元编程功能。混入、类别类和ExpandoMetaClass在功能上有些重叠,它们有微小(但重要)的差异。诸如Ordered之类的Scala中的特征形成了Scala中大部分内置功能所依赖的核心语言特性。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。