深入学习java中的Groovy 和 Scala 类

前言

Java 传承的是平台,而不是语言。有超过 200 种语言可以在 JVM 上运行,它们之中不可避免地会有一种语言最终将取代 Java 语言,成为编写 JVM 程序的最佳方式。本系列将探讨三种下一代 JVM 语言:Groovy、Scala 和 Clojure,比较并对比新的功能和范例,让 Java 开发人员对自己近期的未来发展有大体的认识。

Java 语言的开发人员精通 C++ 和其他语言,包括多继承(multiple inheritance),使得类可以继承自任意数量的父类。多继承带来的一个问题是,不可能确定所继承的功能来自哪个父类。这个问题被称为钻石问题(请参阅 参考资料)。钻石问题和多继承中固有的其他复杂性启发了 Java 语言设计者选择 “单继承加接口” 的方法。

接口定义了语义,但没有定义行为。它们非常适合用来定义方法签名和数据抽象,所有 Java 下一代语言都支持 Java 接口,并且无需进行重大的修改。不过,有些交叉问题不适合使用 “单继承加接口” 模型。这种错位导致必须提供适合 Java 语言的外部机制,比如面向方面的编程。两种 Java 下一代语言(Groovy 和 Scala)通过使用一种被称为混入 或特征 的语言结构在另一个层次的扩展上处理这类问题。本文介绍了 Groovy 中的混入和 Scala 中的特征,并演示了如何使用它们。(Clojure 通过协议处理大致相同的功能,我在 Java 下一代:没有继承性的扩展,第 2 部分 中已经介绍过这一点。)

混入

混入的概念起源于 Flavors 语言(请参阅 参考资料)。这个概念的灵感来自于开发该语言的办公室附近的一家冰​​淇淋店。这家冰淇淋店提供了纯口味的冰淇淋,以及客户想要的其他任何的 “混合物”(糖果碎、糖屑、坚果,等等)。

早期的一些面向对象语言在单个代码块中共同定义某个类的属性和方法,所有类定义是完整的。在其他语言中,开发人员可以在一个地方定义属性,但推迟方法的定义,并在适当的时候将它们 “混合” 到类中。随着面向对象语言的演变,混入与现代语言的配合方式的细节也在演变。

