Jade Dungeon

类的继承与层级

组合与继承

定制一个二维布局库

作为本章运行的例子,我们将创造一个制造和渲染二维布局元素的库。每个元素将代表一个 填充字符的长方形。方便起见,库将提供名为elem的工厂方法来通过传入的数据构造新的 元素。例如,你将能通过工厂方法采用下面的写法创建带有字串的元素:

  elem(s: String): Element 

元素将以名为Element的类型为模型。你将能在元素上调用abovebeside,把另一个 元素放在当前元素的右边或是上边:

  val column1 = elem("hello") above elem("***")
  val column2 = elem("***") above elem("world")
  column1 beside column2

打印这个表达式的结果将是:

  hello ***  
   *** world

抽象类

abstract声明抽象类

布局元素名为Element,存放的文本内容类型为Array[String]。提供方法contents 取得存放的文本内容,但没有定义实现方式,所以这个类是抽象类,要加上abstract 关键字:

  abstract class Element {
    def contents: Array[String]
  } 

抽象方法

注意:类Elementcontents方法并没带有abstract修饰符。不像Java,方法的声明 中不需要(也不允许)抽象修饰符。如果方法没有实现,它就是抽象的。

另一个术语用法需要分辨声明(declaration)和定义(definition)。类Element声明了 抽象方法contents,但当前没有定义具体方法。

抽象字段

abstract class Person {
	val id: Int
	val Name: String

这两个字段并没有生成在对应的Java类中,产生的只有对应的方法:

  • val只有抽象getter方法。
  • var有抽象的gettersetter方法。

实现类要提供具体的字段,对于抽象的字段不用加abstract

class Employee(val: id: Int) extends Person {
	var name = ""
}

实现类可以是一个匿名类:

val fred = new Person {
	val id = 1729
	var name = "Fred"
}

定义无参数方法

添加显示宽度和高度的方法:height方法返回contents里的行数。width方法返回 第一行的长度,或如果元素没有行记录,返回零。(也就是说你不能定义一个高度为零但 宽度不为零的元素。)

  abstract class Element {
    def contents: Array[String]
    def height: Int = contents.length
    def width: Int = if (height == 0) 0 else contents(0).length
  } 

三个方法没一个有参数列表,甚至连个空列表都没有。如:

  def width(): Int
  // 省略括号
  def width: Int 

推荐的惯例是在没有参数并且方法仅通过读含有对象的方式访问可变状态(专指其不改变 可变状态)时使用无参数方法。这样感觉上就和只读字段一样,其实也可以选择把widthheight作为字段而不是方法来实现,只要简单地在每个实现里把def修改成val 即可:

  abstract class Element {
    def contents: Array[String]
    val height = contents.length
    val width = 
      if (height == 0) 0 else contents(0).length
  }

两组定义从客户的观点来看是完全相同的。唯一的差别是字段的访问或许稍微比方法调用要 快,因为字段值在类被初始化的时候被预计算,而方法调用在每次调用的时候都要计算。

换句话说,字段在每个Element实例上需要更多的内存空间。因此类的使用概况,属性 表达成字段还是方法更好,决定了其实现,并且这个概况还可以随时改变。

重点是Element类的客户不应在其内部实现改变的时候受影响。

特别是如果类的字段变成了访问函数,且访问函数是纯的,就是说它没有副作用并且 不依赖于可变状态,那么类Element的客户不需要被重写。客户都不应该需要关心这些。

目前为止一切良好。但仍然有些琐碎的复杂的东西要去做以协同Java处理事情的方式。问题 在于Java没有实现统一访问原则。因此Java里是string.length(),不是string.length (尽管是array.length,不是array.length())。不用说,这让人很困惑。

为了在这道缺口上架一座桥梁,Scala在遇到混合了无参数和空括号方法的情况时很大度。 特别是,你可以用空括号方法重载无参数方法,并且反之亦可。你还可以在调用任何不带 参数的方法时省略空的括号。例如,下面两行在Scala里都是合法的:

