Java语言的开发人员精通C ++和其他包含多重继承的语言,从而使类可以从任意数量的父级继承。 多重继承的问题之一是无法确定派生自哪个父继承功能。 这个问题称为菱形问题 (请参阅参考资料 )。 多重继承中固有的菱形问题和其他复杂性启发了Java语言设计人员选择单一继承加接口。
接口定义语义,但不定义行为。 它们很好地用于定义方法签名和数据抽象,并且所有Java.next语言都支持Java接口,而无需进行必要的更改。 但是,某些跨领域的关注点不适用于单继承+接口模型。 这种错位导致需要Java语言的外部机制,例如面向方面的编程。 两种Java.next语言-Groovy和Scala-通过使用称为mixin或trait的语言构造在另一个扩展级别上处理此类问题。 本文介绍了Groovy mixins和Scala特性,并展示了如何使用它们。 (Clojure通过协议处理了许多相同的功能,我在Java.next中介绍了该协议:没有继承的扩展,第2部分 。)
关于本系列
Java的遗产将是平台,而不是语言。 JVM上运行着200多种语言,不可避免的是,其中之一最终将取代Java语言,成为对JVM进行编程的最佳方法。 本系列探讨了三种下一代JVM语言:Groovy,Scala和Clojure,对新功能和范例进行了比较和对比,以使Java开发人员可以一窥他们不久的将来。
混合蛋白
冰淇淋的灵感
mixin概念起源于Flavors语言(请参阅参考资料 )。 这个概念的灵感来自发生语言发展的办公室附近的一家冰淇淋店。 冰淇淋店提供了普通口味的冰淇淋,以及客户想要的任何其他“混搭”(碎糖条,巧克力粉,坚果等)。
一些早期的面向对象的语言在单个代码块中一起定义了类的属性和方法,从而完成了类定义。 用其他语言,开发人员可以在一个地方定义属性,但是将方法定义推迟到以后再在适当的时候将它们“混合”到类中。 随着面向对象语言的发展,mixin如何与现代语言一起工作的细节也随之发展。
在Ruby,Groovy和类似语言中,mixins扩展了现有类的层次结构,作为接口和父类之间的交叉。 像接口一样,mixin都充当instanceof
检查的类型,并且遵循相同的扩展规则。 您可以将无限数量的mixin应用于一个类。 与接口不同,mixin不仅可以指定方法签名,还可以实现签名的行为。
在最早包含mixin的语言中,mixin仅包含方法,而没有诸如成员变量之类的状态。 现在,许多语言(其中包括Groovy)都包含有状态的mixin。 斯卡拉特质也表现得很庄重。
Groovy Mixins
Groovy通过metaClass.mixin()
方法或@Mixin
批注实现了mixins。 ( @Mixin
批注依次将Groovy抽象语法树(AST)转换用于必需的元编程管道。)清单1中的示例使用metaClass.mixin()
来使File
类具有创建压缩ZIP文件的能力:
清单1.将zip()
方法混合到File
类中
class Zipper {
def zip(dest) {
new ZipOutputStream(new FileOutputStream(dest))
.withStream { ZipOutputStream zos ->
eachFileRecurse { f ->
if (!f.isDirectory()) {
zos.putNextEntry(new ZipEntry(f.getPath()))
new FileInputStream(f).withStream { s ->
zos << s
zos.closeEntry()
}
}
}
}
}
static {
File.metaClass.mixin(Zipper)
}
}
在清单1中,我创建了一个Zipper
类,其中包含新的zip()
方法和将该方法添加到现有File
类的连线。 zip()
方法的(不起眼的)Groovy代码以递归方式创建一个ZIP文件。 清单的最后一部分通过使用静态初始化程序将新方法连接到现有的File
类中。 与Java语言一样,静态类初始化程序在类加载时运行。 静态初始化程序是扩充代码的理想位置,因为可以确保初始化程序在依赖于增强功能的任何代码之前运行。 在清单1中 , mixin()
方法将zip()
方法添加到File
。
在“ 没有继承的扩展,第1部分 ”中,我介绍了两种Groovy机制ExpandoMetaClass
和类别类,可用于在现有类上添加,更改或删除方法。 使用mixin()
添加方法与使用ExpandoMetaClass
或类别类添加方法具有相同的最终结果,但是实现方式有所不同。 考虑清单2中的mixin示例:
清单2.操作继承层次结构的Mixins
import groovy.transform.ToString
class DebugInfo {
def getWhoAmI() {
println "${this.class} <- ${super.class.name}
<<-- ${this.getClass().getSuperclass().name}"
}
}
@ToString class Person {
def name, age
}
@ToString class Employee extends Person {
def id, role
}
@ToString class Manager extends Employee {
def suiteNo
}
Person.mixin(DebugInfo)
def p = new Person(name: "Pete", age: 33)
def e = new Employee(name: "Fred", age: 25, id:"FRE", role:"Manager")
def m = new Manager(name: "Burns", id: "001", suiteNo: "1A")
p.whoAmI
e.whoAmI
m.whoAmI
在清单2中,我创建了一个名为DebugInfo
的类,其中包含一个getWhoAmI
属性定义。 在此属性中,我打印出该类的一些详细信息,例如当前类以及super
和getClass().getSuperClass()
属性对父项的观点。 接下来,我创建一个简单的类层次结构,该层次结构由Person
, Employee
和Manager
。
然后,我将DebugInfo
类混合到Person
类中,该类位于层次结构的顶部。 因为whoAmI
属性存在于Person
,所以它也存在于其子类中。
在输出中,您可以看到(并且可能会惊讶地看到) DebugInfo
类将自身暗示到继承层次结构中:
class Person <- DebugInfo <<-- java.lang.Object
class Employee <- DebugInfo <<-- Person
class Manager <- DebugInfo <<-- Employee
Mixin方法必须适合Groovy已经很复杂的关系才能进行方法解析。 清单2的父类的不同返回值反映了这些关系。 方法解析的详细信息超出了本文的范围。 但是要警惕在mixin方法中依赖this
和super
(以各种形式)的值。
使用类别类或ExpandoMetaClass
不会影响继承,因为您可以对类进行更改,而不是混入不同的新行为。 缺点是无法将这些更改识别为明显的分类工件。 如果我使用类别类或ExpandoMetaClass
将相同的三个方法添加到多个类中,则没有特定的代码工件(例如接口或类签名)标识现在存在的通用性。 mixin的优点是Groovy会将使用mixin作为类别的所有内容都对待。
类别类的实现烦恼之一是严格的类结构。 您必须使用所有静态方法(每个方法至少接受一个参数)来表示正在扩充的类型。 当消除此类样板代码时,元编程最有用。 @Mixin
批注的出现使创建类别并将其混合到类中变得更加容易。 清单3(Groovy文档的摘录)说明了类别和mixin之间的协同作用:
清单3.组合类别和混合
interface Vehicle {
String getName()
}
@Category(Vehicle) class Flying {
def fly() { "I'm the ${name} and I fly!" }
}
@Category(Vehicle) class Diving {
def dive() { "I'm the ${name} and I dive!" }
}
@Mixin([Diving, Flying])
class JamesBondVehicle implements Vehicle {
String getName() { "James Bond's vehicle" }
}
assert new JamesBondVehicle().fly() ==
"I'm the James Bond's vehicle and I fly!"
assert new JamesBondVehicle().dive() ==
"I'm the James Bond's vehicle and I dive!"
在清单3中,我创建了一个简单的Vehicle
接口和两个类别类( Flying
和Diving
)。 @Category
注释符合样板代码要求。 定义类别后,将它们混合到JamesBondVehicle
,从而附加两种行为。
Groovy中的类ExpandoMetaClass
和mixins的交集是激进的语言发展的必然结果。 三种技术有明显的重叠,但每种技术都有其各自的最佳表现。 如果从头开始对Groovy进行重新设计,那么作者可能会将这三种技术的许多功能整合为一个机制。
斯卡拉特质
Scala通过traits实现代码重用, traits是类似于mixin的核心语言功能。 Scala的特点是有状态的-它们可以同时包含方法和字段-和他们玩相同instanceof
角色界面在Java语言中播放。 特质和混合素都解决了许多相同的问题,但特质得到了更多语言严格性的支持。
在“ Groovy,Scala和Clojure的共同点,第1部分 ”中,我使用了复数类来说明Scala中的运算符重载。 我没有在该类中实现布尔比较运算符,因为Scala的内置Ordered
特性使实现变得微不足道。 清单4显示了一个改进的复数类,它利用了Ordered
特性:
清单4.可比复数
final class Complex(val real: Int, val imaginary: Int) extends Ordered[Complex] {
require (real != 0 || imaginary != 0)
def +(operand: Complex) =
new Complex(real + operand.real, imaginary + operand.imaginary)
def +(operand: Int) =
new Complex(real + operand, imaginary)
def -(operand: Complex) =
new Complex(real - operand.real, imaginary - operand.imaginary)
def -(operand: Int) =
new Complex(real - operand, imaginary)
def *(operand: Complex) =
new Complex(real * operand.real - imaginary * operand.imaginary,
real * operand.imaginary + imaginary * operand.real)
override def toString() =
real + (if (imaginary < 0) "" else "+") + imaginary + "i"
override def equals(that: Any) = that match {
case other : Complex => (real == other.real) && (imaginary == other.imaginary)
case _ => false
}
override def hashCode(): Int =
41 * ((41 + real) + imaginary)
def compare(that: Complex) : Int = {
def myMagnitude = Math.sqrt(this.real ^ 2 + this.imaginary ^ 2)
def thatMagnitude = Math.sqrt(that.real ^ 2 + that.imaginary ^ 2)
(myMagnitude - thatMagnitude).round.toInt
}
}
我没有实现清单4中的>
, <
, <=
和>=
运算符,但是我可以在清单5所示的复数实例上调用它们:
清单5.测试比较
class ComplexTest extends FunSuite {
test("comparison") {
assert(new Complex(1, 2) >= new Complex(3, 4))
assert(new Complex(1, 1) < new Complex(2,2))
assert(new Complex(-10, -10) > new Complex(1, 1))
assert(new Complex(1, 2) >= new Complex(1, 2))
assert(new Complex(1, 2) <= new Complex(1, 2))
}
}
没有用于比较复数的数学定义方法,因此在清单4中,我使用了一种公认的算法来比较数的大小。 我使用Ordered[Complex]
特征extend
了类定义,该特征混合了参数化类的布尔运算符。 为了使特征发挥作用,注入的运算符必须比较两个复数,这是compare()
方法的目的。 如果您尝试extend
Ordered
特性但不提供所需的方法,则编译器消息会通知您,由于缺少必需的方法,您的类必须被声明为abstract
。
特性在Scala中有两个明确定义的角色:丰富接口和执行可堆叠的修改 。
丰富的界面
当Java开发人员设计接口时,他们面临着取决于便利性的难题:您应该创建一个包含许多方法的富接口,还是创建仅包含几个方法的瘦接口? 丰富的界面对其用户来说更为方便,因为它提供了广泛的方法选项,但是大量的方法使该界面更难以实现。 精简接口存在相反的问题。
性状解决了富人与瘦者之间的难题。 您可以在瘦界面中创建核心功能,然后使用特征对其进行扩充以提供更丰富的功能。 例如,在Scala中,“ Set
特征实现集合的共享功能,而您选择的子特征( mutable
或immutable
)确定集合是否可变。
可堆叠修改
在Scala中,特征的另一个常见用法是可堆叠的修改 。 使用特征,您可以更改现有方法并添加新方法,而super
提供了将链返回到先前特征的实现的访问权限。
清单6展示了带有数字队列的可堆叠修改:
清单6.构建可堆叠的修改
abstract class IntQueue {
def get(): Int
def put(x: Int)
}
import scala.collection.mutable.ArrayBuffer
class BasicIntQueue extends IntQueue {
private val buf = new ArrayBuffer[Int]
def get() = buf.remove(0)
def put(x: Int) { buf += x }
}
trait Squaring extends IntQueue {
abstract override def put(x: Int) { super.put(x * x) }
}
在清单6中,我创建了一个简单的IntQueue
类。 然后,我构建一个包含ArrayBuffer
的可变版本。 Squaring
特征扩展任何IntQueue
并在将值插入队列时自动平方值。 对Squaring
特征中的super
的调用提供了对堆栈中先前特征的访问。 只要第一个被覆盖的方法(第一个方法除外)都调用super
,这些修改就会彼此堆叠,如清单7所示:
清单7.构建堆叠的实例
object Test {
def main(args: Array[String]) {
val queue = (new BasicIntQueue with Squaring)
queue.put(10)
queue.put(20)
println(queue.get()) // 100
println(queue.get()) // 400
}
}
清单6中super
的使用说明了特性和mixin之间的重要区别。 由于在原始类创建之后(从字面上看)将它们混合在一起,因此,Mixins必须解决类层次结构中当前位置的潜在歧义。 特性在创建类时线性化; 编译器毫无疑问地解决了什么是super
。 复杂,严格定义的规则(不在本文的讨论范围内)控制着Scala中线性化的工作方式。 特性还解决了Scala的钻石问题。 当Scala跟踪方法的来源和解决方案时,就不会有任何歧义,因为该语言定义了明确的规则来处理解决方案。
结论
在本期中,我探讨了混合(在Groovy中)和特征(在Scala中)之间的异同。 Mixins和trait提供许多相似的功能,但是实现细节在说明不同语言哲学的重要方式上有所不同。 在Groovy中,mixins作为注释存在,并使用AST转换提供的强大的元编程功能。 Mixins,类别类和ExpandoMetaClass
在功能上都有重叠,但有细微(但很重要)的差异。 Scala中的诸如Ordered
构成了一种核心语言功能,Scala中的许多内置功能都依赖于此。
在下一期中,我将介绍Java.next语言中的currying和部分应用程序。
翻译自: https://www.ibm.com/developerworks/java/library/j-jn8/index.html