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