  Array(1, 2, 3).toString
  "abc".length

原则上Scala的函数调用中可以省略所有的空括号。然而,在调用的方法表达的超过其接收 调用者实例的属性时,推荐仍然写一对空的括号。例如,如果方法执行了I/O,或写入 可重新赋值的变量(var),或读出不是接受调用者的字段的var,无论是直接的还是非直接 的通过使用可变实例,那么空括号是合适的。这种方式是让参数列表扮演一个可见的线索 说明某些有趣的计算正通过调用被触发。例如:

  "hello".length  // no () because no side-effect
  println()       // better to not drop the ()

总结起来,Scala里定义不带参数也没有副作用的方法为无参数方法,也就是说,省略空的 括号,是鼓励的风格。另一方面,永远不要定义没有括号的带副作用的方法,因为那样的话 方法调用看上去会像选择一个字段。这样你的客户看到了副作用会很奇怪。相同地,当你 调用带副作用的函数,请确信写这个调用的时候包括了空的括号。另一种考虑这个问题的 方式是,如果你调用的函数执行了操作,使用括号,但如果仅提供了对某个属性的访问, 省略括号。

扩展类

实例化一个元素,我们需要创建扩展了Element并实现抽象的contents方法的子类。

  class ArrayElement(conts: Array[String]) extends Element {
    def contents: Array[String] = conts
  }

这种extends子句有两个效果:使类ArrayElement从类Element继承所有非私有的成员 ,并且使ArrayElement成为Element的子类型。由于ArrayElement扩展了Element, 类ArrayElement被称为类Element的子类。反过来,ElementArrayElement的超类 。

如果你省略extends子句,Scala编译器隐式地假设你的类扩展自scala.AnyRef,在Java 平台上与java.lang.Object一致。因此,类Element隐式地扩展了类AnyRef

ArrayElementcontents方法实现了类Element的抽象方法contents

  scala> val ae = new ArrayElement(Array("hello", "world"))
  ae: ArrayElement = ArrayElement@d94e60

  scala> ae.width
  res1: Int = 5

子类型化(subtyping)是指子类的值可以被用在需要其超类的值的任何地方。例如:

  val e: Element = new ArrayElement(Array("hello"))

如果子类中的字段与超类同名,或是子类的中的方法名称和参数与超类类完全一样,就会 覆盖(override)超类中的版本。而且Scala里强制如果覆盖了就一定要加上override 修饰符。

重写方法和字段

命名空间

Java为定义准备了四个命名空间:字段,方法,类型和包。

而Scala仅有两个,与Java的四个命名空间相对:

  • 值(字段,方法,包还有单例对象)
  • 类型(类和特质名)

Scala把字段和方法放进同一个命名空间的理由很清楚,因为这样你就可以使用val重重写 无参数的方法,这种你在Java里做不到的事情。

字段和方法属于相同的命名空间。这使得字段重写无参数方法成为可能。比如说,你可以 改变类ArrayElementcontents的实现,从一个方法变为一个字段,而无需修改类 Elementcontents的抽象方法定义:

  class ArrayElement(conts: Array[String]) extends Element {
    val contents: Array[String] = conts
  }

这个ArrayElement的版本里,字段contents(用val定义)完美地实现了类Element 里的无参数方法contents(用def定义)。

另一方面,Scala里禁止在同一个类里用同样的名称定义字段和方法,而在Java里这样做 被允许。例如,下面的Java类能够很好地编译:

  // This is Java
  class CompilesFine {
    private int f = 0;
    public int f() {
      return 1;
    }
  }

但是相应的Scala类将不能编译:


  class WontCompile {
    private var f = 0 // Won't compile, because a field 
    def f = 1         // and method have the same name
  }

如果要调试实例的构造顺序,可在编译加上参数-Xcheckinit。这样在方法末初始化字段 时会抛出异常。

使用override修饰符

考虑一下这样的场景:

基类和子类是不同的人维护的。原来基类里没有add方法,所以子类里加上了。后来 基类里也加上了add方法,但维护子类的人不知道。这样的规定是为了防止「脆基类」问题 。

所以Scala里override有强制的规定:

