java

位置:IT落伍者 >> java >> 浏览文章

面向Java开发人员的Scala指南: 关于特征和行为


发布日期:2018年03月13日
 
面向Java开发人员的Scala指南: 关于特征和行为

摘要Scala 并不仅仅只给 JVM 引入了函数概念它还为我们提供了一种对于面向对象语言设计的现代视角在这一期的 面向 Java 开发人员的 Scala 指南 中Ted Neward 介绍了 Scala 如何利用特征(trait)使对象更加简单更易于构建您将了解到特征与 Java? 接口和 C++ 多重继承提供的传统极性既有相似之处也有不同之处

着名科学家研究学者艾萨克牛顿爵士有这样一句名言如果说我看得比别人远一些那是因为我站在巨人的肩膀上作为一名热心的历史和政治学家我想对这位伟人的名言略加修改如果说我看得比别人远一些那是因为我站在历史的肩膀上而这句话又体现出另一位历史学家 George Santayana 的名言忘记历史必将重蹈覆辙换句话说如果我们不能回顾历史从过去的错误(包括我们自己过去的经验)中吸取教训就没有机会做出改进

您可能会疑惑这样的哲学与 Scala 有什么关系?继承就是我们要讨论的内容之一考虑这样一个事实Java 语言的创建已经是近 年前的事情当时是 面向对象 的全盛时期它设计用于模仿当时的主流语言 C++尝试将使用这种语言的开发人员吸引到 Java 平台上来毫无疑问在当时看来这样的决策是明智而且必要的但回顾一下就会发现其中有些地方并不像创建者设想的那样有益

例如在二十年前对于 Java 语言的创建者来说反映 C++ 风格的私有继承和多重继承是必要的自那之后许多 Java 开发人开始为这些决策而后悔在这一期的 Scala 指南中我回顾了 Java 语言中多重继承和私有继承的历史随后您将看到 Scala 是怎样改写了历史为所有人带来更大收益

C++ 和 Java 语言中的继承

C++ 工作的人们能够回忆起私有继承是从基类中获取行为的一种方法不必显式地接受 ISA 关系将基类标记为 私有 允许派生类从该基类继承而来而无需实际成为 一个基类但对自身的私有继承是未得到广泛应用的特性之一继承一个基类而无法将它向下或向上转换到基类的理念是不明智的

另一方面多重继承往往被视为面向对象编程的必备要素在建模交通工具的层次结构时 SeaPlane 无疑需要继承 Boat(使用其 startEngine() 和 sail() 方法)以及 Plane(使用其 startEngine() 和 fly() 方法)SeaPlane 既是 Boat也是 Plane难道不是吗?

无论如何这是在 C++ 鼎盛时期的想法在快速转向 Java 语言时我们认为多重继承与私有继承一样存在缺陷所有 Java 开发人员都会告诉您SeaPlane 应该继承 Floatable 和 Flyable 接口(或许还包括 EnginePowered 接口或基类)继承接口意味着能够实现该类需要的所有方法而不会遇到 虚拟多重继承 的难题(遇到这种难题时要弄清楚在调用 SeaPlane 的 startEngine() 方法时应调用哪个基类的 startEngine())

遗憾的是彻底放弃私有继承和多重继承会使我们在代码重用方面付出昂贵的代价Java 开发人员可能会因从虚拟多重继承中解放出来而高兴但代价是程序员往往要完成辛苦而易于出错的工作

关于本系列

Ted Neward 将和您一起深入探讨 Scala 编程语言在这个新的 developerWorks 系列 中您将深入了解 Sacla并在实践中看到 Scala 的语言功能进行比较时Scala 代码和 Java 代码将放在一起展示但(您将发现)Scala 中的许多内容与您在 Java 编程中发现的任何内容都没有直接关联而这正是 Scala 的魅力所在!如果用 Java 代码就能够实现的话又何必再学习 Scala 呢?

回顾可重用行为

规范是 Java 平台的基础它带来了众多 Java 生态系统作为依据的 POJO我们都明白一点Java 代码中的属性由 get()/set() 对管理如清单 所示

清单 Person POJO

