著名科学家、研究学者艾萨克.牛顿爵士有这样一句名言:“如果说我看得比别人远一些,那是因为我站在巨人的肩膀上”。作为一名热心的历史和政治学家,我想对这位伟人的名言略加修改:“如果说我看得比别人远一些,那是因为我站在历史的肩膀上”。而这句话又体现出另一位历史学家 George Santayana 的名言:“忘记历史必将重蹈覆辙”。换句话说,如果我们不能回顾历史,从过去的错误(包括我们自己过去的经验)中吸取教训,就没有机会做出改进。
您可能会疑惑,这样的哲学与 Scala 有什么关系?继承就是我们要讨论的内容之一。考虑这样一个事实,Java 语言的创建已经是近 20 年前的事情,当时是 “面向对象” 的全盛时期。它设计用于模仿当时的主流语言 C++,尝试将使用这种语言的开发人员吸引到 Java 平台上来。毫无疑问,在当时看来,这样的决策是明智而且必要的,但回顾一下,就会发现其中有些地方并不像创建者设想的那样有益。
例如,在二十年前,对于 Java 语言的创建者来说,反映 C++ 风格的私有继承和多重继承是必要的。自那之后,许多 Java 开发人开始为这些决策而后悔。在这一期的 Scala 指南中,我回顾了 Java 语言中多重继承和私有继承的历史。随后,您将看到 Scala 是怎样改写了历史,为所有人带来更大收益。
C++ 和 Java 语言中的继承
历史是人们愿意记录下来的事实。
—拿破仑.波拿巴
从事 C++ 工作的人们能够回忆起,私有继承是从基类中获取行为的一种方法,不必显式地接受 IS-A 关系。将基类标记为 “私有” 允许派生类从该基类继承而来,而无需实际成为 一个基类。但对自身的私有继承是未得到广泛应用的特性之一。继承一个基类而无法将它向下或向上转换到基类的理念是不明智的。
关于本系列Ted Neward 将和您一起深入探讨 Scala 编程语言。在这个新的 developerWorks 系列 中,您将深入了解 Sacla,并在实践中看到 Scala 的语言功能。进行比较时,Scala 代码和 Java 代码将放在一起展示,但(您将发现)Scala 中的许多内容与您在 Java 编程中发现的任何内容都没有直接关联,而这正是 Scala 的魅力所在!如果用 Java 代码就能够实现的话,又何必再学习 Scala 呢?
.另一方面,多重继承往往被视为面向对象编程的必备要素。在建模交通工具的层次结构时, SeaPlane 无疑需要继承 Boat(使用其 startEngine() 和 sail() 方法)以及 Plane(使用其 startEngine() 和 fly() 方法)。SeaPlane 既是 Boat,也是 Plane,难道不是吗?
无论如何,这是在 C++ 鼎盛时期的想法。在快速转向 Java 语言时,我们认为多重继承与私有继承一样存在缺陷。所有 Java 开发人员都会告诉您,SeaPlane 应该继承 Floatable 和 Flyable 接口(或许还包括 EnginePowered 接口或基类)。继承接口意味着能够实现该类需要的所有方法,而不会遇到 虚拟多重继承 的难题(遇到这种难题时,要弄清楚在调用 SeaPlane 的 startEngine() 方法时应调用哪个基类的 startEngine())。
遗憾的是,彻底放弃私有继承和多重继承会使我们在代码重用方面付出昂贵的代价。Java 开发人员可能会因从虚拟多重继承中解放出来而高兴,但代价是程序员往往要完成辛苦而易于出错的工作。
--------------------------------------------------------------------------------
回页首
回顾可重用行为
事情大致可以分为可能永远不会发生的和不重要的。
—William Ralph Inge
JavaBeans 规范是 Java 平台的基础,它带来了众多 Java 生态系统作为依据的 POJO。我们都明白一点,Java 代码中的属性由 get()/set() 对管理,如清单 1 所示:
清单 1. Person POJO
//This is Java
这些代码看起来非常简单,编写起来也不难。但如果您希望提供通知支持 — 使第三方能够使用 POJO 注册并在变更属性时接收回调,事情会怎样?根据 JavaBeans 规范,必须实现 PropertyChangeListener 接口以及它的一个方法 propertyChange()。如果您希望允许任何 POJO 的 PropertyChangeListener 都能够对属性更改 “投票”,那么 POJO 就需要实现 VetoableChangeListener 接口,该接口的实现又依赖于 vetoableChange() 方法的实现。
至少,事情应该是这样运作的。
实际上,希望成为属性变更通知接收者的用户必须实现 PropertyChangeListener 接口,发送者(本例中的 Person 类)必须提供接收该接口实例的公共方法和监听器需要监听的属性名称。最终得到更加复杂的 Person,如清单 2 所示:
清单 2. Person POJO,第 2 种形式
//This is Java
保持引用属性变更监听器意味着 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,如下所示:
清单 3. Person POJO,第 3 种形式
//This is Java
不知道您有何感想,但这段代码的复杂得让我想去重拾汇编语言。最糟糕的是,您要对所编写的每一个 POJO 重复这样的代码序列。清单 3 中的半数工作都是在 POJO 本身中完成的,因此无法被重用 — 除非是通过传统的 “复制粘贴” 编程方法。
现在,让我们来看看 Scala 提供什么样内容来实现更好的变通方法。
--------------------------------------------------------------------------------
回页首
Scala 中的特征和行为重用
所有人都有义务考虑自己的性格特征。必须合理控制这些特征,而不去质疑他人的性格特征是否更适合自己。
—西塞罗
Scala 使您能够定义处于接口和类之间的新型结构,称为特征(trait)。特征很奇特,因为一个类可以按照需要整合许多特征,这与接口相似,但它们还可包含行为,这又与类相似。同样,与类和接口类似,特征可以引入新方法。但与类和接口不同之处在于,在特征作为类的一部分整合之前,不会检查行为的定义。或者换句话说,您可以定义出这样的方法,在整合到使用特征的类定义之前,不会检查其正确性。
特征听起来十分复杂,但一个实例就可以非常轻松地理解它们。首先,下面是在 Scala 中重定义的 Person POJO:
清单 4. Scala 的 Person POJO
//This is Scala
您还可以确认 Scala POJO 具备基于 Java POJO 的环境中需要的 get()/set() 方法,只需在类参数 firstName、lastName 和 age 上使用 scala.reflect.BeanProperty 注释即可。现在,为简单起见,我们暂时不考虑这些方法。
如果 Person 类需要能够接收 PropertyChangeListener,可以使用如清单 5 所示的方式来完成此任务:
清单 5. Scala 的 Person POJO 与监听器
//This is Scala
注意,如何使用清单 5 中的 object 实现将静态方法注册为监听器 — 而在 Java 代码中,除非显式创建并实例化 Singleton 类,否则永远无法实现。这进一步证明了一个理论:Scala 从 Java 开发的历史 痛苦 中吸取了教训。
Person 的下一步是提供 addPropertyChangeListener() 方法,并在属性更改时对各监听器触发 propertyChange() 方法调用。在 Scala 中,以可重用的方式完成此任务与定义和使用特征一样简单,如清单 6 所示。我将此特征称为 BoundPropertyBean,因为在 JavaBeans 规范中,“已通知” 的属性称为绑定属性。
清单 6. 神圣的行为重用!
//This is Scala
同样,我依然要使用 java.beans 包的 PropertyChangeSupport 类,不仅因为它提供了约 60% 的实现细节,还因为我所具备的行为与直接使用它的 JavaBean/POJO 相同。对 “Support” 类的其他任何增强都将传播到我的特征。不同之处在于 Person POJO 不需要再直接使用 PropertyChangeSupport,如清单 7 所示:
清单 7. Scala 的 Person POJO,第 2 种形式
//This is Scala
在编译后,简单查看 Person 定义即可发现它有公共方法 addPropertyChangeListener()、removePropertyChangeListener() 和 firePropertyChange(),就像 Java 版本的 Person 一样。实际上,Scala 的 Person 版本仅通过一行附加的代码即获得了这些新方法:类声明中的 with 子句将 Person 类标记为继承 BoundPropertyBean 特征。
遗憾的是,我还没有完全实现;Person 类现在支持接收、移除和通知监听器,但 Scala 为 firstName 成员生成的默认方法并没有利用它们。同样遗憾的是,这样编写的 Scala 没有很好的注释以自动地 生成利用 PropertyChangeSupport 实例的 get/set 方法,因此我必须自行编写,如清单 8 所示:
清单 8. Scala 的 Person POJO,第 3 种形式
//This is Scala
应该具备的出色特征
特征不是一种函数编程 概念,而是十多年来反思对象编程的结果。实际上,您很有可能正在简单的 Scala 程序中使用以下特征,只是没有意识到而已:
清单 9. 再见,糟糕的 main()!
//This is Scala
Application 特征定义了一直都是手动定义的 main() 的方法。实际上,它包含一个有用的小工具:计时器,如果系统属性 scala.time 传递给了 Application 实现代码,它将为应用程序的执行计时,如清单 10 所示:
清单 10. 时间就是一切
--------------------------------------------------------------------------------
回页首
JVM 中的特征
任何足够高级的技术都近乎魔术。
— Arthur C Clarke
在这个时候,有必要提出这样一个问题,这种看似魔术的接口与方法结构(即 特征)是如何映射到 JVM 的。在清单 11 中,我们的好朋友 javap 展示了魔术背后发生了什么:
清单 11. Person 内幕
请注意 Person 的类声明。该 POJO 实现了一个名为 BoundPropertyBean 的接口,这就是特征作为接口映射到 JVM 本身的方法。但特征方法的实现又是什么样的呢?请记住,编译器可以容纳所有技巧,只要最终结果符合 Scala 语言的语义含义即可。在这种情况下,它会将特征中定义的方法实现和字段声明纳入实现特征的类 Person 中。使用 -private 运行 javap 会使这更加显著 — 如果 javap 输出的最后两行体现的还不够明显(引用特征中定义的 pcs 值):
清单 12. Person 内幕,第 2 种形式
实际上,这个解释也回答了为何可以推迟特征方法的执行,直至用该检查的时候。因为在类实现特征的方法之前,它实际上并不是任何类的一 “部分”,因此编译器可将方法的某些逻辑方面留到以后再处理。这非常有用,因为它允许特征在不了解实现特征的实际基类将是什么的情况下调用 super()。
关于特征的备注
在 BoundPropertyBean 中,我在 PropertyChangeSupport 实例的构建中使用了特征功能。其构造方法需要属性得到通知的 bean,在早先定义的特征中,我传入了 “this”。由于在 Person 上实现之前并不会真正定义特征,“this” 将引用 Person 实例,而不是 BoundPropertyBean 特征本身。特征的这个具体方面 — 定义的推迟解析 — 非常微妙,但对于此类的 “迟绑定” 来说可能非常强大。
对于 Application 特征的情况,有两部分很有魔力;Application 特征的 main() 方法为 Java 应用程序提供普适入口点,还会检查 -Dscala.time 系统属性,查看是否应该跟踪执行时间。但由于 Application 是一个特征,方法实际上会在子类上出现(App)。要执行此方法,必须创建 App 单体,也就是说构造 App 的一个实例,“处理” 类的主体,这将有效地执行应用程序。只有在这种处理完成之后,特征的 main() 才会被调用并显示执行所耗费的时间。
虽然有些落后,但它仍然有效,尽管应用程序无权访问任何传入 main() 的命令行参数。它还表明特征的行为如何 “下放到” 实现类。
--------------------------------------------------------------------------------
回页首
特征和集合
不是解决方法的一部分,就注定被淘汰。
— Henry J Tillman
在将具体行为与抽象声明相结合以便为实现者提供便捷时,特征非常强大。例如,考虑经典的 Java 集合接口/类 List 和 ArrayList。List 接口保证此集合的内容能够按照插入时的次序被遍历,用更正规的术语来说,“位置语义得到了保证”。
ArrayList 是 List 的具体类型,在分配好的数组中存储内容,而 LinkedList 使用的是链表实现。ArrayList 更适合列表内容的随机访问,而 LinkedList 更适合在除了列表末尾以外的位置进行插入和删除操作。无论如何,这两种类之间存在大量相同的行为,它们继承了公共基类 AbstractList。
如果 Java 编程支持特征,它们应已成为出色的结构,能够解决 “可重用行为,而无需诉诸于继承公共基类” 之类的问题。特征可以作为 C++ “私有继承” 机制,避免出现新 List 子类型是否应直接实现 List(还有可能忘记实现 RandomAccess 接口)或者扩展基类 AbstractList 的迷惑。这有时在 C++ 中称为 “混合”,与 Ruby 的混合(或后文中探讨的 Scala 混合)有所不同。
在 Scala 文档集中,经典的示例就是 Ordered 特征,它定义了名字很有趣的方法,以提供比较(以及排序)功能,如清单 13 所示:
清单 13. 顺序、顺序
//This is Scala
在这里,Ordered 特征(具有参数化类型,采用 Java 5 泛型方式)定义了一个抽象方法 compare,它应获得一个 A 作为参数,并需要在 “小于” 的情况下返回小于 1 的值,在 “大于” 的情况下返回大于 1 的值,在相等的情况下返回 0。然后它继续使用 compare() 方法和更加熟悉的 compareTo() 方法(java.util.Comparable 接口也使用该方法)定义关系运算符(< 和 > 等)。
--------------------------------------------------------------------------------
回页首
Scala 和 Java 兼容性
一张图片胜过千言万语。一个界面胜过上千图片。
—Ben Shneiderman
实际上,伪实现继承并不是 Scala 内特征的最常见应用或最强大用法,与此不同,特征在 Scala 内作为 Java 接口的基本替代项。希望使用 Scala 的 Java 程序员也应熟悉特征,将其作为使用 Scala 的一种机制。
我在本系列的文章中一直强调,编译后的 Scala 代码并非总是能够保证 Java 语言的特色。例如,回忆一下,Scala 的 “名字很有趣的方法”(例如 “+” 或 “\”),这些方法往往会使用 Java 语言语法中不直接可用的字符编码(“$” 就是一个需要考虑的严重问题)。出于这方面的原因,创建 “Java 可调用” 的接口往往要求深入研究 Scala 代码。
这个特殊示例有些憋足,Scala 主义者 通常并不需要特征提供的间接层(假设我并未使用 “名字很有趣的方法”),但概念在这里十分重要。在清单 14 中,我希望获得一个传统的 Java 风格工厂,生成 Student 实例,就像您经常在各种 Java 对象模型中可以看到的那样。最初,我需要一个兼容 Java 的接口,接合到 Student:
清单 14. 我,学生
//This is Scala
在编译时,它会转换成 POJI:Plain Old Java Interface,查看 javap 会看到这样的内容:
清单 15. 这是一个 POJI!
接下来,我需要一个类成为工厂本身。通常,在 Java 代码中,这应该是类上的一个静态方法(名称类似于 “StudentFactory”),但回忆一下,Scala 并没有此类的实例方法。我认为这就是我在这里希望得到的结论,因此,我创建了一个 StudentFactory 对象,将我的 Factory 方法放在那里:
清单 16. 我构造 Students
//This is Java
嵌套类 StudentImpl 是 Student 特征的实现,因而提供了必需的 get()/set() 方法对。切记,尽管特征可以具有行为,但它根据 JVM 作为接口建模这一事实意味着尝试实例化特征将产生错误 —— 表明 Student 是抽象的。
当然,这个简单示例的目的在于编写出一个 Java 应用程序,使之可以利用这些由 Scala 创建的新对象:
清单 17. 学生 Neo
运行此代码,您将看到:“I know Kung fu”。(我知道,我们经过了漫长的设置过程,只是得到了一部廉价电影的推介)。
--------------------------------------------------------------------------------
回页首
结束语
人们不喜欢思考。思考总是要得出结论。而结论并非总是令人愉快。
— Helen Keller
特征提供了在 Scala 中分类和定义的强大机制,目的在于定义一种接口,供客户端使用,按照 传统 Java 接口的形式定义;同时提供一种机制,根据特征内定义的其他行为来继承行为。或许我们需要的是一种全新的继承术语,用于 描述特征和实现类之间的关系。
您可能会疑惑,这样的哲学与 Scala 有什么关系?继承就是我们要讨论的内容之一。考虑这样一个事实,Java 语言的创建已经是近 20 年前的事情,当时是 “面向对象” 的全盛时期。它设计用于模仿当时的主流语言 C++,尝试将使用这种语言的开发人员吸引到 Java 平台上来。毫无疑问,在当时看来,这样的决策是明智而且必要的,但回顾一下,就会发现其中有些地方并不像创建者设想的那样有益。
例如,在二十年前,对于 Java 语言的创建者来说,反映 C++ 风格的私有继承和多重继承是必要的。自那之后,许多 Java 开发人开始为这些决策而后悔。在这一期的 Scala 指南中,我回顾了 Java 语言中多重继承和私有继承的历史。随后,您将看到 Scala 是怎样改写了历史,为所有人带来更大收益。
C++ 和 Java 语言中的继承
历史是人们愿意记录下来的事实。
—拿破仑.波拿巴
从事 C++ 工作的人们能够回忆起,私有继承是从基类中获取行为的一种方法,不必显式地接受 IS-A 关系。将基类标记为 “私有” 允许派生类从该基类继承而来,而无需实际成为 一个基类。但对自身的私有继承是未得到广泛应用的特性之一。继承一个基类而无法将它向下或向上转换到基类的理念是不明智的。
关于本系列Ted Neward 将和您一起深入探讨 Scala 编程语言。在这个新的 developerWorks 系列 中,您将深入了解 Sacla,并在实践中看到 Scala 的语言功能。进行比较时,Scala 代码和 Java 代码将放在一起展示,但(您将发现)Scala 中的许多内容与您在 Java 编程中发现的任何内容都没有直接关联,而这正是 Scala 的魅力所在!如果用 Java 代码就能够实现的话,又何必再学习 Scala 呢?
.另一方面,多重继承往往被视为面向对象编程的必备要素。在建模交通工具的层次结构时, SeaPlane 无疑需要继承 Boat(使用其 startEngine() 和 sail() 方法)以及 Plane(使用其 startEngine() 和 fly() 方法)。SeaPlane 既是 Boat,也是 Plane,难道不是吗?
无论如何,这是在 C++ 鼎盛时期的想法。在快速转向 Java 语言时,我们认为多重继承与私有继承一样存在缺陷。所有 Java 开发人员都会告诉您,SeaPlane 应该继承 Floatable 和 Flyable 接口(或许还包括 EnginePowered 接口或基类)。继承接口意味着能够实现该类需要的所有方法,而不会遇到 虚拟多重继承 的难题(遇到这种难题时,要弄清楚在调用 SeaPlane 的 startEngine() 方法时应调用哪个基类的 startEngine())。
遗憾的是,彻底放弃私有继承和多重继承会使我们在代码重用方面付出昂贵的代价。Java 开发人员可能会因从虚拟多重继承中解放出来而高兴,但代价是程序员往往要完成辛苦而易于出错的工作。
--------------------------------------------------------------------------------
回页首
回顾可重用行为
事情大致可以分为可能永远不会发生的和不重要的。
—William Ralph Inge
JavaBeans 规范是 Java 平台的基础,它带来了众多 Java 生态系统作为依据的 POJO。我们都明白一点,Java 代码中的属性由 get()/set() 对管理,如清单 1 所示:
清单 1. 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,如清单 2 所示:
清单 2. Person POJO,第 2 种形式
//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;
// pcs.firePropertyChange("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,如下所示:
清单 3. Person POJO,第 3 种形式
//This is Java
import java.beans.*;
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;
propChgSupport.firePropertyChange("firstName", old, newValue);
}
public String getLastName() { return lastName; }
public void setLastName(String newValue)
{
String old = lastName;
lastName = newValue;
propChgSupport.firePropertyChange("lastName", old, newValue);
}
public int getAge() { return age; }
public void setAge(int newValue)
{
int old = age;
age = newValue;
propChgSupport.firePropertyChange("age", old, newValue);
}
public void addPropertyChangeListener(PropertyChangeListener pcl)
{
propChgSupport.addPropertyChangeListener(pcl);
}
public void removePropertyChangeListener(PropertyChangeListener pcl)
{
propChgSupport.removePropertyChangeListener(pcl);
}
}
不知道您有何感想,但这段代码的复杂得让我想去重拾汇编语言。最糟糕的是,您要对所编写的每一个 POJO 重复这样的代码序列。清单 3 中的半数工作都是在 POJO 本身中完成的,因此无法被重用 — 除非是通过传统的 “复制粘贴” 编程方法。
现在,让我们来看看 Scala 提供什么样内容来实现更好的变通方法。
--------------------------------------------------------------------------------
回页首
Scala 中的特征和行为重用
所有人都有义务考虑自己的性格特征。必须合理控制这些特征,而不去质疑他人的性格特征是否更适合自己。
—西塞罗
Scala 使您能够定义处于接口和类之间的新型结构,称为特征(trait)。特征很奇特,因为一个类可以按照需要整合许多特征,这与接口相似,但它们还可包含行为,这又与类相似。同样,与类和接口类似,特征可以引入新方法。但与类和接口不同之处在于,在特征作为类的一部分整合之前,不会检查行为的定义。或者换句话说,您可以定义出这样的方法,在整合到使用特征的类定义之前,不会检查其正确性。
特征听起来十分复杂,但一个实例就可以非常轻松地理解它们。首先,下面是在 Scala 中重定义的 Person POJO:
清单 4. Scala 的 Person POJO
//This is Scala
class Person(var firstName:String, var lastName:String, var age:Int)
{
}
您还可以确认 Scala POJO 具备基于 Java POJO 的环境中需要的 get()/set() 方法,只需在类参数 firstName、lastName 和 age 上使用 scala.reflect.BeanProperty 注释即可。现在,为简单起见,我们暂时不考虑这些方法。
如果 Person 类需要能够接收 PropertyChangeListener,可以使用如清单 5 所示的方式来完成此任务:
清单 5. Scala 的 Person POJO 与监听器
//This is Scala
object PCL
extends java.beans.PropertyChangeListener
{
override def propertyChange(pce:java.beans.PropertyChangeEvent):Unit =
{
System.out.println("Bean changed its " + pce.getPropertyName() +
" from " + pce.getOldValue() +
" to " + pce.getNewValue())
}
}
object App
{
def main(args:Array[String]):Unit =
{
val p = new Person("Jennifer", "Aloi", 28)
p.addPropertyChangeListener(PCL)
p.setFirstName("Jenni")
p.setAge(29)
System.out.println(p)
}
}
注意,如何使用清单 5 中的 object 实现将静态方法注册为监听器 — 而在 Java 代码中,除非显式创建并实例化 Singleton 类,否则永远无法实现。这进一步证明了一个理论:Scala 从 Java 开发的历史 痛苦 中吸取了教训。
Person 的下一步是提供 addPropertyChangeListener() 方法,并在属性更改时对各监听器触发 propertyChange() 方法调用。在 Scala 中,以可重用的方式完成此任务与定义和使用特征一样简单,如清单 6 所示。我将此特征称为 BoundPropertyBean,因为在 JavaBeans 规范中,“已通知” 的属性称为绑定属性。
清单 6. 神圣的行为重用!
//This is Scala
trait BoundPropertyBean
{
import java.beans._
val pcs = new PropertyChangeSupport(this)
def addPropertyChangeListener(pcl : PropertyChangeListener) =
pcs.addPropertyChangeListener(pcl)
def removePropertyChangeListener(pcl : PropertyChangeListener) =
pcs.removePropertyChangeListener(pcl)
def firePropertyChange(name : String, oldVal : _, newVal : _) : Unit =
pcs.firePropertyChange(new PropertyChangeEvent(this, name, oldVal, newVal))
}
同样,我依然要使用 java.beans 包的 PropertyChangeSupport 类,不仅因为它提供了约 60% 的实现细节,还因为我所具备的行为与直接使用它的 JavaBean/POJO 相同。对 “Support” 类的其他任何增强都将传播到我的特征。不同之处在于 Person POJO 不需要再直接使用 PropertyChangeSupport,如清单 7 所示:
清单 7. Scala 的 Person POJO,第 2 种形式
//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 方法,因此我必须自行编写,如清单 8 所示:
清单 8. Scala 的 Person POJO,第 3 种形式
//This is Scala
class 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 程序中使用以下特征,只是没有意识到而已:
清单 9. 再见,糟糕的 main()!
//This is Scala
object App extends Application
{
val p = new Person("Jennifer", "Aloi", 29)
p.addPropertyChangeListener(PCL)
p.setFirstName("Jenni")
p.setAge(30)
System.out.println(p)
}
Application 特征定义了一直都是手动定义的 main() 的方法。实际上,它包含一个有用的小工具:计时器,如果系统属性 scala.time 传递给了 Application 实现代码,它将为应用程序的执行计时,如清单 10 所示:
清单 10. 时间就是一切
$ scala -Dscala.time App
Bean changed its firstName from Jennifer to Jenni
Bean changed its age from 29 to 30
[Person: firstName=Jenni lastName=Aloi age=30]
[total 15ms]
--------------------------------------------------------------------------------
回页首
JVM 中的特征
任何足够高级的技术都近乎魔术。
— Arthur C Clarke
在这个时候,有必要提出这样一个问题,这种看似魔术的接口与方法结构(即 特征)是如何映射到 JVM 的。在清单 11 中,我们的好朋友 javap 展示了魔术背后发生了什么:
清单 11. Person 内幕
$ javap -classpath C:\Prg\scala-2.7.0-final\lib\scala-library.jar;classes Person
Compiled from "Person.scala"
public class Person extends java.lang.Object implements BoundPropertyBean,scala.
ScalaObject{
public Person(java.lang.String, java.lang.String, int);
public java.lang.String toString();
public void setAge(int);
public void setLastName(java.lang.String);
public void setFirstName(java.lang.String);
public void age_$eq(int);
public int age();
public void lastName_$eq(java.lang.String);
public java.lang.String lastName();
public void firstName_$eq(java.lang.String);
public java.lang.String firstName();
public int $tag();
public void firePropertyChange(java.lang.String, java.lang.Object, java.lang
.Object);
public void removePropertyChangeListener(java.beans.PropertyChangeListener);
public void addPropertyChangeListener(java.beans.PropertyChangeListener);
public final void pcs_$eq(java.beans.PropertyChangeSupport);
public final java.beans.PropertyChangeSupport pcs();
}
请注意 Person 的类声明。该 POJO 实现了一个名为 BoundPropertyBean 的接口,这就是特征作为接口映射到 JVM 本身的方法。但特征方法的实现又是什么样的呢?请记住,编译器可以容纳所有技巧,只要最终结果符合 Scala 语言的语义含义即可。在这种情况下,它会将特征中定义的方法实现和字段声明纳入实现特征的类 Person 中。使用 -private 运行 javap 会使这更加显著 — 如果 javap 输出的最后两行体现的还不够明显(引用特征中定义的 pcs 值):
清单 12. Person 内幕,第 2 种形式
$ javap -private -classpath C:\Prg\scala-2.7.0-final\lib\scala-library.jar;classes Person
Compiled from "Person.scala"
public class Person extends java.lang.Object implements BoundPropertyBean,scala.
ScalaObject{
private final java.beans.PropertyChangeSupport pcs;
private int age;
private java.lang.String lastName;
private java.lang.String firstName;
public Person(java.lang.String, java.lang.String, int);
public java.lang.String toString();
public void setAge(int);
public void setLastName(java.lang.String);
public void setFirstName(java.lang.String);
public void age_$eq(int);
public int age();
public void lastName_$eq(java.lang.String);
public java.lang.String lastName();
public void firstName_$eq(java.lang.String);
public java.lang.String firstName();
public int $tag();
public void firePropertyChange(java.lang.String, java.lang.Object, java.lang.Object);
public void removePropertyChangeListener(java.beans.PropertyChangeListener);
public void addPropertyChangeListener(java.beans.PropertyChangeListener);
public final void pcs_$eq(java.beans.PropertyChangeSupport);
public final java.beans.PropertyChangeSupport pcs();
}
实际上,这个解释也回答了为何可以推迟特征方法的执行,直至用该检查的时候。因为在类实现特征的方法之前,它实际上并不是任何类的一 “部分”,因此编译器可将方法的某些逻辑方面留到以后再处理。这非常有用,因为它允许特征在不了解实现特征的实际基类将是什么的情况下调用 super()。
关于特征的备注
在 BoundPropertyBean 中,我在 PropertyChangeSupport 实例的构建中使用了特征功能。其构造方法需要属性得到通知的 bean,在早先定义的特征中,我传入了 “this”。由于在 Person 上实现之前并不会真正定义特征,“this” 将引用 Person 实例,而不是 BoundPropertyBean 特征本身。特征的这个具体方面 — 定义的推迟解析 — 非常微妙,但对于此类的 “迟绑定” 来说可能非常强大。
对于 Application 特征的情况,有两部分很有魔力;Application 特征的 main() 方法为 Java 应用程序提供普适入口点,还会检查 -Dscala.time 系统属性,查看是否应该跟踪执行时间。但由于 Application 是一个特征,方法实际上会在子类上出现(App)。要执行此方法,必须创建 App 单体,也就是说构造 App 的一个实例,“处理” 类的主体,这将有效地执行应用程序。只有在这种处理完成之后,特征的 main() 才会被调用并显示执行所耗费的时间。
虽然有些落后,但它仍然有效,尽管应用程序无权访问任何传入 main() 的命令行参数。它还表明特征的行为如何 “下放到” 实现类。
--------------------------------------------------------------------------------
回页首
特征和集合
不是解决方法的一部分,就注定被淘汰。
— Henry J Tillman
在将具体行为与抽象声明相结合以便为实现者提供便捷时,特征非常强大。例如,考虑经典的 Java 集合接口/类 List 和 ArrayList。List 接口保证此集合的内容能够按照插入时的次序被遍历,用更正规的术语来说,“位置语义得到了保证”。
ArrayList 是 List 的具体类型,在分配好的数组中存储内容,而 LinkedList 使用的是链表实现。ArrayList 更适合列表内容的随机访问,而 LinkedList 更适合在除了列表末尾以外的位置进行插入和删除操作。无论如何,这两种类之间存在大量相同的行为,它们继承了公共基类 AbstractList。
如果 Java 编程支持特征,它们应已成为出色的结构,能够解决 “可重用行为,而无需诉诸于继承公共基类” 之类的问题。特征可以作为 C++ “私有继承” 机制,避免出现新 List 子类型是否应直接实现 List(还有可能忘记实现 RandomAccess 接口)或者扩展基类 AbstractList 的迷惑。这有时在 C++ 中称为 “混合”,与 Ruby 的混合(或后文中探讨的 Scala 混合)有所不同。
在 Scala 文档集中,经典的示例就是 Ordered 特征,它定义了名字很有趣的方法,以提供比较(以及排序)功能,如清单 13 所示:
清单 13. 顺序、顺序
//This is Scala
trait Ordered[A] {
def compare(that: A): Int
def < (that: A): Boolean = (this compare that) < 0
def > (that: A): Boolean = (this compare that) > 0
def <= (that: A): Boolean = (this compare that) <= 0
def >= (that: A): Boolean = (this compare that) >= 0
def compareTo(that: A): Int = compare(that)
}
在这里,Ordered 特征(具有参数化类型,采用 Java 5 泛型方式)定义了一个抽象方法 compare,它应获得一个 A 作为参数,并需要在 “小于” 的情况下返回小于 1 的值,在 “大于” 的情况下返回大于 1 的值,在相等的情况下返回 0。然后它继续使用 compare() 方法和更加熟悉的 compareTo() 方法(java.util.Comparable 接口也使用该方法)定义关系运算符(< 和 > 等)。
--------------------------------------------------------------------------------
回页首
Scala 和 Java 兼容性
一张图片胜过千言万语。一个界面胜过上千图片。
—Ben Shneiderman
实际上,伪实现继承并不是 Scala 内特征的最常见应用或最强大用法,与此不同,特征在 Scala 内作为 Java 接口的基本替代项。希望使用 Scala 的 Java 程序员也应熟悉特征,将其作为使用 Scala 的一种机制。
我在本系列的文章中一直强调,编译后的 Scala 代码并非总是能够保证 Java 语言的特色。例如,回忆一下,Scala 的 “名字很有趣的方法”(例如 “+” 或 “\”),这些方法往往会使用 Java 语言语法中不直接可用的字符编码(“$” 就是一个需要考虑的严重问题)。出于这方面的原因,创建 “Java 可调用” 的接口往往要求深入研究 Scala 代码。
这个特殊示例有些憋足,Scala 主义者 通常并不需要特征提供的间接层(假设我并未使用 “名字很有趣的方法”),但概念在这里十分重要。在清单 14 中,我希望获得一个传统的 Java 风格工厂,生成 Student 实例,就像您经常在各种 Java 对象模型中可以看到的那样。最初,我需要一个兼容 Java 的接口,接合到 Student:
清单 14. 我,学生
//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)
}
在编译时,它会转换成 POJI:Plain Old Java Interface,查看 javap 会看到这样的内容:
清单 15. 这是一个 POJI!
$ javap Student
Compiled from "Student.scala"
public interface Student extends scala.ScalaObject{
public abstract void setLastName(java.lang.String);
public abstract void setFirstName(java.lang.String);
public abstract java.lang.String getLastName();
public abstract java.lang.String getFirstName();
public abstract void teach(java.lang.String);
}
接下来,我需要一个类成为工厂本身。通常,在 Java 代码中,这应该是类上的一个静态方法(名称类似于 “StudentFactory”),但回忆一下,Scala 并没有此类的实例方法。我认为这就是我在这里希望得到的结论,因此,我创建了一个 StudentFactory 对象,将我的 Factory 方法放在那里:
清单 16. 我构造 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) =
System.out.println("I know " + subject)
}
def getStudent(firstName: String, lastName: String) : Student =
{
new StudentImpl(firstName, lastName, "Scala")
}
}
嵌套类 StudentImpl 是 Student 特征的实现,因而提供了必需的 get()/set() 方法对。切记,尽管特征可以具有行为,但它根据 JVM 作为接口建模这一事实意味着尝试实例化特征将产生错误 —— 表明 Student 是抽象的。
当然,这个简单示例的目的在于编写出一个 Java 应用程序,使之可以利用这些由 Scala 创建的新对象:
清单 17. 学生 Neo
//This is Java
public class App
{
public static void main(String[] args)
{
Student s = StudentFactory.getStudent("Neo", "Anderson");
s.teach("Kung fu");
}
}
运行此代码,您将看到:“I know Kung fu”。(我知道,我们经过了漫长的设置过程,只是得到了一部廉价电影的推介)。
--------------------------------------------------------------------------------
回页首
结束语
人们不喜欢思考。思考总是要得出结论。而结论并非总是令人愉快。
— Helen Keller
特征提供了在 Scala 中分类和定义的强大机制,目的在于定义一种接口,供客户端使用,按照 传统 Java 接口的形式定义;同时提供一种机制,根据特征内定义的其他行为来继承行为。或许我们需要的是一种全新的继承术语,用于 描述特征和实现类之间的关系。