  • 如果实现了抽象成员,加不加随便。
  • 如果重载了具体实现,就一定要加。
  • 没有重载就绝不能加。

这样起码保证了维护子类的人知道自己会覆盖超类的方法。

不可重写 final

Scala中字段与方法都可以用final修饰为不可重写(因为Scala的字段也是可以重写为 方法的)。注意这与Java不一样,Java里final字段表示不可改变,而Scala里已经有 val表示不可改变了。

重写限制

概括:

  • def只能重写另一个def
  • val只能重写另一个val与无参def
  • var只能重写另一个抽象的var

详述:

  def val var
用val重写 子类有一个私有字段。<br/>重写超类的getter方法 超类同名的私有字段。<br/>重写超类的getter方法 错误
用def重写 同Java 错误 错误
用var重写 同时重写getter/setter。<br/>只重写getter会报错 错误 重写超类的抽象var

在当前类中,随时可以对gettersetter重新实现var,但在子类中不能通过 gettersetter重新实现var,只能接受现有的实现。

结构类型

结构类型(structural type)只给出类必须拥有的方法,而不是类的名称。

如:我不知道反射出来的是什么类型,但我知道应该要有hello(String): String方法:

class Foo { def hello(name: String): String = "Hello there, %s".format(name) }

object FooMain {
    def main(args: Array[String]) {
        val foo = Class.forName("Foo").newInstance
          .asInstanceOf[{ def hello(name: String): String }]
        println(foo.hello("Walter")) // prints "Hello there, Walter"
    }
}

定义参数化字段

ArrayElement类的定义。它有一个参数conts,其唯一目的是被复制到contents字段 。选择conts这个参数的名称只是为了让它看上去更像字段名contents而又不会因为 名字一样而发生实际冲突。这是一种「代码异味」,一个表明或许某些不必须的累赘和重复。

可以通过在单一的参数化字段(parametric field)定义中组合参数和字段避免:

  class ArrayElement(val contents: Array[String]) extends Element

注意用的是val,所以现在拥有一个可以从类外部访问的,(不能重新赋值的)字段 contents。字段使用参数值初始化。等同于:

  class ArrayElement(x123: Array[String]) extends Element { 
    val contents: Array[String] = x123
  } 

同样也可以使用var前缀类参数,这种情况下相应的字段将能重新被赋值。还有可能添加 如privateprotectedoverride这类的修饰符到这些参数化字段上,就好象 你可以在其他类成员上做的事情:

  class Cat {
    val dangerous = false
  }
  class Tiger(
    override val dangerous: Boolean,
    private var age: Int
  ) extends Cat

Tiger的定义是以下包括重写成员dangerousprivate成员age的类定义替代写法的 简写:

  class Tiger(param1: Boolean, param2: Int) extends Cat {
    override val dangerous = param1
    private var age = param2
  }

调用超类构造器

如果再要新的子类:

  class LineElement(s: String) extends ArrayElement(Array(s)) {
    override def width = s.length
    override def height = 1
  }

由于LineElement扩展了ArrayElement,并且ArrayElement的构造器带一个参数 (Array[String]),LineElement需要传递一个参数到它的超类的主构造器。要调用超类 构造器,只要把你要传递的参数或参数列表放在超类名之后的括号里)即可。

只有主构造器可以调用超类构造器

Scala中辅助构造器不能调用超类构造器,只有主构造器可以调用超类构造器。

构造顺序

Scala与Java有一个共同的问题:超类的构造器会调用被子类覆盖的方法。

用动物的视力(或感知以距离)来作为例子:

  • 默认动物的视力为10
  • 蚂蚁的视力只有2
class Creature {
	val range: Int = 10
	val env: Array[Int] = new array[Int](range)
}

class Ant extends Creature {
	override val range = 2
}