//This is Java

public class Person

{

private String lastName;

private String firstName;

private int age;

public Person(String fn String ln int a)

{

lastName = ln; firstName = fn; age = a;

}

public String getFirstName() { return firstName; }

public void setFirstName(String v) { firstName = v; }

public String getLastName() { return lastName; }

public void setLastName(String v) { lastName = v; }

public int getAge() { return age; }

public void setAge(int v) { age = v; }

}

这些代码看起来非常简单编写起来也不难但如果您希望提供通知支持 — 使第三方能够使用 POJO 注册并在变更属性时接收回调事情会怎样?根据 JavaBeans 规范必须实现 PropertyChangeListener 接口以及它的一个方法 propertyChange()如果您希望允许任何 POJO 的 PropertyChangeListener 都能够对属性更改 投票那么 POJO 就需要实现 VetoableChangeListener 接口该接口的实现又依赖于 vetoableChange() 方法的实现

至少事情应该是这样运作的

实际上希望成为属性变更通知接收者的用户必须实现 PropertyChangeListener 接口发送者(本例中的 Person 类)必须提供接收该接口实例的公共方法和监听器需要监听的属性名称最终得到更加复杂的 Person如清单 所示

清单 Person POJO 种形式

//This is Java

public class Person

{

// rest as before except that inside each setter we have to do something

// like:

// public setFoo(T newValue)

// {

// T oldValue = foo;

// foo = newValue;

// pcsfirePropertyChange(foo oldValue newValue);

// }

public void addPropertyChangeListener(PropertyChangeListener pcl)

{

// keep a reference to pcl

}

public void removePropertyChangeListener(PropertyChangeListener pcl)

{

// find the reference to pcl and remove it

}

}

保持引用属性变更监听器意味着 Person POJO 必须保留某种类型的集合类(例如 ArrayList)来包含所有引用然后必须实例化插入并移除 POJO — 由于这些操作不是原子操作因此还必须包含恰当的同步保护

最后如果某个属性发生变化属性监听器列表必须得到通知通常通过遍历 PropertyChangeListener 的集合并对各元素调用 propertyChange() 来实现此过程包括传入新的 PropertyChangeEvent 描述属性原有值和新值这是 PropertyChangeEvent 类和 JavaBeans 规范的要求

在我们编写的 POJO 中只有少数支持监听器通知这并不意外在这里要完成大量工作必须手动地重复处理所创建的每一个 JavaBean/POJO

除了工作还是工作 — 变通方法在哪里?

有趣的是C++ 对于私有继承的支持在 Java 语言中得到了延续今天我们用它来解决 JavaBeans 规范的难题一个基类为 POJO 提供了基本 add() 和 remove() 方法集合类以及 firePropertyChanged() 方法用于通知监听器属性变更

我们仍然可以通过 Java 类完成但由于 Java 缺乏私有继承Person 类必须继承 Bean 基类从而可向上转换 到 Bean这妨碍了 Person 继承其他类多重继承可能使我们不必处理后续的问题但它也重新将我们引向了虚拟继承而这是绝对要避免的

针对这个问题的 Java 语言解决方案是运用众所周知的支持 类在本例中是 PropertyChangeSupport实例化 POJO 中的一个类为 POJO 本身使用必要的公共方法各公共方法都调用 Support 类来完成艰难的工作更新后的 Person POJO 可以使用 PropertyChangeSupport如下所示

清单 Person POJO 种形式

//This is Java

import javabeans*;

public class Person

{

private String lastName;

private String firstName;

private int age;

private PropertyChangeSupport propChgSupport =

new PropertyChangeSupport(this);

public Person(String fn String ln int a)

{

lastName = ln; firstName = fn; age = a;

}

public String getFirstName() { return firstName; }

public void setFirstName(String newValue)

{

String old = firstName;

firstName = newValue;

propChgSupportfirePropertyChange(firstName old newValue);

}

public String getLastName() { return lastName; }

public void setLastName(String newValue)

{

String old = lastName;

lastName = newValue;

propChgSupportfirePropertyChange(lastName old newValue);

}

public int getAge() { return age; }

public void setAge(int newValue)

{

int old = age;

age = newValue;

propChgSupportfirePropertyChange(age old newValue);

}

public void addPropertyChangeListener(PropertyChangeListener pcl)

{

propChgSupportaddPropertyChangeListener(pcl);

}

public void removePropertyChangeListener(PropertyChangeListener pcl)

{

propChgSupportremovePropertyChangeListener(pcl);

}

}

