Java 下一代: 混入和特征
来自http://www.ibm.com/developerworks/cn/java/j-jn8/index.html
======================================转载正文
Java 语言的开发人员精通 C++ 和其他语言,包括多继承(multiple inheritance),使得类可以继承自任意数量的父类。多继承带来的一个问题是,不可能确定所继承的功能来自哪个父类。这个问题被称为钻石问题(请参阅 参考资料)。钻石问题和多继承中固有的其他复杂性启发了 Java 语言设计者选择 “单继承加接口” 的方法。
接口定义了语义,但没有定义行为。它们非常适合用来定义方法签名和数据抽象,所有 Java 下一代语言都支持 Java 接口,并且无需进行重大的修改。不过,有些交叉问题不适合使用 “单继承加接口” 模型。这种错位导致必须提供适合 Java 语言的外部机制,比如面向方面的编程。两种 Java 下一代语言(Groovy 和 Scala)通过使用一种被称为混入 或特征 的语言结构在另一个层次的扩展上处理这类问题。本文介绍了 Groovy 中的混入和 Scala 中的特征,并演示了如何使用它们。(Clojure 通过协议处理大致相同的功能,我在 Java 下一代:没有继承性的扩展,第 2 部分 中已经介绍过这一点。)
混入
早期的一些面向对象语言在单个代码块中共同定义某个类的属性和方法,所有类定义是完整的。在其他语言中,开发人员可以在一个地方定义属性,但推迟方法的定义,并在适当的时候将它们 “混合” 到类中。随着面向对象语言的演变,混入与现代语言的配合方式的细节也在演变。
在 Ruby、Groovy 和类似的语言中,作为一个接口和父类之间的交叉,混入可以扩充现有的类层次结构。像接口一样,混入可以充当 instanceof
检查的类型,同时也要遵循相同的扩展规则。您可以将无限数量的混入应用于某一个类。与接口不同的是,混入不仅指定了方法签名,也可以实现签名的行为。
在包括混入的第一种语言中,混入只包含方法,不包含状态,例如,成员变量。现在很多语言(Groovy 也在其中)都包括有状态的混入。Scala 的特征也以有状态的方式进行操作。
Groovy 的混入
Groovy 通过 metaClass.mixin()
方法或 @Mixin
注解来实现混入。(@Mixin
注解依次使用 Groovy Abstract Syntax Tree (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 中混入示例:
清单 2. 混入操纵继承层次结构
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
类。由于 Person
具有 whoAmI
属性,所以其子类也具有该属性。
在输出中,可以看到(并且可能会感到惊讶),DebugInfo
类将自己插入到继承层次结构中:
class Person <- DebugInfo <<-- java.lang.Object class Employee <- DebugInfo <<-- Person class Manager <- DebugInfo <<-- Employee
混入方法必须适应 Groovy 中现有的复杂关系,以便进行方法解析。清单 2 中的父类的不同返回值反映了这些关系。方法解析的细节不属于本文的讨论范围。但请小心处理对混入方法中的 this
和 super
值(及其各种形式)的依赖。
使用类别类或 ExpandoMetaClass
不影响继承,因为您只是对类进行修改,而不是混入不同的新行为中。这样做的一个缺点是,无法将这些更改识别为一个不同类别的构件。如果我使用类别类或 ExpandoMetaClass
将相同的三个方法添加到多个类中,那么没有特定的代码构件(比如接口或类签名)可以识别目前存在的共性。混入的优点是,Groovy 将使用混入的一切都视为一个类别。
类别类实现的一个麻烦之处在于严格的类结构。您必须完全使用静态方法,每个方法至少需要接受一个参数,以代表正在进行扩充的类型。元编程是最有用的,它可以消除这样的样板代码。@Mixin
注释的出现使得创建类别并将它们混合到类中变得更容易。清单 3(摘自 Groovy 文档)说明了类别和混入之间的协同效应:
清单 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
,以便连接两个行为。
类别、ExpandoMetaClass
和混入在 Groovy 中的交集是积极的语言进化的必然结果。三种技术明显有重叠之处,但每种技术都有它们自身才能处理得最好的强项。如果从头重新设计 Groovy,那么作者可能会将三种技术的多个特性整合在一个机制中。
Scala 的特征
Scala 通过特征 实现了代码重用,这是类似于混入的一个核心语言特性。Scala 中的特征是有状态的(它们可以同时包括方法和字段),它们与 Java 语言中的接口扮演相同的 instanceof
角色。特征和混入解决了多个相同的问题,但特征在语言严谨性方面获得了更多的支持。
In "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 } }
super
清单 6 中对 super
的使用说明了特征和混入之间的重要区别。因为您在创建原始的类后(确实)混入了它们,所以混入必须解决类层次结构中的当前位置上的潜在不确定性。特征在创建类的时候已被线性化;编译器解决了什么是 super
的问题,没有不确定性。严格定义的复杂规则(这超出了本文的范围)控制了线性化在 Scala 中的工作方式。特征还为 Scala 解决了钻石问题。当 Scala 跟踪方法的源和解析时,不可能出现不确定性,因为该语言定义了明确的规则来处理解析。
结束语
在本期文章中,我探讨了混入(在 Groovy 中)和特征(在 Scala 中)之间的异同。混入和特征提供了许多相似的特性,但在实现细节方面有所不同,在某些重要方面,说明了不同的语言哲学。在 Groovy 中,混入是以注释的形式存在的,并使用了 AST 转换所提供的强大元编程功能。混入、类别类和 ExpandoMetaClass
在功能上有些重叠,它们有微小(但重要)的差异。诸如 Ordered
之类的 Scala 中的特征形成了 Scala 中大部分内置功能所依赖的核心语言特性。
相关推荐
在Java开发领域,`java.lang.ClassFormatError: Bad version information`是一个常见的错误,通常发生在尝试加载或执行不兼容的字节码时。这个错误表明Java虚拟机(JVM)遇到了一个它无法识别或者处理的类文件版本。...
1. **类型擦除**:Java泛型的主要特点是类型擦除。这意味着在编译完成后,所有的泛型信息都会被擦除,替换为Object或者其他基础类型。因此,泛型在运行时并不存在,所有关于泛型的操作都在编译期间完成。 2. **边界...
另外,Ruby中的模块(Module)和Java的包(Package)相似,但模块还扮演着命名空间和混入(Mix-in)的角色。混入允许模块中的方法被其他类直接继承和使用,这是一种不同于Java继承的独特方式。 在错误处理方面,...
在现代前端开发中,"es6-cssnext-postcss-test"是一个典型的项目名称,它揭示了开发者正在探索和应用下一代前端技术。这个项目的核心是利用ES6(ECMAScript 2015)的特性,CSSNext(一个CSS预处理器),以及PostCSS...
1. **Java源代码解析**:Java文件分析器首先需要读取.java文件,通过解析Java语法,将源代码转化为抽象语法树(Abstract Syntax Tree, AST)。AST是对源代码的一种结构化表示,每个节点代表代码的一个部分,如类、...
### Java编码常犯错误 #### 一、字符串拼装SQL **问题描述**:在Java开发过程中,直接使用字符串拼接的方式构造SQL语句是一种常见的做法。然而这种方式存在明显的安全隐患和性能问题。 - **安全漏洞**:直接拼接...
compileOnly files('libs/abilityshell_ide_java.jar') 2. 在AndroidManifest.xml中,向根节点下增加。 <uses-feature android:name="zidane.software.ability" android:required="false" /> 3. 向...
- **Trait继承**:trait在Scala中是一种可混入的模块化代码块,可以被类和对象混入使用。 在JVM上编译Scala时,需要考虑如何将Scala的mixin概念转化为JVM能够理解的形式。这涉及到多个阶段: 1. **Encoding mixins...
10. `JDBC`:Java数据库连接,用于执行SQL查询和操作数据库。 通过这个压缩包,开发者可以学习到Spring AOP的实际应用,以及Java Web开发中的常见组件和机制,如过滤器、监听器、MVC架构、JDBC数据库操作等,对于...
通过让一个类模板从其自身类的模板实例化中派生,CRTP能够实现方法重写和编译时的多态行为,提高Java应用程序的效率和性能。 ## 二、好奇递归模板模式的别名 * CRTP * Mixin Inheritance(混入继承) * Recursive ...
ASM是一个Java字节码操控和分析框架,可以直接生成和修改Java类和注解。ASM提供了非常底层的访问,允许开发者在运行时动态改变类的行为,或者在编译时生成新的类。通过ASM,Mixin能够精确地定位并修改Java类的字节码...
Java泛型机制详解 Java泛型是Java语言中的一种机制,用于在编译期检查类型安全。Java泛型的出现解决了Java早期版本中类型安全检查的缺陷。Java泛型的好处是可以在编译期检查类型安全,避免了运行时的...
2. 在组件中使用混入:var vm = new Vue({ mixins: [mixin], ... }),其中 mixin 是刚才定义的混入对象。 混入的选项合并: 1. 当组件和混入对象含有同名选项时,这些选项将以恰当的方式混合。 2. 数据对象在内部会...
"治疗"和"耳穴 贴压"这两个标签看似与Java编程无关,但可能是由于数据标注错误或者混入了其他主题的信息。如果它们确实与Java资料有关,可能是指东软在某个项目中采用了与健康科技或生物信息学相关的技术,例如利用...
- **计算属性与侦听器**:混入的计算属性和侦听器与组件的计算属性和侦听器合并,不会发生冲突。 - **方法**:混入的方法和组件的方法合并到一个数组中,混入的方法优先级高于组件的方法。 - **生命周期钩子**:所有...
Servlet主要用于处理请求和响应,更适合编写业务逻辑,而JSP则更倾向于页面展示,允许直接在HTML中混入Java代码。按照Sun(现Oracle)的推荐,应将JSP作为视图层,只包含HTML内容,而业务逻辑应放在JavaBean中,通过...
Vue.js 混入 混入 (mixins)定义了一部分可复用的方法或者计算属性。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。 来看一个简单的实例: 实例 var vm = new ...
根据提供的文档内容,可以看出这份文档实际上是一份个人简历模板,并且混入了一段与主题无关的文学内容。这里我们将重点放在简历模板中的相关信息,并从中提取出对于一个高级Java工程师或架构师来说重要的知识点。 ...