如果初始化Ant实例,过程比较复杂:

  1. 调用超类构造器,设置range为10。
  2. 超类构造器初始化env长度时要用到range,发现range被子类重写。
  3. 调用子类的range,但是子类还没有初始化,所以range值为0
  4. env被初始化为长度为0的数组。超类构造器执行完毕。
  5. 子类构造器开始执行,把range设置为2.

解决的方案:

  • final val声明不能覆盖,这样安全但是不灵活。
  • lazy懒加载。安全但是影响性能。
  • 预初始化。接下来就讲。
预初始化

with代替extends,并给字段定义加上花括号,放在超类的构造器之前:

class Ant {
	override val range = 2
} with Creature

由于预初始化的字段的超类构造器调用前被初始化,所以不能引用正在被构造的实例。所以 对于this实际指向的是正被构造的类或对象的实例,而来是被构造的实例本身。

多态和动态绑定

创建一个新的子类,它可以按给出的长度宽度,用指定的字符填充:

  class UniformElement(
    ch: Char, 
    override val width: Int,
    override val height: Int 
  ) extends Element {
    private val line = ch.toString * width
    def contents = Array.make(height, line)
  }

父类的变量可以存放子类的实例,就是多态的一种体现。这么多子类都可以用父类的变量来 存放:

  val e1: Element = new ArrayElement(Array("hello", "world"))
  val ae: ArrayElement = new LineElement("hello")
  val e2: Element = ae
  val e3: Element = new UniformElement('x', 2, 3)

变量和表达式上的方法调用是动态绑定(dynamically bound)的。这意味着被调用的 实际方法实现取决于运行期实例实际的类,而不是变量或表达式的类型。

为了演示这种行为,我们会从我们的Element类中临时移除所有存在的成员并添加一个名 为demo的方法。我们会在ArrayElementLineElement中重写demo,但 UniformElement除外:

  abstract class Element {
    def demo() {
      println("Element's implementation invoked")
    }
  }

  class ArrayElement extends Element {
    override def demo() {
      println("ArrayElement's implementation invoked")
    }
  }

  class LineElement extends ArrayElement {
    override def demo() {
      println("LineElement's implementation invoked")
    }
  }

  // UniformElement inherits Element's demo
  class UniformElement extends Element 

如果你把这些代码输入到了解释器中,那么你就能定义这个带了一个Element并调用 demo的方法:

  def invokeDemo(e: Element) {
    e.demo()
  }

如果你传给invokeDemo一个ArrayElement,你会看到一条消息指明ArrayElementdemo实现被调用,尽管被调用demo的变量e的类型是Element

  scala> invokeDemo(new ArrayElement)
  ArrayElement's implementation invoked

相同的,如果你传递LineElementinvokeDemo,你会看到一条指明LineElementdemo实现被调用的消息:

  scala> invokeDemo(new LineElement)
  LineElement's implementation invoked

传递UniformElement时的行为一眼看上去会有些可以,但是正确:

  scala> invokeDemo(new UniformElement)
  Element's implementation invoked

因为UniformElement没有重写demo,它从它的超类Element继承了demo的实现。 因此,当实例的类是UniformElement时,Element的实现就是要调用的demo的正确 实现。

使用组合与继承

组合与继承是利用其它现存类定义新类的两个方法。

如果你接下来的工作主要是代码重用,通常你应采用组合而不是继承。只有继承受脆基类 问题困扰,这种情况你可能会无意中通过改变超类而破坏了子类。

关于继承关系你可以问自己一个问题,是否它建模了一个is-a关系。你能问的另一个问题是 ,是否客户想要把子类类型当作超类类型来用。

实现示例中的功能

把一个元素放在另一个上面是指串连这两个元素的contents值。

  def above(that: Element): Element =
    new ArrayElement(this.contents ++ that.contents)

操作符++把两个元素靠在一起,我们将创造一个新的元素,其中的每一行都来自于两个 元素的相应行的串连。

  def beside(that: Element): Element = {
    val contents = new Array[String](this.contents.length)
    for (i <- 0 until this.contents.length) 
      contents(i) = this.contents(i) + that.contents(i)
    new ArrayElement(contents)
  }

