Scala学习(一):Scala中的函数式对象

来源:互联网 发布:医疗软件上市公司 编辑:程序博客网 时间:2024/05/17 08:39

目录

      • 目录
  • Scala中的函数式对象
    • 一Scala中的类
    • 二重写toString方法
    • 三先决条件检查
    • 四this关键字
    • 五从构造器
    • 六私有字段和方法
    • 七Scala中的操作符和标识符
    • 八方法重载

Scala中的函数式对象

本节通过一个例子来学习函数式对象。首先需要定义函数式对象,也就是没有任何可变状态的对象的类。

分数:rational number 是一种可以表达为比率n/d的数字。这里的 n 和 d 是数字,其中d不能为零。 n 被称作是分子:numerator,d 被称作是分母: denominator。本章我们将要设计的类必须模型化分数的行为,包括允许它们执行加,减,乘还有除运算。

数学上,分数不具有可变的状态。一个分数加到另外一个分数上,产生的结果是一个新的分数。而原来的数不会被“改变”。我们将在本章设计的不可变的 Rational 类将秉承这一属性。 每个分数将都被表示成一个 Rational 对象。当两个 Rational 对象相加时,一个新的带着累加结果的 Rational 对象将被创建出来。

一、Scala中的类

创建一个Scala类的语法为:

class Rational(n: Int, d: Int)

如果类没有主体,可以省略大括号(当然也可以加上)。其中n和d被称为类参数。Scala编译器会收集这两个参数并创建一个带同样的两个参数的主构造器

注意:Java中的类可以定义带有参数的构造器,而Scala中的类可以直接带参数,Scala的写法更为简介——类参数可以直接在类的主体中使用。

Scala编译器将把你放在类内部的任何不是字段的部分或者方法定义的代码,编译进主构造器。例如,你可以像这样打印输出一条消息:

class Rational(n: Int, d: Int) {    println("Created "+n+"/"+d)}

根据这个代码,Scala 编译器将把 println 调用放在 Rational 的主构造器。因此每次创建一个新的 Rational 实例时都会打印这条信息:

scala> new Rational(1, 2)Created 1/2res0: Rational = Rational@a0b0f5

二、重写toString方法

缺省情况下,Rational类继承了定义在java.lang.Object 类上的 toString 实现,只是打印类名,一个@符号和一个十六进制数。
更有用的 toString 实现应该打印出 Rational 的分子和分母。可以通过在Rational 类里增加 toString 方法的方式重载override 缺省的实现,如:

class Rational(n: Int, d: Int) {    override def toString = n +"/"+ d}

方法定义前的 override 修饰符标示了之前的方法定义被重载
在解释器中测试Rational可以得到:

scala> val x = new Rational(1, 3)x: Rational = 1/3scala> val y = new Rational(5, 7)y: Rational = 5/7

三、先决条件检查

因为分数的分母不能为零。但是目前的主构造器可以把0传递给分母。

面向对象编程的一个优点就是它允许你把数据封装在对象之内以便于你确保数据在整个生命周期中是有效的。像 Rational 这样的不可变对象,这就意味着你必须确保在对象创建的时候数据是有效的(并且,确保对象的确是不可变的,这样数据就不会在之后变成无效的状态)。由于零做分母对 Rational 来说是无效状态,因此在把零传递给 d 的时候,务必不能让 Rational 被构建出来。

最好的解决方法就是为主构造器定义一个先决条件说明分母d必须为非零的值。一种方式是使用require1方法。

先决条件:先决条件是对传递给方法或构造器的值的限制,是调用者必须满足的需求。

require 方法带一个布尔型参数。如果传入的值为真, require 将正常返回。反之, require将通过抛出 IllegalArgumentException 来阻止对象被构造。

class Rational(n: Int, d: Int) {    require(d != 0)    override def toString = n +"/"+ d}

四、this关键字

关键字 this 指向当前执行方法被调用的对象实例,或者如果使用在构造器里的话,就是正被构建的对象实例。

五、从构造器

有时,一个类里需要多个构造器。Scala中主构造器之外的构造器被称为从构造器。比如在创建Rational实例时,对于分母为1的分数可以只写分子(new Rational(5)),这样就会更加方便。

这就需要给Rational 添加一个只带一个参数(分子)的从构造器并预先设定分母为 1。

class Rational(n: Int, d: Int) {    require(d != 0)    val numer: Int = n    val denom: Int = d    def this(n: Int) = this(n, 1)    override def toString = numer+"/"+denom    def add(that: Rational): Rational =        new Rational(            numer * that.denom + that.numer * denom,            denom * that.denom        )}