在 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() 方法的目的。如果您尝试 extendOrdered 特征,但不提供所需的方法,那么编译器消息会通知您,因为缺少所需的方法,所以必须将您的类声明为 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 中大部分内置功能所依赖的核心语言特性。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java8与Scala中的Lambda表达式深入讲解

    前言 最近几年Lambda表达式风靡于编程界.很多现代编程语言都把它作为函数式编程的基本组成部分.基于JVM的编程语言如Scala.Groovy及Clojure把它作为关键部分集成在语言中.而如今,(最终)Java 8也加入了这个有趣的行列. Java8 终于要支持Lambda表达式!自2009年以来Lambda表达式已经在Lambda项目中被支持.在那时候,Lambda表达式仍被称为Java闭包.在我们进入一些代码示例以前,先来解释下为什么Lambda表达式在Java程序员中广受欢迎. 1.为

  • 浅析Java和Scala中的Future

    随着CPU的核数的增加,异步编程模型在并发领域中的得到了越来越多的应用,由于Scala是一门函数式语言,天然的支持异步编程模型,今天主要来看一下Java和Scala中的Futrue,带你走入异步编程的大门. Future 很多同学可能会有疑问,Futrue跟异步编程有什么关系?从Future的表面意思是未来,一个Future对象可以看出一个将来得到的结果,这就和异步执行的概念很像,你只管自己去执行,只要将最终的结果传达给我就行,线程不必一直暂停等待结果,可以在具体异步任务执行的时候去执行其他操作

  • Java和scala实现 Spark RDD转换成DataFrame的两种方法小结

    一:准备数据源 在项目下新建一个student.txt文件,里面的内容为: 1,zhangsan,20 2,lisi,21 3,wanger,19 4,fangliu,18 二:实现 Java版: 1.首先新建一个student的Bean对象,实现序列化和toString()方法,具体代码如下: package com.cxd.sql; import java.io.Serializable; @SuppressWarnings("serial") public class Stude

  • 深入学习java中的Groovy 和 Scala 类

    前言 Java 传承的是平台,而不是语言.有超过 200 种语言可以在 JVM 上运行,它们之中不可避免地会有一种语言最终将取代 Java 语言,成为编写 JVM 程序的最佳方式.本系列将探讨三种下一代 JVM 语言:Groovy.Scala 和 Clojure,比较并对比新的功能和范例,让 Java 开发人员对自己近期的未来发展有大体的认识. Java 语言的开发人员精通 C++ 和其他语言,包括多继承(multiple inheritance),使得类可以继承自任意数量的父类.多继承带来的一

  • 学习Java中的List集合

    目录 1.概述 2.List的使用 2.1List的常用方法 3.List的实现类 3.1ArrayList 3.2Vector 3.3LinkedList 3.4ArrayList与Vector的区别 1.概述 List是一个有序集合(也被称为序列).此接口的用户在列表中的每个元素都被插入的地方有精确的控制.用户可以通过它们的整数索引(在列表中的位置)访问元素,并在列表中搜索元素. 说是List集合,其实只是习惯说法,因为它是Collection接口的一个子接口(Collection有很多的子

  • java中如何反射获取一个类

    反射说白了就是可以获得一个类的所有信息,主要包括方法和属性两部分. 1.获得方法包括获得方法的名称,方法的返回类型,方法的访问修饰符,以及通过反射执行这个方法. 2.获得属性包括属性的名称,类型,访问修饰符,以及这个属性的值. 这些获得都有相应的API提供操作. 代码如下: package poi; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Meth

  • java中GZIP压缩解压类使用实例

    java中GZIP压缩解压类使用实例 当我们客户端与服务端进行数据传输时需要走流量,为了节省流量我们常常需要写一个压缩类对数据进行压缩. 实例代码: import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStr

  • 浅谈Java中常用数据结构的实现类 Collection和Map

    线性表,链表,哈希表是常用的数据结构,在进行Java开发时,JDK已经为我们提供了一系列相应的类来实现基本的数据结构.这些类均在java.util包中.本文试图通过简单的描述,向读者阐述各个类的作用以及如何正确使用这些类. Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set Map ├Hashtable ├HashMap └WeakHashMap Collection接口 Collection是最基本的集合接口,一个C

  • 全面了解Java中的内部类和匿名类

    Java内部类(Inner Class),类似的概念在C++里也有,那就是嵌套类(Nested Class),乍看上去内部类似乎有些多余,它的用处对于初学者来说可能并不是那么显著,但是随着对它的深入了解,你会发现Java的设计者在内部类身上的确是用心良苦.学会使用内部类,是掌握Java高级编程的一部分,它可以让你更优雅地设计你的程序结构.下面从以下几个方面来介绍: 第一次见面 public interface Contents { int value(); } public interface

  • java 中 String format 和Math类实例详解

    java 中 String format 和Math类实例详解 java字符串格式化输出 @Test public void test() { // TODO Auto-generated method stub //可用printf(); System.out.println(String.format("I am %s", "jj")); //%s字符串 System.out.println(String.format("首字母是 %c",

  • Java中字符数组、String类、StringBuffer三者之间相互转换

    一.StringBuffer与String的相互转换 1.将StringBuffer转换成String StringBuffer类成员toString函数可将其转换成String类型. StringBuffer buffer = newStringBuffer("abcd"); String str = buffer.toString(); 通过String类中的构造将一个StringBuffer类转换为String类:String(StringBuffer buffer) Strin

  • java中的Arrays这个工具类你真的会用吗(一文秒懂)

    Java源码系列三-工具类Arrays ​ 今天分享java的源码的第三弹,Arrays这个工具类的源码.因为近期在复习数据结构,了解到Arrays里面的排序算法和二分查找等的实现,收益匪浅,决定研读一下Arrays这个类的源码.不足之处,欢迎在评论区交流和指正. 1.认识Arrays这个类: ​ 首先它在java的utils包下,属于Java Collections Framework中的一员.它的初衷就是一个工具类,封装了操纵数组的各种方法,比如排序,二分查找,数组的拷贝等等.满足了我们日常

  • Java中解密微信加密数据工具类

    当我们开发微信公众号,小程序等,微信返回给我们的数据往往是经过加密的,我们需要使用 sessionKey 配合解密,才能得到我们想要的数据 1.引入依赖 <!-- lombok依赖 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> <

随机推荐