索引数组的循环是指令式风格。这个方法可以替代缩减成一个表达式:

  new ArrayElement(
    for (
      (line1, line2) <- this.contents zip that.contents
    ) yield line1 + line2
  )

zip操作符转换为一个对子的数组(可以称为Tupele2)。zip方法从它的两个参数中 拣出相应的元素并组织成对子数组。

例如,表达式:

  Array(1, 2, 3) zip Array("a", "b")

将生成:

  Array((1, "a"), (2, "b"))

如果两个操作数组的其中一个比另一个长,zip将舍弃余下的元素。

定义toString方法返回元素格式化成的字串:

  override def toString = contents mkString "\n"

最后是这个样子:

  abstract class Element {

    def contents: Array[String]

    def width: Int =
      if (height == 0) 0 else contents(0).length

    def height: Int = contents.length

    def above(that: Element): Element =
      new ArrayElement(this.contents ++ that.contents)

    def beside(that: Element): Element =
      new ArrayElement(
        for (
          (line1, line2) <- this.contents zip that.contents
        ) yield line1 + line2
      )

    override def toString = contents mkString "\n"
  }

定义工厂对象

最直接的方案是创建类Element的伴生对象并把它做成布局元素的工厂方法。这种方式 唯一要暴露给客户的就是Element的类/实例组合,隐藏它的三个实现类ArrayElementLineElementUniformElement

  object Element {

    def elem(contents: Array[String]): Element = 
      new ArrayElement(contents)

    def elem(chr: Char, width: Int, height: Int): Element = 
      new UniformElement(chr, width, height)

    def elem(line: String): Element = 
      new LineElement(line)
  }

这些工厂方法使得改变类Element的实现通过使用elem工厂方法实现而不用new操作 产新的ArrayElement实例成为可能。

为了不使用单例对象的名称Element从而化调用工厂方法,我们将在源文件引入 Element.elem

换句话说,代之以在Element类内部使用Element.elem调用工厂方法,我们将引用 Element.elem,这样我们只要使用它们的简化名,elem,就可以调用工厂方法。

  import Element.elem

  abstract class Element {

    def contents: Array[String]

    def width: Int =
      if (height == 0) 0 else contents(0).length

    def height: Int = contents.length

    def above(that: Element): Element =
      elem(this.contents ++ that.contents)

    def beside(that: Element): Element =
      elem(
        for (
          (line1, line2) <- this.contents zip that.contents
        ) yield line1 + line2
      )

    override def toString = contents mkString "\n"
  }

有了工厂方法之后,子类ArrayElementLineElementUniformElement不再需要 直接被客户访问,所以可以改成是私有的。

Scala里,你可以在类和单例对象中定义其它的类和单例对象。因此一种让Element的子类 私有化的方式就是把它们放在Element单例对象中并在那里声明它们为私有。需要的时候 ,这些类将仍然能被三个elem工厂方法访问。

    private class ArrayElement(
      val contents: Array[String]
    ) extends Element

    private class LineElement(s: String) extends Element {
      val contents = Array(s)
      override def width = s.length
      override def height = 1
    }

    private class UniformElement(
      ch: Char,
      override val width: Int,
      override val height: Int
    ) extends Element {
      private val line = ch.toString * width
      def contents = Array.make(height, line)
    }

    def elem(contents:  Array[String]): Element =
      new ArrayElement(contents)

    def elem(chr: Char, width: Int, height: Int): Element =
      new UniformElement(chr, width, height)

    def elem(line: String): Element =
      new LineElement(line)
  }

变高变宽

Element的版本并不完全,因为他不允许客户把不同宽度的元素堆叠在一起,或者不同高度 的元素靠在一起。比方说,下面的表达式将不能正常工作,因为组合元素的第二行比第一行 要长:

  new ArrayElement(Array("hello")) above 
  new ArrayElement(Array("world!"))

