在20年的大部分时间里,面向对象语言设计的主要内容一直是继承的概念。 不支持继承的语言(例如Visual Basic)被嘲笑为不适合实际工作的“玩具语言”。 同时,支持继承的语言的做法也有所不同,引发了许多小时的争论。 多重继承是真的必要吗(由C ++的开发者决定),还是无用又丑陋(由C#和Java语言的开发者确定)? Ruby和Scala是两种较新的语言,它们已在多重继承方面走了中间路线-正如我上个月在介绍Scala的特性时所讨论的那样(请参阅参考资料 )。
与所有出色的语言一样,Scala也支持实现继承(请参阅参考资料 )。 在Java语言中,单一实现继承模型允许您扩展基类并添加新的方法和字段,等等。 尽管在语法上进行了一些更改,但是Scala的实现继承外观和感觉与Java语言中的相同。 差异与Scala融合对象和功能语言设计的方式有关,本月值得探讨。
就像我在本系列以前的文章中所做的那样,我将使用Person
类作为探索Scala继承系统的起点。 清单1显示了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对象),具有三个只读字段。 你可能还记得,让他们读,写,你只需要改变val
的UE var
iables在主构造函数的声明。
无论如何,使用Person
类型也很简单,如清单2所示:
// This is Scala
object PersonApp
{
def main(args : Array[String]) : Unit =
{
val bindi = new Person("Tabinda", "Khan", 38)
System.out.println(bindi)
}
}
几乎没有破天荒的代码,但这为我们提供了一个起点。
作为这个系统的发展,很明显的是, Person
类缺乏一个相当重要的一块是一个Person
,这是做什么的行为。 我们中的许多人通过我们对生活的态度来定义自己,而不仅仅是存在和占用空间。 因此,我将添加一个新方法,如清单3所示,它为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
本身,如清单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
方法的定义Person.doSomething
无法使用; 该方法要么需要一个完整的主体(可能抛出异常以指示应在派生类中重写它),要么它不需要任何主体,类似于抽象方法在Java代码中的工作方式。 我尝试清单5中的抽象路线:
// 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语言没有什么不同。
由于Scala融合了对象和功能语言样式,因此如上所述,我实际上可以为Person
建模,但无需创建子类型。 这有点让人不解,但确实确实强调了Scala将这两种设计风格以及随之而来的非常有趣的想法进行了整合。
回想一下以前的文章 ,Scala将函数视为值,就像该语言中的其他任何值一样,例如Int
, Float
或Double
。 在这种情况下,我可以通过对我的Person
进行建模以使其具有doSomething
而不是将其作为要在派生类中重写的方法,而是将其作为要调用,替换和扩展的函数值 。 清单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)之类的动态语言以及许多功能语言中的常见技巧。 尽管可以使用其他语言(通过函数的指针和/或成员函数的指针,或通过接口引用的匿名内部类实现的Java代码,来实现C ++),但它的工作量远远超过Scala(以及Ruby,Groovy,ECMAScript等)。 这是功能程序员抛弃的“高阶函数”概念的扩展。 (请参阅相关主题的更多高阶功能。)
由于Scala将函数视为值,因此您可以在运行时可能需要切换功能的任何地方使用函数值。 您可能会认识到这种方法是“角色”模式,这是“四个策略”模式的一种变体,其中对象角色(例如Person
的当前就业状况)比静态类型层次结构更好地表示为运行时值。
回想一下,如果您从编写Java代码的那一天开始,有时派生类需要将参数从其构造函数传递到其基类构造函数,以便允许对基类字段进行初始化。 在Scala中,由于主要构造函数出现在类声明中,而不是作为类的“传统”成员,因此将参数传递到基数将具有一个全新的维度。
在Scala中,主要的构造函数参数是在class
行上传递的,但是您也可以在这些参数上使用val
修饰符,以便在类本身上轻松引入访问器(对于var
,则是变异器)。
因此, 清单5中的Scala类Person
变成了清单7中的Java类,如javap所示:
// 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
类:
// 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
的参数一对一地Student
; 这里的规则与Java的构造函数完全一样。 我们这样做只是为了易于阅读。 而且, Student
可以要求其他构造函数参数,就像Java语言一样,如清单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:
// 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的方法对隐藏/私有成员的作用与对公共成员的作用一样容易。
通常,派生类希望更改在其基类之一中定义的方法的行为; 在Java代码中,我们只需在派生类中添加一个具有相同名称和签名的新方法即可解决此问题。 这种方法的缺点是签名中的错别字或轻微歧义可能会悄无声息地失败,这意味着代码可以编译,但在运行时“做错了事”。
为了解决这个问题,Java 5编译器引入了@Override
注释。 @Override
为javac验证派生类中引入的方法实际上已经重写了基类方法。 在Scala中, override
已成为该语言的一部分,并且忘记它会生成编译器错误。 因此,派生的toString()
方法应如清单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继承模型,则应该有可能让Scala类从Java语言继承,反之亦然。 实际上,这绝对是有可能的,因为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.AnyRef
继承。 ( scala.ScalaObject
依次继承自scala.AnyRef
。)
通常,您不必直接担心这一点,但是在考虑跨两种语言的继承时,它可能会产生一些罕见的有趣副作用。 例如,考虑清单12中的ScalaJavaPerson
:
// This is Scala
class 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
:
// 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类型,因此我们可以期望它支持根据Scala的规则分配给Any
引用:
// 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
引用时,会发生什么情况呢?
// This is Scala
val carl = new JavaPerson("Carl", "Franklin", 35)
System.out.println(carl)
val host2 : Any = carl
System.out.println(host2)
事实证明,由于Any
类型与java.lang.Object
类型的相似性,Scala可以静默确保JavaPerson
“做正确的事”,因此该代码可以按预期进行编译和运行。 实际上,可以说任何扩展java.lang.Object
东西也都支持将其存储到Any
引用中。 (有人告诉我有一些极端的情况,但到目前为止我还没有遇到任何我自己的人。)
净结果? 出于所有实际目的,我们可以在Java语言和Scala中混合并匹配继承,而不必太担心。 (最大的麻烦是试图弄清楚如何重写Scala的“带有有趣名称的方法”,例如“ ^=!#
”或类似名称。)
正如我本月向您展示的那样,Scala代码与Java代码之间的紧密保真度意味着Scala的继承模型对于Java开发人员来说很容易理解。 方法覆盖是相同的,成员可见性是相同的,依此类推。 在Scala的所有功能中,继承可能与您自己的Java开发中所见的最相似。 唯一棘手的部分是Scala语法,这明显不同。
熟悉两种语言在继承方式上的重叠(略有不同)意味着您可以开始轻松编写自己的Java程序的Scala实现。 例如,考虑流行的Java基类和框架(例如JUnit,Servlet,Swing或SWT)的Scala实现。 事实上,Scala的团队制作一个Swing应用程序,名为OOPScala(见相关信息 ),所使用JTable
在一个小得可笑的几行代码的(容易的不是传统的Java相当于量级以下)提供了简单的电子表格功能。
因此,如果您一直想知道Scala如何应用于您的生产代码,那么现在就应该准备开始第一步。 只需考虑在Scala中编写下一个程序的某些部分。 正如本月所学,从适当的基类继承并以Java程序中的方式提供重写将没有问题。
翻译自: https://www.ibm.com/developerworks/java/library/j-scala05298/index.html