不知道您有何感想但这段代码的复杂得让我想去重拾汇编语言最糟糕的是您要对所编写的每一个 POJO 重复这样的代码序列清单 中的半数工作都是在 POJO 本身中完成的因此无法被重用 — 除非是通过传统的 复制粘贴 编程方法

现在让我们来看看 Scala 提供什么样内容来实现更好的变通方法

Scala 中的特征和行为重用

Scala 使您能够定义处于接口和类之间的新型结构称为特征(trait)特征很奇特因为一个类可以按照需要整合许多特征这与接口相似但它们还可包含行为这又与类相似同样与类和接口类似特征可以引入新方法但与类和接口不同之处在于在特征作为类的一部分整合之前不会检查行为的定义或者换句话说您可以定义出这样的方法在整合到使用特征的类定义之前不会检查其正确性

特征听起来十分复杂但一个实例就可以非常轻松地理解它们首先下面是在 Scala 中重定义的 Person POJO

清单 Scala 的 Person POJO

//This is Scala

class Person(var firstName:String var lastName:String var age:Int)

{

}

您还可以确认 Scala POJO 具备基于 Java POJO 的环境中需要的 get()/set() 方法只需在类参数 firstNamelastName 和 age 上使用 scalareflectBeanProperty 注释即可现在为简单起见我们暂时不考虑这些方法

如果 Person 类需要能够接收 PropertyChangeListener可以使用如清单 所示的方式来完成此任务

清单 Scala 的 Person POJO 与监听器

//This is Scala

object PCL

extends javabeansPropertyChangeListener

{

override def propertyChange(pce:javabeansPropertyChangeEvent):Unit =

{

Systemoutprintln(Bean changed its + pcegetPropertyName() +

from + pcegetOldValue() +

to + pcegetNewValue())

}

}

object App

{

def main(args:Array[String]):Unit =

{

val p = new Person(Jennifer Aloi )

paddPropertyChangeListener(PCL)

psetFirstName(Jenni)

psetAge()

Systemoutprintln(p)

}

}

注意如何使用清单 中的 object 实现将静态方法注册为监听器 — 而在 Java 代码中除非显式创建并实例化 Singleton 类否则永远无法实现这进一步证明了一个理论Scala 从 Java 开发的历史 痛苦 中吸取了教训

Person 的下一步是提供 addPropertyChangeListener() 方法并在属性更改时对各监听器触发 propertyChange() 方法调用在 Scala 中以可重用的方式完成此任务与定义和使用特征一样简单如清单 所示我将此特征称为 BoundPropertyBean因为在 JavaBeans 规范中已通知 的属性称为绑定属性

清单 神圣的行为重用!

//This is Scala

trait BoundPropertyBean

{

import javabeans_

val pcs = new PropertyChangeSupport(this)

def addPropertyChangeListener(pcl : PropertyChangeListener) =

pcsaddPropertyChangeListener(pcl)

def removePropertyChangeListener(pcl : PropertyChangeListener) =

pcsremovePropertyChangeListener(pcl)

def firePropertyChange(name : String oldVal : _ newVal : _) : Unit =

pcsfirePropertyChange(new PropertyChangeEvent(this name oldVal newVal))

}

同样我依然要使用 javabeans 包的 PropertyChangeSupport 类不仅因为它提供了约 % 的实现细节还因为我所具备的行为与直接使用它的 JavaBean/POJO 相同Support 类的其他任何增强都将传播到我的特征不同之处在于 Person POJO 不需要再直接使用 PropertyChangeSupport如清单 所示

清单 Scala 的 Person POJO 种形式

//This is Scala

class Person(var firstName:String var lastName:String var age:Int)

extends Object