与之相似的,下面的表达式也不能正常工作:


  new ArrayElement(Array("one", "two")) beside 
  new ArrayElement(Array("one"))

添加私有帮助方法widen通过带个宽度做参数并返回那个宽度的Elementheighten, 能在竖直方向执行同样的功能。

  import Element.elem

  abstract class Element {
    def contents:  Array[String]

    def width: Int = contents(0).length
    def height: Int = contents.length

    def above(that: Element): Element = {
      val this1 = this widen that.width
      val that1 = that widen this.width
      elem(this1.contents ++ that1.contents)
    }

    def beside(that: Element): Element = {
      val this1 = this heighten that.height
      val that1 = that heighten this.height
      elem(
        for ((line1, line2) <- this1.contents zip that1.contents) 
        yield line1 + line2)
    }

    def widen(w: Int): Element = 
      if (w <= width) this
      else {
        val left = elem(' ', (w - width) / 2, height) 
        var right = elem(' ', w - width - left.width, height)
        left beside this beside right
      }

    def heighten(h: Int): Element = 
      if (h <= height) this
      else {
        val top = elem(' ', width, (h - height) / 2)
        var bot = elem(' ', width, h - height - top.height)
        top above this above bot
      }

    override def toString = contents mkString "\n"
  }

完整的示例代码

写一个画给定数量边界的螺旋的程序。

// In file compo-inherit/Spiral.scala

  import Element.elem

  object Spiral {

    val space = elem(" ")
    val corner = elem("+")

    def spiral(nEdges: Int, direction: Int): Element = {
      if (nEdges == 1)
        elem("+")
      else {
        val sp = spiral(nEdges - 1, (direction + 3) % 4)
        def verticalBar = elem('|', 1, sp.height)
        def horizontalBar = elem('-', sp.width, 1)
        if (direction == 0)
          (corner beside horizontalBar) above (sp beside space)
        else if (direction == 1)
          (sp above space) beside (corner above verticalBar)
        else if (direction == 2)
          (space beside sp) above (horizontalBar beside corner)
        else
          (verticalBar above corner) beside (space above sp)
      }
    }

    def main(args: Array[String]) {
      val nSides = args(0).toInt
      println(spiral(nSides, 0))
    }
  }
$ scala Spiral 6    $ scala Spiral 11    $ scala Spiral 17
+-----              +----------          +----------------
|                   |                    |                
| +-+               | +------+           | +------------+ 
| + |               | |      |           | |            | 
|   |               | | +--+ |           | | +--------+ | 
+---+               | | |  | |           | | |        | | 
                    | | ++ | |           | | | +----+ | | 
                    | |    | |           | | | |    | | | 
                    | +----+ |           | | | | ++ | | | 
                    |        |           | | | |  | | | | 
                    +--------+           | | | +--+ | | | 
                                         | | |      | | | 
                                         | | +------+ | | 
                                         | |          | | 
                                         | +----------+ | 
                                         |              | 
                                         +--------------+ 

Scala类的层级

Scala里,每个类都继承自通用的名为Any的超类。因为所有的类都是Any的子类,那么 定义在Any中的方法就是「普遍」方法:它们可以被任何实例调用。

Scala还在层级的底端定义了NullNothing,主要都扮演通用的子类。例如,就像说 Any是所有其它类的超类,Nothing是所有其它类的子类。

Scala类层级关系

Scala类的概览

层级的顶端是类Any,定义了包含下列的方法:

  final def ==(that: Any): Boolean
  final def !=(that: Any): Boolean
  def equals(that: Any): Boolean
  def hashCode: Int
  def toString: String

类Any里的=!=,被声明为final,因此它们不能在子类里面重载。实际上,== 总是与equals相同,!=总是与equals相反。因此独立的类可以通过重载equals方法 修改==!=的意义。

根类Any有两个子类:AnyValAnyRef

值类型(AnyVal)

AnyVal是Scala里每个内建值类型的父类。有九个这样的值类型:ByteShortCharIntLongFloatDoubleBooleanUnit。其中的前八个对应到 Java的原始类型,它们的值在运行时表示成Java的原始值。

