近十几年来,面向对象语言设计的要素一直是继承的核心。不支持继承的语言(如 Visual Basic)被嘲讽是 “玩具语言” ,不适合真正的工作。与此同时,支持继承的语言所采用的支持方法五花八门,导致了许多争论。多重继承是否真的必不可少(就像 C++ 的创作者认定的那样),它是否不必要而丑陋的(就像 C# 和 Java 的创作者坚信的那样)?Ruby 和 Scala 是两种较新的语言,采取了多重继承的这种方法 — 正如我在上期介绍 Scala 的特征时所讨论的那样。
与所有 杰出的语言一样,Scala 也支持实现继承。在 Java 语言中,单一实现继承模型允许您扩展基类,添加新方法和字段等。尽管存在某些句法变更,Scala 的实现继承依然类似于 Java 语言中的实现。不同的是 Scala 融合了对象和函数语言设计,这非常值得我们在本期文章中进行讨论。
普通 Scala 对象
与本系列之前的文章类似,我将使用 Person 类作为起点,探索 Scala 的继承系统。清单 1 展示了 Person 的类定义:
清单 1. 嘿,我是人类
- // This is Scala
- class Person(val firstName:String, val lastName:String, val age:Int)
- {
- def toString = "[Person: firstName="+firstName+" lastName="+lastName+
- " age="+age+"]"
- }
Person 是一个非常简单的 POSO(普通 Scala 对象,Plain Old Scala Object),具有三个只读字段。您可能会想起,要使这些字段可以读写,只需将主构造函数声明中的 val 更改为 var 即可
无论如何,使用 Person 类型也非常简单,如清单 2 所示:
清单 2. PersonApp
- // This is Scala
- object PersonApp
- {
- def main(args : Array[String]) : Unit =
- {
- val bindi = new Person("Tabinda", "Khan", 38)
- System.out.println(bindi)
- }
- }
这算不上什么令人惊讶的代码,但给我们提供了一个起点。
Scala 中的抽象方法
随着该系统的发展,越来越明显地意识到 Person 类缺乏一个成为 Person 的重要部分,这个部分是做些事情 的行为。许多人都会根据我们在生活中的作为来定义自己,而不是根据现有和占用的空间。因此,我会添加一个新方法,如清单 3 所示,这赋予了 Person 一些意义:
清单 3. 很好,做些事情!
- // This is Scala
- class Person(val firstName:String, val lastName:String, val age:Int)
- {
- override def toString = "[Person: firstName="+firstName+" lastName="+lastName+
- " age="+age+"]"
- def doSomething = // uh.... what?
- }
这带来了一个问题:Person 的用途究竟是什么?有些 Person 绘画,有些唱歌,有些编写代码,有些玩视频游戏,有些什么也不做(问问十几岁青少年的父母)。因此,我会为 Person 创建 子类,而不是尝试去将这些活动直接整合到 Person 本身之中,如清单 4 所示:
清单 4. 这个人做的事情很少
- // This is Scala
- class Person(val firstName:String, val lastName:String, val age:Int)
- {
- override def toString = "[Person: firstName="+firstName+" lastName="+lastName+
- " age="+age+"]"
- def doSomething = // uh.... what?
- }
- class Student(firstName:String, lastName:String, age:Int)
- extends Person(firstName, lastName, age)
- {
- def doSomething =
- {
- System.out.println("I'm studying hard, Ma, I swear! (Pass the beer, guys!)")
- }
- }
当尝试编译代码时,我发现无法编译。这是因为 Person.doSomething 方法的定义无法工作;这个方法需要一个完整的主体(或许可抛出异常来表示它应在继承类中被覆盖),或者不需要主体,类似于 Java 代码中抽象方法的工作方式。我在清单 5 中尝试使用抽象的方法:
清单 5. 抽象类 Person
- // This is Scala
- abstract class Person(val firstName:String, val lastName:String, val age:Int)
- {
- override def toString = "[Person: firstName="+firstName+" lastName="+lastName+
- " age="+age+"]"
- def doSomething; // note the semicolon, which is still optional
- // but stylistically I like having it here
- }
- class Student(firstName:String, lastName:String, age:Int)
- extends Person(firstName, lastName, age)
- {
- def doSomething =
- {
- System.out.println("I'm studying hard, Ma, I swear! (Pass the beer, guys!)")
- }
- }
请注意,我如何使用 abstract 关键字装饰 Person 类。abstract 为编译器指出,是的,这个类应该是抽象的。在这方面,Scala 与 Java 语言没有区别。
#p#
对象,遇到函数
由于 Scala 融合了对象和函数语言风格,我实际上建模了 Person(如上所述),但并未创建子类型。这有些古怪,但强调了 Scala 对于这两种设计风格的整合,以及随之而来的有趣理念。
回忆 前几期文章,Scala 将函数作为值处理,就像处理语言中的其他值一样,例如 Int、Float 或 Double。在建模 Person 时,我可以利用这一点来获得 doSomething,不仅将其作为一种继承类中覆盖的方法,还将其作为可调用、替换、扩展的 函数值。清单 6 展示了这种方法:
清单 6. 努力工作的人
- // This is Scala
- class Person(val firstName:String, val lastName:String, val age:Int)
- {
- var doSomething : (Person) => Unit =
- (p:Person) => System.out.println("I'm " + p + " and I don't do anything yet!");
- def work() =
- doSomething(this)
- override def toString = "[Person: firstName="+firstName+" lastName="+lastName+
- " age="+age+"]"
- }
- object App
- {
- def main(args : Array[String]) =
- {
- val bindi = new Person("Tabinda", "Khan", 38)
- System.out.println(bindi)
- bindi.work()
- bindi.doSomething =
- (p:Person) => System.out.println("I edit textbooks")
- bindi.work()
- bindi.doSomething =
- (p:Person) => System.out.println("I write HTML books")
- bindi.work()
- }
- }
将函数作为***建模工具是 Ruby、Groovy 和 ECMAScript(也就是 JavaScript)等动态语言以及许多函数语言的常用技巧。尽管其他语言也可以用函数作为建模工具,(C++ 通过函数指针和/或成员函数指针实现,Java 代码中通过接口引用的匿名内部类实现),但所需的工作比 Scala(以及 Ruby、Groovy、ECMAScript 和其他语言)多得多。这是函数语言使用的 “高阶函数” 概念的扩展。(关于高阶函数的更多内容,请参见 参考资料。)
多亏 Scala 将函数视为值,这样您就可以在运行时需要切换功能的时候利用函数值。可将这种方法视为角色模式 —— Gang of Four 战略模式的一种变体,在这种模式中,对象角色(例如 Person 的当前就职状态)作为运行时值得到了更好的表现,比静态类型的层次结构更好。
层次结构上层的构造函数
回忆一下编写 Java 代码的日子,有时继承类需要从构造函数传递参数至基类构造函数,从而使基类字段能够初始化。在 Scala 中,由于主构造函数出现在类声明中,不再是类的 “传统” 成员,因而将参数传递到基类将成为一个全新维度的问题。
在 Scala 中,主构造函数的参数在 class 行传递,但您也可以为这些参数使用 val 修饰符,以便在类本身上轻松引入读值器(对于 var,则为写值器)。
因此,清单 5 中的 Scala 类 Person 转变为清单 7 中的 Java 类,使用 javap 查看:
清单 7. 请翻译一下
- // This is javap
- C:\Projects\scala-inheritance\code>javap -classpath classes Person
- Compiled from "person.scala"
- public abstract class Person extends java.lang.Object implements scala.ScalaObje
- ct{
- public Person(java.lang.String, java.lang.String, int);
- public java.lang.String toString();
- public abstract void doSomething();
- public int age();
- public java.lang.String lastName();
- public java.lang.String firstName();
- public int $tag();
- }
JVM 的基本规则依然有效:Person 的继承类在构造时向基类传递某些内容,而不管语言强调的是什么。(实际上,这并非完全 正确,但在语言尝试规避此规则时,JVM 会表现失常,因此大多数语言仍然坚持通过某种方法为其提供支持。)当然,Scala 需要坚守此规则,因为它不仅需要保持 JVM 正常运作,而且还要保持 Java 基类正常运作。这也就是说,无论如何,Scala 必须实现一种语法,允许继承类调用基类,同时保留允许我们在基类上引入读值器和写值器的语法。
为了将此放到更具体的上下文中,假设我通过以下方式编写了 清单 5 中的 Student 类:
清单 8. 坏学生!
- // This is Scala
- // This WILL NOT compile
- class Student(val firstName:String, val lastName:String, val age:Int)
- extends Person(firstName, lastName, age)
- {
- def doSomething =
- {
- System.out.println("I'm studying hard, Ma, I swear! (Pass the beer, guys!)")
- }
- }
本例中的编译器将运行很长一段时间,因为我尝试为 Student 类引入一组新方法(firstName、lastName 和 age)。这些方法将与 Person 类上名称类似的方法彼此冲突,Scala 编译器不一定了解我是否正在尝试覆盖基类方法(这很糟糕,因为我可以在这些基类方法后隐藏实现和字段),或者引入相同名称的新方法(这也很糟糕,因为我可以在这些基类方法后隐藏实现和字段)。简而言之,您将看到如何成功覆盖来自基类的方法,但那并不是我们目前要追求的目标。
您还应注意到,在 Scala 中,Person 构造函数的参数不必一对一地与传递给 Student 的参数联系起来;这里的规则实际上与 Java 构造函数的规则完全相同。我们这样做只是为了便于阅读。同样,Student 可要求额外的构造函数参数,与在 Java 语言中一样,如清单 9 所示:
清单 9. 苛求的学生!
- // This is Scala
- class Student(firstName:String, lastName:String, age:Int, val subject:String)
- extends Person(firstName, lastName, age)
- {
- def doSomething =
- {
- System.out.println("I'm studying hard, Ma, I swear! (Pass the beer, guys!)")
- }
- }
您又一次看到了 Scala 代码与 Java 代码有多么的相似,至少涉及继承和类关系时是这样。
语法差异
至此,您可能会对语法的细节感到迷惑。毕竟 Scala 并未像 Java 语言那样将字段与方法区分开来。这实际上是一项深思熟虑的设计决策,允许 Scala 程序员轻而易举地向使用基类的用户 “隐藏” 字段和方法之间的差异。考虑清单 10:
清单 10. 我是什么?
- // This is Scala
- abstract class Person(val firstName:String, val lastName:String, val age:Int)
- {
- def doSomething
- def weight : Int
- override def toString = "[Person: firstName="+firstName+" lastName="+lastName+
- " age="+age+"]"
- }
- class Student(firstName:String, lastName:String, age:Int, val subject:String)
- extends Person(firstName, lastName, age)
- {
- def weight : Int =
- age // students are notoriously skinny
- def doSomething =
- {
- System.out.println("I'm studying hard, Ma, I swear! (Pass the beer, guys!)")
- }
- }
- class Employee(firstName:String, lastName:String, age:Int)
- extends Person(firstName, lastName, age)
- {
- val weight : Int = age * 4 // Employees are not skinny at all
- def doSomething =
- {
- System.out.println("I'm working hard, hon, I swear! (Pass the beer, guys!)")
- }
- }
注意查看如何定义 weight 使其不带有任何参数并返回 Int。这是 “无参数方法”。因为它看上去与 Java 语言中的 “专有” 方法极其相似,Scala 实际上允许将 weight 定义为一种方法(如 Student 中所示),也允许将其定义为字段/存取器(如 Employee 中所示)。这种句法决策使您在抽象类继承的实现方面有一定的灵活性。请注意,在 Java 中,即便是在同一个类中,只有通过 get/set 方法来访问各字段时,才能获得类似的灵活性。不知道判断正确与否,但我认为只有少数 Java 程序员会用这种方式编写代码,因此不经常使用灵活性。此外,Scala 的方法可像处理公共成员一样轻松地处理隐藏/私有成员。
#p#
从 @Override 到 override
继承类经常需要更改在其某个基类内定义的方法的行为;在 Java 代码中,我们通过为继承类添加相同名称、相同签名的新方法来处理这个问题。这种方法的缺点在于签名录入的错误或含糊不清可能会导致没有征兆的故障,这也就意味着代码可以编译,但在运行时无法正确完成操作。
为解决这个问题,Java 5 编译器引入了 @Override 注释。@Override 验证引入继承类的方法实际上已经覆盖了基类方法。在 Scala 中,override 已经成为语言的一部分,几乎可以忘记它会生成编译器错误。因而,继承 toString() 方法应如清单 11 所示:
清单 11. 这是继承的结果
- // This is Scala
- class Student(firstName:String, lastName:String, age:Int, val subject:String)
- extends Person(firstName, lastName, age)
- {
- def weight : Int =
- age // students are notoriously skinny
- def doSomething =
- {
- System.out.println("I'm studying hard, Ma, I swear! (Pass the beer, guys!)")
- }
- override def toString = "[Student: firstName="+firstName+
- " lastName="+lastName+" age="+age+
- " subject="+subject+"]"
- }
非常简单明了。
敲定
当然,允许继承覆盖的反面就是采取措施防止它:基类需要禁止子类更改其基类行为,或禁止任何类型的继承类。在 Java 语言中,我们通过为方法应用修饰符 final 来实现这一点,确保它不会被覆盖。此外,也可以为类整体应用 final,防止继承。实现层次结构在 Scala 中的效果是相同的:我们可以向方法应用 final 来防止子类覆盖它,也可应用于类声明本身来防止继承。
牢记,所有这些关于 abstract、final 和 override 的讨论都同样适用于 “名字很有趣的方法”(Java 或 C# 或 C++ 程序员可能会这样称呼运算符),与应用于常规名称方法的效果相同。因此,我们常常会定义一个基类或特征,为数学函数设定某些预期(可以称之为 “Mathable”),这些函数定义抽象成员函数 “+”、“-”、“*” 和 “/”,另外还有其他一些应该支持的数学运算,例如 pow 或 abs。随后,其他程序员可创建其他类型 — 可能是一个 Matrix 类,实现或扩展 “Mathable”,定义一些成员,看上去就像 Scala 以开箱即用的方式提供的内置算术类型。
差别在于……
如果 Scala 能够如此轻松地映射到 Java 继承模型(就像本文至此您看到的那样),就应该能够从 Java 语言继承 Scala 类,或反之。实际上,这必须 可行,因为 Scala 与其他编译为 Java 字节码的语言相似,必须生成继承自 java.lang.Object 的对象。请注意,Scala 类可能也要继承自其他内容,例如特征,因此实际继承的解析和代码生成的工作方式可能有所不同,但最终我们必须能够以某种形式继承 Java 基类。(切记,特征类似于有行为的接口,Scala 编译器将特征分成接口并将实现推入特征编译的目标类中,通过这种方式来使之运作。)
但结果表明,Scala 的类型层次结构与 Java 语言中的对应结构略有不同;从技术上来讲,所有 Scala 类继承的基类(包括 Int、Float、Double 和其他数字类型)都是 scala.Any 类型,这定义了一组核心方法,可在 Scala 内的任意类型上使用:==、!=、equals、hashCode、toString、isInstanceOf 和 asInstanceOf,大多数方法通过名称即可轻松理解。在这里,Scala 划分为两大分支,“原语类型” 继承自 scala.AnyVal;“类类型” 继承自 scala.AnyRe。(scala.ScalaObject 又继承自 scala.AnyRef。)
通常,这并不是您要直接去操心的方面,但在考虑跨两种语言的继承时,可能会带来某些非常有趣的副作用。例如,考虑清单 12 中的 ScalaJavaPerson:
清单 12. 混合!
- // This is Scala
- ass ScalaJavaPerson(firstName:String, lastName:String, age:Int)
- extends JavaPerson(firstName, lastName, age)
- val weight : Int = age * 2 // Who knows what Scala/Java people weigh?
- override def toString = "[SJPerson: firstName="+firstName+
- " lastName="+lastName+" age="+age+"]"
……它继承自 JavaPerson:
清单 13. 看起来是否眼熟?
- // This is Java
- public class JavaPerson
- {
- public JavaPerson(String firstName, String lastName, int age)
- {
- this.firstName = firstName;
- this.lastName = lastName;
- this.age = age;
- }
- public String getFirstName()
- {
- return this.firstName;
- }
- public void setFirstName(String value)
- {
- this.firstName = value;
- }
- public String getLastName()
- {
- return this.lastName;
- }
- public void setLastName(String value)
- {
- this.lastName = value;
- }
- public int getAge()
- {
- return this.age;
- }
- public void setAge(int value)
- {
- this.age = value;
- }
- public String toString()
- {
- return "[Person: firstName" + firstName + " lastName:" + lastName +
- " age:" + age + " ]";
- }
- private String firstName;
- private String lastName;
- private int age;
- }
在编译 ScalaJavaPerson 时,它将照常扩展 JavaPerson,但按照 Scala 的要求,它还会实现 ScalaObject 接口。并照例支持继承自 JavaPerson 的方法,因为 ScalaJavaPerson 是一种 Scala 类型,我们可以期望它支持 Any 引用的指派,根据 Scala 的规则:
清单 14. 使用 ScalaJavaPerson
- // This is Scala
- val richard = new ScalaJavaPerson("Richard", "Campbell", 45)
- System.out.println(richard)
- val host : Any = richard
- System.out.println(host)
但在 Scala 中创建 JavaPerson 并将其指派给 Any 引用时会发生什么?
清单 15. 使用 JavaPerson
- // This is Scala
- val carl = new JavaPerson("Carl", "Franklin", 35)
- System.out.println(carl)
- val host2 : Any = carl
- System.out.println(host2)
结果显示,这段代码如期编译并运行,因为 Scala 能确保 JavaPerson “做正确的事情”,这要归功于 Any 类型与 java.lang.Object 类型的相似性。实际上,几乎可以说,所有扩展 java.lang.Object 的内容都支持存储到 Any 引用之中。(存在一些极端情况,我听说过,但我自己还从未遇到过这样的极端情况。)
最终结果?出于实践的目的,我们可以跨 Java 语言和 Scala 混搭继承,而无需过分担心。(***的麻烦将是试图了解如何覆盖 “名字很有趣的 Scala 方法”,例如 ^=!# 或类似方法。)
结束语
在本月的文章中,我为您介绍了 Scala 代码和 Java 代码之间的高度相似性意味着 Java 开发人员可以轻松理解并使用 Scala 的继承模型。方法覆盖的工作方式相同,成员可见性的工作方式相同,还有更多相同的地方。对于 Scala 中的所有功能,继承或许与 Java 开发中的对应部分最为相似。惟一需要技巧的部分就是 Scala 语法,这有着明显的差异。
习惯两种语言中继承方法的相似之处和细微的差异,您就可以轻松编写您自己的 Java 程序的 Scala 实现。例如,考虑流行的 Java 基类和框架的 Scala 实现,如 JUnit、Servlets、Swing 或 SWT。实际上,Scala 团队已经提供了一个 Swing 应用程序,名为 OOPScala(参见 参考资料),它使用 JTable,通过相当少的几行代码(数量级远远低于传统 Java 的对应实现)提供了简单的电子表格功能。
因此,如果您想知道如何在您的生产代码中应用 Scala,就应该准备好迈出探索的***步。考虑在 Scala 中编写下一个程序的一小部分。正如您在这期文章中所了解到的那样,从恰当的基类继承,采用与 Java 程序中相同的方式提供覆盖,您就不会遇到任何麻烦。
【相关阅读】