with BoundPropertyBean

{

override def toString = [Person: firstName= + firstName +

lastName= + lastName + age= + age + ]

}

在编译后简单查看 Person 定义即可发现它有公共方法 addPropertyChangeListener()removePropertyChangeListener() 和 firePropertyChange()就像 Java 版本的 Person 一样实际上Scala 的 Person 版本仅通过一行附加的代码即获得了这些新方法类声明中的 with 子句将 Person 类标记为继承 BoundPropertyBean 特征

遗憾的是我还没有完全实现Person 类现在支持接收移除和通知监听器但 Scala 为 firstName 成员生成的默认方法并没有利用它们同样遗憾的是这样编写的 Scala 没有很好的注释以自动地 生成利用 PropertyChangeSupport 实例的 get/set 方法因此我必须自行编写如清单 所示

清单 Scala 的 Person POJO 种形式

//This is Scalaclass Person(var firstName:String var lastName:String var age:Int)    extends Object    with BoundPropertyBean{    def setFirstName(newvalue:String) =    {        val oldvalue = firstName        firstName = newvalue        firePropertyChange(firstName oldvalue newvalue)    }    def setLastName(newvalue:String) =    {        val oldvalue = lastName        lastName = newvalue        firePropertyChange(lastName oldvalue newvalue)    }    def setAge(newvalue:Int) =    {        val oldvalue = age        age = newvalue        firePropertyChange(age oldvalue newvalue)    }    override def toString = [Person: firstName= + firstName +         lastName= + lastName + age= + age + ]}

应该具备的出色特征

特征不是一种函数编程 概念而是十多年来反思对象编程的结果实际上您很有可能正在简单的 Scala 程序中使用以下特征只是没有意识到而已

清单 再见糟糕的 main()!

//This is Scalaobject App extends Application{    val p = new Person(Jennifer Aloi )    paddPropertyChangeListener(PCL)        psetFirstName(Jenni)    psetAge()        Systemoutprintln(p)}

Application 特征定义了一直都是手动定义的 main() 的方法实际上它包含一个有用的小工具计时器如果系统属性 scalatime 传递给了 Application 实现代码它将为应用程序的执行计时如清单 所示

清单 时间就是一切

$ scala Dscalatime App

Bean changed its firstName from Jennifer to Jenni

Bean changed its age from to

[Person: firstName=Jenni lastName=Aloi age=]

[total ms]

JVM 中的特征

在这个时候有必要提出这样一个问题这种看似魔术的接口与方法结构(即 特征)是如何映射到 JVM 的在清单 我们的好朋友 javap 展示了魔术背后发生了什么

清单 Person 内幕

$ javap classpath C:\Prg\scalafinal\lib\scalalibraryjar;classes Person

Compiled from Personscala

public class Person extends javalangObject implements BoundPropertyBeanscala

ScalaObject{

public Person(javalangString javalangString int);

public javalangString toString();

public void setAge(int);

public void setLastName(javalangString);

public void setFirstName(javalangString);

public void age_$eq(int);

public int age();

public void lastName_$eq(javalangString);

public javalangString lastName();

public void firstName_$eq(javalangString);

public javalangString firstName();

public int $tag();

public void firePropertyChange(javalangString javalangObject javalang

Object);

public void removePropertyChangeListener(javabeansPropertyChangeListener);

public void addPropertyChangeListener(javabeansPropertyChangeListener);

public final void pcs_$eq(javabeansPropertyChangeSupport);

public final javabeansPropertyChangeSupport pcs();

}

请注意 Person 的类声明该 POJO 实现了一个名为 BoundPropertyBean 的接口这就是特征作为接口映射到 JVM 本身的方法但特征方法的实现又是什么样的呢?请记住编译器可以容纳所有技巧只要最终结果符合 Scala 语言的语义含义即可在这种情况下它会将特征中定义的方法实现和字段声明纳入实现特征的类 Person 中使用 private 运行 javap 会使这更加显着 — 如果 javap 输出的最后两行体现的还不够明显(引用特征中定义的 pcs 值)

清单 Person 内幕 种形式