Scala里这些类的实例都写成字面量,不能使用new创造这些类的实例。值类都被定义为 即是抽象的又是final的,强制贯彻。因此如果你写了new就会出错:

  scala> new Int
  <console>:5: error: class Int is abstract; cannot be 
  instantiated
         new Int
         ^

另一个值类型Unit大约对应于Java的void类型;被用作不返回任何有趣结果的方法的 结果类型。Unit只有一个实例值,被写作()

值类型支持作为方法的通用的数学和布尔操作符。例如,Int有名为+*的方法, Boolean有名为||&&的方法。值类型也从类Any继承所有的方法:

  scala> 42 max 43
  res4: Int = 43

  scala> 42 min 43
  res5: Int = 42

  scala> 1 until 5
  res6: Range = Range(1, 2, 3, 4)

  scala> 1 to 5
  res7: Range.Inclusive = Range(1, 2, 3, 4, 5)

  scala> 3.abs
  res8: Int = 3

  scala> (-3).abs
  res9: Int = 3

值类型的空间是扁平的;所有的值类型都是scala.AnyVal的子类型,但是它们不是互相的 子类。代之以它们不同的值类型之间可以隐式地互相转换。例如,需要的时候,类 scala.Int的实例可以自动放宽(通过隐式转换)到类scala.Long的实例。

隐式转换还用来为值类型添加更多的功能。例如,类型Int支持以下所有的操作:

  scala> 42 max 43
  res4: Int = 43

  scala> 42 min 43
  res5: Int = 42

  scala> 1 until 5
  res6: Range = Range(1, 2, 3, 4)

  scala> 1 to 5
  res7: Range.Inclusive = Range(1, 2, 3, 4, 5)

  scala> 3.abs
  res8: Int = 3

  scala> (-3).abs
  res9: Int = 3

工作原理:

方法minmaxuntiltoabs都定义在类scala.runtime.RichInt里,并且 有一个从类IntRichInt的隐式转换。当你在Int上调用没有定义在Int上但定义在 RichInt上的方法时,这个转换就被应用了:

引用类型(AnyRef)

Any的另一个子类是类AnyRef。这个是Scala里所有引用类的基类。正如前面提到的 ,在Java平台上AnyRef实际就是类java.lang.Object的别名。因此Java里写的类和 Scala里写的都继承自AnyRef

存在AnyRef别名代替使用java.lang.Object名称的理由是,Scala被设计成可以同时 工作在Java和.Net平台。在.NET平台上,AnyRefSystem.Object的别名。

可以认为java.lang.Object是Java平台上实现AnyRef的方式。因此,尽管你可以在Java 平台上的Scala程序里交换使用ObjectAnyRef,推荐的风格是在任何地方都只使用 AnyRef

Scala类与Java类不同在于它们还继承自一个名为ScalaObject的特别的记号特质。理念是 ScalaObject包含了Scala编译器定义和实现的方法,目的是让Scala程序的执行更有效。 到现在为止,Scala实例包含了单个方法,名为$tag,用于内部以提速模式匹配。

原始类型是如何实现的

Scala以与Java同样的方式存储整数:把它当作32位的字。这对在JVM上的效率以及与Java库 的互操作性方面来说都很重要。标准的操作如加法或乘法都被实现为原始操作。然而,当 整数需要被当作(Java)对象看待的时候,Scala使用了「备份」类java.lang.Integer。 如在整数上调用toString方法或者把整数赋值给Any类型的变量时,就会这么做。

所有这些听上去都近似Java5里的自动装箱并且它们的确很像。不过有一个关键差异,Scala 里的装箱比Java里的更少看见。尝试下面的Java代码:

  // This is Java
  boolean isEqual(int x, int y) {
    return x == y;
  }
  System.out.println(isEqual(421, 421));

当然会得到true。现在,把isEqual的参数类型变为java.lang.Integer(或Object ,结果都一样):

  // This is Java
  boolean isEqual(Integer x, Integer y) {
    return x == y;
  }
  System.out.println(isEqual(421, 421));

