20.3 类和trait

现在来看看类与接口在Java和Scala中的不同。这两种结构在我们设计应用时都很常用。你会看到相对于Java的类和接口,Scala的类和接口提供了更多的灵活性。

20.3.1 更加简洁的Scala类

由于Scala也是一门完全的面向对象语言,因此你可以创建类,并将其实例化生成对象。最基础的形态上,声明和实例化类的语法与Java非常类似。比如,下面是一个声明Hello类的例子:

  1. class Hello {
  2. def sayThankYou(){
  3. println("Thanks for reading our book")
  4. }
  5. }
  6. val h = new Hello()
  7. h.sayThankYou()
getter方法和setter方法

一旦你定义的类具有了字段,这件事情就变得有意思了。你碰到过单纯只定义字段列表的Java类吗?很明显,你还需要声明一长串的getter方法、setter方法,以及恰当的构造器。多麻烦啊!除此之外,你还需要为每一个方法编写测试。在企业Java应用中,大量的代码都消耗在了这样的类中。比如下面这个简单的Student类:

  1. public class Student {
  2. private String name;
  3. private int id;
  4. public Student(String name) {
  5. this.name = name;
  6. }
  7. public String getName() {
  8. return name;
  9. }
  10. public void setName(String name) {
  11. this.name = name;
  12. }
  13. public int getId() {
  14. return id;
  15. }
  16. public void setId(int id) {
  17. this.id = id;
  18. }
  19. }

你需要手工定义构造器对所有的字段进行初始化,还要实现两个getter方法和两个setter方法。一个非常简单的类现在需要超过20行的代码才能实现!有的集成开发环境或者工具能帮你自动生成这些代码,不过你的代码库中还是需要增加大量额外的代码,而这些代码与你实际的业务逻辑并没有太大的关系。

Scala语言中构造器、getter方法以及setter方法都能隐式地生成,从而大大降低你代码中的冗余:

  1. class Student(var name: String, var id: Int)
  2. val s = new Student("Raoul", 1) ←---- 初始化Student对象
  3. println(s.name) ←---- 取得名称,打印输出Raoul
  4. s.id = 1337 ←---- 设置id
  5. println(s.id) ←---- 打印输出1337

在Java中,可以通过定义公共字段来获得类似的行为,但仍然需要显式地定义构造函数。Scala类为你保存模板代码。

20.3.2 Scala的trait与Java 8的接口对比

Scala还提供了另一个非常有助于抽象对象的特性,名称叫trait。它是Scala为实现Java中的接口而设计的替代品。trait中既可以定义抽象方法,也可以定义带有默认实现的方法。trait同时还支持Java中接口那样的多继承,所以你可以将它们看成与Java中接口类似的特性,它们都支持默认方法。trait中还可以包含像抽象类这样的字段,而Java的接口不支持这样的特性。那么,trait就类似于抽象类吗?显然不是,因为trait支持多继承,而抽象类不支持多继承。Java支持类型的多继承,因为一个类可以实现多个接口。现在,Java 8通过默认方法又引入了对行为的多继承,不过它依旧不支持对状态的多继承,而这恰恰是trait支持的。

为了展示Scala中的trait到底是什么样,来看一个例子。我们定义了一个名为Sized的trait,它包含一个名为size的可变字段,以及一个带有默认实现的isEmpty方法:

  1. trait Sized{
  2. var size : Int = 0 ←---- 名为size的字段
  3. def isEmpty() = size == 0 ←---- 带默认实现的isEmpty方法
  4. }

你现在可以使用一个类在声明时构造它,下面这个例子中Empty类的size恒定为0:

  1. class Empty extends Sized ←---- 一个继承自trait Sized的类
  2. println(new Empty().isEmpty()) ←---- 打印输出true

有一件事非常有趣,trait和Java的接口类似,也是在对象实例化时被创建(不过这依旧是一个编译时的操作)。比如,你可以创建一个Box类,动态地决定到底选择哪一个实例支持由trait Sized定义的操作:

  1. class Box
  2. val b1 = new Box() with Sized ←---- 在对象实例化时构建trait
  3. println(b1.isEmpty()) ←---- 打印输出true
  4. val b2 = new Box()
  5. b2.isEmpty() ←---- 编译错误:因为Box类的声明并未继承Sized

如果一个类继承了多个trait,各trait中声明的方法又使用了相同的签名或者相同的字段,这时会发生什么情况?为了解决这些问题,Scala中定义了一系列限制,这些限制和之前在第13章介绍默认方法时的限制极其类似。