$ javap private classpath C:\Prg\scalafinal\lib\scalalibraryjar;classes Person

Compiled from Personscala

public class Person extends javalangObject implements BoundPropertyBeanscala

ScalaObject{

private final javabeansPropertyChangeSupport pcs;

private int age;

private javalangString lastName;

private javalangString firstName;

public Person(javalangString javalangString int);

public javalangString toString();

public void setAge(int);

public void setLastName(javalangString);

public void setFirstName(javalangString);

public void age_$eq(int);

public int age();

public void lastName_$eq(javalangString);

public javalangString lastName();

public void firstName_$eq(javalangString);

public javalangString firstName();

public int $tag();

public void firePropertyChange(javalangString javalangObject javalangObject);

public void removePropertyChangeListener(javabeansPropertyChangeListener);

public void addPropertyChangeListener(javabeansPropertyChangeListener);

public final void pcs_$eq(javabeansPropertyChangeSupport);

public final javabeansPropertyChangeSupport pcs();

}

实际上这个解释也回答了为何可以推迟特征方法的执行直至用该检查的时候因为在类实现特征的方法之前它实际上并不是任何类的一 部分因此编译器可将方法的某些逻辑方面留到以后再处理这非常有用因为它允许特征在不了解实现特征的实际基类将是什么的情况下调用 super()

关于特征的备注

在 BoundPropertyBean 中我在 PropertyChangeSupport 实例的构建中使用了特征功能其构造方法需要属性得到通知的 bean在早先定义的特征中我传入了 this由于在 Person 上实现之前并不会真正定义特征this 将引用 Person 实例而不是 BoundPropertyBean 特征本身特征的这个具体方面 — 定义的推迟解析 — 非常微妙但对于此类的 迟绑定 来说可能非常强大

对于 Application 特征的情况有两部分很有魔力Application 特征的 main() 方法为 Java 应用程序提供普适入口点还会检查 Dscalatime 系统属性查看是否应该跟蹤执行时间但由于 Application 是一个特征方法实际上会在子类上出现(App)要执行此方法必须创建 App 单体也就是说构造 App 的一个实例处理 类的主体这将有效地执行应用程序只有在这种处理完成之后特征的 main() 才会被调用并显示执行所耗费的时间

虽然有些落后但它仍然有效尽管应用程序无权访问任何传入 main() 的命令行参数它还表明特征的行为如何 下放到 实现类

特征和集合

在将具体行为与抽象声明相结合以便为实现者提供便捷时特征非常强大例如考虑经典的 Java 集合接口/类 List 和 ArrayListList 接口保证此集合的内容能够按照插入时的次序被遍历用更正规的术语来说位置语义得到了保证

ArrayList 是 List 的具体类型在分配好的数组中存储内容而 LinkedList 使用的是链表实现ArrayList 更适合列表内容的随机访问而 LinkedList 更适合在除了列表末尾以外的位置进行插入和删除操作无论如何这两种类之间存在大量相同的行为它们继承了公共基类 AbstractList

如果 Java 编程支持特征它们应已成为出色的结构能够解决 可重用行为而无需诉诸于继承公共基类 之类的问题特征可以作为 C++ 私有继承 机制避免出现新 List 子类型是否应直接实现 List(还有可能忘记实现 RandomAccess 接口)或者扩展基类 AbstractList 的迷惑这有时在 C++ 中称为 混合与 Ruby 的混合(或后文中探讨的 Scala 混合)有所不同

在 Scala 文档集中经典的示例就是 Ordered 特征它定义了名字很有趣的方法以提供比较(以及排序)功能如清单 所示

清单 顺序顺序

//This is Scala

trait Ordered[A] {

def compare(that: A): Int

def < (that: A): Boolean = (this compare that) <

def > (that: A): Boolean = (this compare that) >

def <= (that: A): Boolean = (this compare that) <=

def >= (that: A): Boolean = (this compare that) >=

def compareTo(that: A): Int = compare(that)

}