Scala 的从构造器开始于 def this(…)。 Rational 的从构造器主体几乎完全是调用主构造器,直接传递了它的唯一的参数, n,作为分子和 1 作为分母。输入下列代码到解释器里可以实际看到从构造器的效果:

scala> val y = new Rational(3)y: Rational = 3/1

Scala 里的每一个从构造器的第一个动作都是调用同一个类里面其他的构造器。换句话说就是,每个 Scala 类里的每个从构造器都是以“ this(…)”形式开头的。被调用的构造器既可以是主构造器(好像 Rational 这个例子),也可以是从文本上来看早于调用构造器的其它从构造器。这个规则的根本结果就是每一个 Scala 的构造器调用终将结束于对类的主构造器的调用。因此主构造器是类的唯一入口点

六、私有字段和方法

上一个版本的 Rational 里,我们只是分别用 n 初始化了 numer,用 d 初始化了 denom。结果, Rational 的分子和分母可能比它所需要的要大。例如分数
42/66 ,可以更约简化为相同的最简形式,7/11 ,但 Rational 的主构造器当前并不做这个工作。

要想对分数进行约简化,需要把分子和分母都除以最大公约数: greatest common divisor。如: 66 和 42 的最大公约数是 6。(另一种说法就是, 6 是能够除尽 66 和 42 的最大的整数。)
42/66 的分子和分母都除以 6 就产生它的最简形式,7/11。 以下代码展示了如何做到这点:

class Rational(n: Int, d: Int) {    require(d != 0)    private val g = gcd(n.abs, d.abs)    val numer = n / g    val denom = d / g    def this(n: Int) = this(n, 1)    def add(that: Rational): Rational =    new Rational(        numer * that.denom + that.numer * denom,        denom * that.denom    )    override def toString = numer+"/"+denom    private def gcd(a: Int, b: Int): Int =    if (b == 0) a else gcd(b, a % b)}

这个版本的 Rational 里,我们添加了私有字段, g,并修改了 numer 和 denom 的初始化器(初始化器: initializer 是初始化变量,例如初始化 numer 的“ n / g”,的代码)。因为g 是私有的, 它只能在类的主体之内,而不能在外部被访问。我们还添加了一个私有方法,gcd,用来计算传入的两个 Int 的最大公约数。比方说, gcd(12, 8)是 4。 正如你在 4.1节中看到的, 想让一个字段或方法私有化你只要把 private 关键字放在定义的前面。私有
的“助手方法” gcd 的目的是把类的其它部分,这里是主构造器,需要的代码分离出来。
为了确保 g 始终是正的,我们传入 n 和 d 的绝对值,调用 abs 即可获得任意整数的绝对值。
Scala编译器将把 Rational的三个字段的初始化代码依照它们在源代码中出现的次序放入
主构造器。所以 g 的初始化代码, gcd(n.abs, d.abs),将在另外两个之前执行, 因为它
在源文件中出现得最早。 g 将被初始化为类参数, n 和 d,的绝对值的最大公约数。然后再
被用于 numer 和 denom 的初始化。 通过把 n 和 d 整除它们的最大公约数, g,每个 Rational
都将被构造成它的最简形式:

scala> new Rational(66, 42)res24: Rational = 11/7

七、Scala中的操作符和标识符

关于Scala中的操作符具体见《Scala编程》一书中的相关章节。

Scala中的标识符有四种:

  1. 字母数字标识符:起始于一个字母或下划线,之后可以跟字母,数字,或下划线。美元(dollor)字符也被当作是字母,但是被保留作为 Scala 编译器产生的标识符之用。用户程序里的标识符不应该包含美元(dollor)字符,尽管能够编译通过; 但是这样做有可能导致与 Scala 编译器产生的标识符发生名称冲撞。
  2. 操作符标识符:由一个或多个操作符字符组成。操作符字符是一些如+, :, ?, ~或#的可打印的ASCII字符。
  3. 混合标识符: 由字母数字组成,后面跟着下划线和一个操作符标识符。例如, unary_+被用做定义一元的‘ +’操作符的方法名。或者, myvar_=被用做定义赋值操作符的方法名。
  4. 文本标识符:是用反引号`…`包括的任意字串。 如:
    `x` `` `yield`
    它的思路是你可以把任何运行时认可的字串放在反引号之间当作标识符。结果总是Scala标识符。即使包含在反引号间的名称是 Scala 保留字,这个规则也是有效的。在 Java 的Thread 类中访问静态的 yield 方法是其典型的用例。你不能写 Thread.yield()因为 yield是 Scala 的保留字。然而,你仍可以在反引号里引用方法的名称,例如 Thread.`yield`()。

