著名科学家、研究学者艾萨克.牛顿爵士有这样一句名言:“如果说我看得比别人远一些,那是因为我站在巨人的肩膀上”。作为一名热心的历史和政治学家,我想对这位伟人的名言略加修改:“如果说我看得比别人远一些,那是因为我站在历史的肩膀上”。而这句话又体现出另一位历史学家 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 接口的形式定义;同时提供一种机制,根据特征内定义的其他行为来继承行为。或许我们需要的是一种全新的继承术语,用于 描述特征和实现类之间的关系。
发表评论
-
Scala + Twitter = Scitter(scala代码学习第15天)
2011-04-08 09:11 865Twitter 迅速占领了 Interne ... -
面向 Java 开发人员的 Scala 指南: Scala 和 servlet(scala代码学习第十一天)
2011-04-02 07:40 732Scala 显然是一门有趣的语言,很适合体现语言理论和创新方面 ... -
构建计算器,第 3 部分将 Scala 解析器组合子和 case 类结合起来(scala代码学习第十天)
2011-04-01 09:25 949欢迎勇于探索的读者回到我们的系列文章中!本月继续探索 Scal ... -
scala代码学习构建计算器,第2 部分(代码学习第九天)
2011-03-31 10:53 807回忆一下我们的英雄所处的困境:在试图创建一个 DSL(这里只不 ... -
Scala构建计算器,第1 部分(代码学习第8天)
2011-03-30 11:59 1192特定于领域的语言 可能您无法(或没有时间)承受来自于您的项目 ... -
scala包和访问修饰符(代码学习第七天)
2011-03-29 15:51 1615系列的过程中我遗漏了 ... -
实现继承(代码学习第五天)
2011-03-26 10:13 963近十几年来,面向对象语言设计的要素一直是继承的核心。不支持继承 ... -
Scala 控制结构内部揭密(scala代码学习第三天)
2011-03-24 09:15 1304迄今为止,在此 系列 ... -
面向 Java 开发人员的 Scala 指南: 类操作(代码学习第2天)
2011-03-22 19:06 740第一天中只是些简单应用 ,您只是稍微了解了一些 Scala 语 ... -
programming in scala 2nd代码学习(第一天)
2011-03-22 18:42 932近来没事,拿出了原先学习scala的代码 书中代码噢、拿出自己 ... -
scalatra web框架快速搭建(官方使用文档)
2011-03-21 22:42 2513昨天写了个sbt构建scala项目的文章,就是为了今天的sca ... -
A build tool for Scala(simple-build-tool) sbt安装指南
2011-03-20 22:49 2200今天有位写框架的大哥叫我学一学scalatra框架,找了 ... -
Scala functional style deferent from java OOP(特点)
2011-03-20 17:34 981该程序通过一段斐波那契数列的计算,比较一下Scala的函数式编 ... -
Java 开发人员的 Scala 指南: 面向对象的函数编程
2011-03-20 11:59 1034函数概念 开始之前, ...
相关推荐
1. **数据预处理**:这是深度学习的第一步,包括数据清洗(去除缺失值或异常值)、数据转换(如归一化或标准化)和数据集划分(训练集、验证集和测试集)。 2. **特征工程**:基于业务理解和数据洞察,可能需要创建...
开发者则需要编写代码来检测和防止这种注入行为,防止特征码被篡改。 3. **逆向工程**:逆向工程是分析已编译代码以了解其内部工作原理的过程。作弊者可能使用逆向工程技术来找到特征码的位置,而开发者则需要对抗...
4. **社交网络特征**:分析用户在社交网络上的关系和行为,如好友网络、互动频率等。欺诈者往往试图通过建立虚假联系来掩盖真实身份。 5. **历史记录特征**:用户的历史信用记录、投诉记录、违约记录等,这些可以...
在IT领域,特别是数据分析与机器学习方向,"weiziman数据库上行为识别代码实现"是一个典型的项目案例,它涉及到对特定行为的识别和分析。Weiziman数据库通常包含丰富的动作样本,这些样本可能是由人体动作捕捉系统...
描述中的"第4章"可能涵盖了如何创建和实例化类,以及如何使用构造函数初始化对象。构造函数是一个特殊的方法,用于在创建对象时执行特定的初始化任务。 其次,对象是类的实例,是实际存在的数据结构。通过使用new...
【标题】"第一行代码Java源代码第4章课程代码面向对象高级知识"涉及的是Java编程语言中的面向对象高级概念,这些概念是Java开发者在深入学习时必须掌握的关键点。面向对象编程(Object-Oriented Programming,OOP)...
特征码分析是免杀的第一步,需确定哪些部分的代码是杀毒软件识别的关键。这通常需要借助专门的工具,如CCL、MYCCL、multiCCL等,帮助定位特征码的具体位置。 ### 四、修改特征码的工具 修改特征码的工具多种多样,...
该压缩包文件“基于python深度学习的车辆特征分析系统源码数据库.zip”包含了使用Python编程语言和深度学习技术开发的车辆特征分析系统的源代码和可能的数据库文件。这个系统可能是用于车辆识别、车型分类或者车辆...
检测手段可能包括特征码检测、行为监测、启发式分析和沙箱技术等。取证分析则是在攻击发生后,对系统进行详细的检查和分析,以确定发生了什么、恶意代码是如何工作的、如何从系统中清除等。 恶意代码攻防学习是指导...
【标题】中的“第一届CCF比赛中的客户用电异常行为分析比赛的源代码”指的是一个编程竞赛,该竞赛可能由中国计算机学会(CCF)主办,旨在挑战参赛者对电力使用数据的分析能力,特别是识别异常行为。这类比赛通常要求...
《图像处理分析与机器视觉 第3版 源代码-第4部分》是关于计算机视觉领域的一个重要资源,尤其对于学习和研究图像处理与机器视觉的学者和工程师来说,具有极高的价值。这一部分的源代码包含在09ObjRec、08ShapeRepr和...
10. **数据结构**:深入理解列表、字典、集合等数据结构的性能特征和使用场景,对于编写高效代码很重要。 11. **单元测试和调试**:使用unittest或pytest库进行代码测试,确保代码质量和可靠性。 12. **Web开发**...
二元灰狼优化(Binarized Grey Wolf Optimizer, BGWO)是一种基于生物行为的优化算法,它模拟了灰狼群在捕猎过程中的协作和领导机制。在特征选择任务中,BGWO被广泛用于寻找最优特征子集,以提高模型的性能和减少...
"数学建模学习,数学建模学习,Matlab代码3"这个标题暗示了我们将会深入探讨如何使用Matlab进行数学建模的第三个阶段的学习,这可能包括更高级或特定的建模技术。 数学建模是一个将实际问题抽象成数学模型的过程,...
为了有效地对抗恶意代码,研究者们采用多种方法收集恶意软件样本,并通过分析这些样本,提取恶意代码的特征和行为模式。研究者们通常会使用杀毒软件公司发布的恶意代码公告、安全研究员的分析报告、相关博客文章等...
这个系统可能包含四个特定的特征和状态,用于更准确地识别语音的开始和结束。 首先,`stdafx.h`是预编译头文件,通常包含项目中频繁使用的、但改动较少的头文件,以提高编译效率。在这个项目中,它包含了对MFC库和...
《C#入门经典第四版随书代码3》是针对初学者设计的一套代码资源集合,旨在帮助读者通过实践深入理解C#编程语言的基础知识和高级特性。这些代码示例覆盖了多个章节,从基础语法到复杂的编程概念,为学习者提供了丰富...
4. **异常检测**:一旦有了特征,就可以使用统计学方法(如阈值、聚类、支持向量机)或者机器学习算法(如随机森林、深度学习)来区分正常行为与异常行为。 5. **行为识别**:对于已知的行为类别,可以训练分类器来...
【贵州省道路交通安全违法行为代码与处罚记分标准】是根据《贵州省道路交通安全条例》制定的一份详细规定,旨在规范贵州省内的交通行为,确保道路交通安全。该标准涵盖了多种违法行为及其对应的处罚措施,包括记分和...
学习Java的第一步通常是理解这些基本概念,比如声明变量、使用控制结构(如if语句和for循环)以及创建函数。 2. **类与对象**:Java是面向对象的,这意味着它基于类和对象的概念。类是对象的蓝图,定义了对象的属性...