在这里Ordered 特征(具有参数化类型采用 Java 泛型方式)定义了一个抽象方法 compare它应获得一个 A 作为参数并需要在 小于 的情况下返回小于 的值大于 的情况下返回大于 的值在相等的情况下返回 然后它继续使用 compare() 方法和更加熟悉的 compareTo() 方法(javautilComparable 接口也使用该方法)定义关系运算符(< 和 > 等)

Scala 和 Java 兼容性

实际上伪实现继承并不是 Scala 内特征的最常见应用或最强大用法与此不同特征在 Scala 内作为 Java 接口的基本替代项希望使用 Scala 的 Java 程序员也应熟悉特征将其作为使用 Scala 的一种机制

我在本系列的文章中一直强调编译后的 Scala 代码并非总是能够保证 Java 语言的特色例如回忆一下Scala 的 名字很有趣的方法(例如 +\这些方法往往会使用 Java 语言语法中不直接可用的字符编码($ 就是一个需要考虑的严重问题)出于这方面的原因创建 Java 可调用 的接口往往要求深入研究 Scala 代码

这个特殊示例有些憋足Scala 主义者 通常并不需要特征提供的间接层(假设我并未使用 名字很有趣的方法但概念在这里十分重要在清单 我希望获得一个传统的 Java 风格工厂生成 Student 实例就像您经常在各种 Java 对象模型中可以看到的那样最初我需要一个兼容 Java 的接口接合到 Student

清单 学生

//This is Scala

trait Student

{

def getFirstName : String;

def getLastName : String;

def setFirstName(fn : String) : Unit;

def setLastName(fn : String) : Unit;

def teach(subject : String)

}

在编译时它会转换成 POJIPlain Old Java Interface查看 javap 会看到这样的内容

清单 这是一个 POJI!

$ javap Student

Compiled from Studentscala

public interface Student extends scalaScalaObject{

public abstract void setLastName(javalangString);

public abstract void setFirstName(javalangString);

public abstract javalangString getLastName();

public abstract javalangString getFirstName();

public abstract void teach(javalangString);

}

接下来我需要一个类成为工厂本身通常在 Java 代码中这应该是类上的一个静态方法(名称类似于 StudentFactory但回忆一下Scala 并没有此类的实例方法我认为这就是我在这里希望得到的结论因此我创建了一个 StudentFactory 对象将我的 Factory 方法放在那里

清单 我构造 Students

//This is Java

object StudentFactory

{

class StudentImpl(var first:String var last:String var subject:String)

extends Student

{

def getFirstName : String = first

def setFirstName(fn: String) : Unit = first = fn

def getLastName : String = last

def setLastName(ln: String) : Unit = last = ln

def teach(subject : String) =

Systemoutprintln(I know + subject)

}

def getStudent(firstName: String lastName: String) : Student =

{

new StudentImpl(firstName lastName Scala)

}

}

嵌套类 StudentImpl 是 Student 特征的实现因而提供了必需的 get()/set() 方法对切记尽管特征可以具有行为但它根据 JVM 作为接口建模这一事实意味着尝试实例化特征将产生错误 —— 表明 Student 是抽象的

当然这个简单示例的目的在于编写出一个 Java 应用程序使之可以利用这些由 Scala 创建的新对象

清单 学生 Neo

//This is Java

public class App

{

public static void main(String[] args)

{

Student s = StudentFactorygetStudent(Neo Anderson);

steach(Kung fu);

}

}

运行此代码您将看到I know Kung fu(我知道我们经过了漫长的设置过程只是得到了一部廉价电影的推介)

结束语

特征提供了在 Scala 中分类和定义的强大机制目的在于定义一种接口供客户端使用按照 传统 Java 接口的形式定义同时提供一种机制根据特征内定义的其他行为来继承行为或许我们需要的是一种全新的继承术语用于 描述特征和实现类之间的关系

除了本文所述内容之外还有很多种方法可以使用特征但本系列文章的部分目的就在于提供关于这种语言的足够信息鼓励您在家中进一步开展实验下载 Scala 实现亲自试用查看 Scala 可插入当前 Java 系统的什么位置此外如果您发现 Scala 非常有用

               

上一篇:JAVA凸包算法

下一篇:Java操作文本封装类