却得到了false!原因是数421被装箱了两次,因此参数xy是两个不同的实例。

因为在引用类型上==表示引用相等,而Integer是引用类型,所以结果是false。这是 展示了Java不是纯面向对象语言的一个方面。我们能清楚观察到原始类型和引用类型之间的 差别。

现在在Scala里尝试同样的实验:

  scala> def isEqual(x: Int, y: Int) = x == y
  isEqual: (Int,Int)Boolean

  scala> isEqual(421, 421)
  res10: Boolean = true

  scala> def isEqual(x: Any, y: Any) = x == y
  isEqual: (Any,Any)Boolean

  scala> isEqual(421, 421)
  res11: Boolean = true

实际上Scala里的相等操作==被设计为透明的参考类型代表的东西。对值类型来说,就是 自然的(数学或布尔)相等。对于引用类型,==被视为继承自Objectequals方法的 别名。这个方法被初始地定义为引用相等,但被许多子类重载实现它们种族的相等概念。 这也意味着Scala里你永远也不会落入Java知名的关于字串比较的陷阱。Scala里,字串比较 以其应有的方式工作:

  scala> val x = "abcd".substring(2)
  x: java.lang.String = cd

  scala> val y = "abcd".substring(2)
  y: java.lang.String = cd

  scala> x == y
  res12: Boolean = true

Java里,xy的比较结果将是false。程序员在这种情况应该用equals,不过它 容易被忘记。

然而,有些情况你需要使用引用相等代替用户定义的相等。

例如,某些时候效率是首要因素,你想要把某些类哈希合并(hash cons)然后通过引用 相等比较它们的实例(类实例的哈希合并是指把创建的所有实例缓存在弱集合中。然后, 一旦需要类的新实例,首先检查缓存。如果缓存中已经有一个元素等于你打算创建的,你 可以重用存在的实例。这样安排的结果是,任何以equals()判断相等的两个实例同样在 引用相等上判断一致。)。

为这种情况,类AnyRef定义了附加的eq方法,它不能被重载并且实现为引用相等(也就 是说,它表现得就像Java里对于引用类型的==那样)。同样也有一个eq的反义词,被 称为ne。例如:

  scala> val x = new String("abc")
  x: java.lang.String = abc

  scala> val y = new String("abc")
  y: java.lang.String = abc

  scala> x == y
  res13: Boolean = true

  scala> x eq y
  res14: Boolean = false

  scala> x ne y
  res15: Boolean = true

底层类型

层级的底部你看到了两个类scala.NullScala.Nothing。它们是用统一的方式处理 某些Scala的面向对象类型系统的「边界情况」的特殊类型。

Null

Null唯一的实例是null值;它是每个引用类(就是说,每个继承自AnyRef的类)的 子类。Null不兼容值类型。比方说,不可把null值赋给整数变量:

  scala> val i: Int = null
  <console>:4: error: type mismatch;
   found   : Null(null)
   required: Int

Nothing

类型NothingScala的类层级的最底端;它是任何其它类型的子类型。然而,根本没有 这个类型的任何值。要一个没有值的类型有什么意思呢?在控制结构的try-catch中讨论过 ,Nothing的一个用处是它标明了不正常的终止。例如Scala的标准库中的Predef单例 对象有一个error方法,如下定义:

  def error(message: String): Nothing =
    throw new RuntimeException(message)

error的返回类型是Nothing,告诉用户方法不是正常返回的(代之以抛出了异常)。 因为Nothing是任何其它类型的子类,你可以非常灵活的使用像error这样的方法。 例如:


  def divide(x: Int, y: Int): Int = 
    if (y != 0) x / y 
    else error("can't divide by zero")

if状态分支,x / y,类型为Int,而else分支,调用了error,类型为Nothing。 因为NothingInt的子类型,整个状态语句的类型是Int,正如需要的那样。

Unit

Unit类似于Java中的voidUnit只有一个实例()