八、方法重载

Scala 分辨重载方法的过程与 Java 极为相似。任何情况下,被选中的重载版本都是最符合参数静态类型的那个。有时如果不止一个最符合的版本;这种情况下编译器会给你一个“参考模糊”的错误。

回到我们的例子上,目前你不能把一个分数和一个整数乘在一起,因为‘ *’
的操作数只能是分数。所以对于分数 r 你不能写 r * 2。而必须写成 r * new Rational(2),这样看上去不漂亮。为了让 Rational 用起来更方便,可以在类上增加能够执行分数和整数之间的加法和乘法的新方法。既然已经到这里了, 还可以再加上减法和除法。结果展示在以下代码中:

class Rational(n: Int, d: Int) {    require(d != 0)    private val g = gcd(n.abs, d.abs)    val numer = n / g    val denom = d / g    def this(n: Int) = this(n, 1)    def +(that: Rational): Rational =    new Rational(        numer * that.denom + that.numer * denom,        denom * that.denom    )    def +(i: Int): Rational =        new Rational(numer + i * denom, denom)    def -(that: Rational): Rational =    new Rational(        numer * that.denom - that.numer * denom,        denom * that.denom    )    def -(i: Int): Rational =        new Rational(numer - i* denom, denom)    def *(that: Rational): Rational =        new Rational(numer * that.numer, denom * that.denom)    def *(i: Int): Rational =        new Rational(numer * i, denom)    def /(that: Rational): Rational =        new Rational(numer * that.denom, denom * that.numer)    def /(i: Int): Rational =        new Rational(numer, denom * i)    override def toString = numer+"/"+denom    private def gcd(a: Int, b: Int): Int =    if (b == 0) a else gcd(b, a % b)}

现在每种数学方法都有两个版本了: 一个带分数做参数,另一个带整数。或者可以说,这些方法名都被重载: overload 了,因为每个名字现在都被多个方法使用。例如, +这个名字被一个带 Rational 的和另一个带 Int 的方法使用。方法调用里,编译器会拣出正确地匹配了参数类型的重载方法版本。例如,如果 x.+(y)的参数 y 是 Rational,编译器就会拣带有 Rational 参数的+方法来用。相反如果参数是整数,编译器就会拣带有 Int 参数的+方法做替代。 如果你尝试输入:

scala> val x = new Rational(2, 3)x: Rational = 2/3scala> x * xres37: Rational = 4/9scala> x * 2res38: Rational = 4/3

你会看到*方法的调用取决于每个例子里面右侧操作数的类型。

九、隐式转换
现在你能写 r * 2 了,或许你想交换操作数,就像 2 * r 这样。不幸的是这样做还不可以:

scala> 2 * r<console>:7: error: overloaded method value * with alternatives(Double)Double <and> (Float)Float <and> (Long)Long <and> (Int)Int<and> (Char)Int <and> (Short)Int <and> (Byte)Int cannot beapplied to (Rational)val res2 = 2 * r             ˆ

这里的问题是 2 * r 等同于 2.*(r),因此这是在整数 2 上的方法调用。但 Int 类没有带Rational 参数的乘法——没办法,因为类 Rational 不是 Scala 库的标准类。
然而, Scala 里有另外一种方法解决这个问题:你可以创建一个在需要的时候能自动把整数转换为分数的隐式转换。试着把这行代码加入到解释器:

scala> implicit def intToRational(x: Int) = new Rational(x)

这行代码定义了从 Int 到 Rational 的转换方法。方法前面的 implicit 修饰符告诉编译器若干情况下自动调用它。定义了转换之后,你现在可以重试之前失败的例子了:

scala> val r = new Rational(2,3)r: Rational = 2/3scala> 2 * rres0: Rational = 4/3

请注意隐式转换要起作用,需要定义在作用范围之内。如果你把隐式方法定义放在类Rational 之内,它就不在解释器的作用范围。 现在,你要在解释器内直接定义它,之后会继续学习更多关于隐式转换的细节。


  1. require方法:定义在scala包里的Predef对象上。参考资料:http://www.jianshu.com/p/69675d0139aa ↩