Rational 的式样书

分数:rational number 是一种可以表达为比率 d n 的数字,这里的 n 和 d 是数字,其中 d 不能为零。n 被称作是分子:numerator,d 被称作是分母:denominator。分数的例子 有: 2/1 , 3/2 , 239/112 和 1/2 。

本章我们将要设计的类必须模型化数的行为,包括允许它们执行加,减,乘还有除运算。

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

创建 Rational

class Rational (n:Int,d:Int)

这行代码里首先应当注意到的是如果类没有主体,就不需要指定一对空的大括号。在类名Rational之后括号里的 n 和 d,被称为类参数:class parameter。Scala 编译器会收集这两个类参数并创造一个带同样的两个参数的主构造器: primary constructor。

注意 这个最初的 Rational 例子凸显了 Java 和 Scala 之间的不同。Java 类具有可以带参数的构造器, 而 Scala 类可以直接带参数。Scala 的写法更简洁——类参数可以直接在类的主体中使用;没必要定义字 段然后写赋值函数把构造器的参数复制到字段里。这可以潜在地节省很多固定写法,尤其是对小类来说。

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

class Rational (n:Int,d:Int){
println(n+"/"+d)
} def main(args: Array[String]): Unit = { val r = new Rational(1,2);
}

打印出:

1/2

重新实现 toString 方法

更有用的 toString 实现应该打印出 Rational 的分子和分母。你可以通过在 Rational 类里增加 toString 方法的方式重载:override 缺省的实现,如

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

方法定义前的 override 修饰符标示了之前的方法定义被重载;

检查先决条件

由于零做分母对 Rational 来说是无效状态,因此在把零传递给 d 的时候,务必 不能让 Rational 被构建出来。 解决这个问题的最好办法是为主构造器定义一个先决条件:precondition说明d必须为非零 值。先决条件是对传递给方法或构造器的值的限制,是调用者必须满足的需求。一种方式 是使用require方法:

class Rational (n:Int,d:Int){

  require(d != 0,"d should not be zero")
override def toString: String = n+"/"+d
}
def main(args: Array[String]): Unit = {

  val r = new Rational(1,0);
}

打印输出如下:

Exception in thread "main" java.lang.IllegalArgumentException: requirement failed: d should not be zero
 at scala.Predef$.require(Predef.scala:224)
 at chapter6.Rational.<init>(Rational.scala:5)
 at chapter6.Main$.main(Main.scala:7)
 at chapter6.Main.main(Main.scala)

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

添加字段

我们将在类 Rational 上定义一个公开的 add 方法,它带另一个 Rational 做参数。为了保 持 Rational 不可变,add 方法必须不能把传入的分数加到自己身上。而是必须创建并返回 一个全新的带有累加值的 Rational。

class Rational (n:Int,d:Int){

  require(d != 0,"d should not be zero")
val numer: Int = n
val denom: Int = d def add(that: Rational): Rational =
new Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
) override def toString: String = numer+"/"+denom
}

我们增加了两个字段,分别是numer和denom,并用类参数n和d初始化它们(这个初始化会在构造函数中完成。那么这就引出了一个新的问题:number和denom是定义在构造方法中的局部变量,在构造方法之外还可以引用它们么?是可以的,这与Java不同)。我们还改变了toString和add的实现,让它们使用字段,而不是类参数。

注意:通过that无法引用到方法参数n和d,在类的外部也无法直接引用到这俩参数。

  def main(args: Array[String]): Unit = {

    val r = new Rational(1,2);
val r2 = new Rational(2,3);
val rational: Rational = r.add(r2)
println(rational)
}

打印结果如下:

7/6

自指向

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

lessThan来测试给定的分数是否小于传入的参数:

def lessThan(that: Rational):Boolean =
this.numer * that.denom < that.numer * this.denom

max 方法返回指定分数和参数中 的较大者:

def max(that: Rational):Rational =
if (this.lessThan(that)) that else this

这里,第一个 this 是冗余的,你写成(lessThan(that))也是一样的。但第二个 this 表示 了当测试为假的时候的方法的结果;如果你省略它,就什么都返回不了了。

从构造器

有些时候一个类里需要多个构造器。Scala 里主构造器之外的构造器被称为从构造器: auxiliary constructor。比方说,分母为 1 的分数只写分子的话就更为简洁。这就需要给 Rational 添加一个只带一个参数,分子,的 从构造器并预先设定分母为 1。

def this(n: Int) = this(n, 1)

测试如下:

  def main(args: Array[String]): Unit = {

    val r = new Rational(5)
println(r)

输出结果为:5/1

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

私有字段和方法

Rational 的分子和分母可能比它所需要的要大,例如分数 42/66 ,可以更约简化为相同 的最简形式, 7/11.要想对分数进行约简化,需要把分子和分母都除以最大公约数:greatest common divisor。

class Rational (n:Int,d:Int){

  require(d != 0,"d should not be zero")
private val g: Int = gcd(n,d)
val numer: Int = n/g
val denom: Int = 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
) private def gcd(a: Int, b: Int): Int =
if (b == 0) a else gcd(b, a % b) override def toString: String = numer+"/"+denom
}

这个版本的 Rational 里,我们添加了私有字段,g,并修改了 numer 和 denom 的初始化器 。因为 g 是私有的,它只能在类的主体之内,而不能在外部被访问。我们还添加了一个私有方法, gcd,用来计算传入的两个 Int 的最大公约数。

Scala编译器将把Rational的三个字段的初始化代码依照它们在源代码中出现的次序放入主构造器。所以 g 的初始化代码,gcd(n.abs, d.abs),将在另外两个之前执行,因为它 在源文件中出现得最早。g 将被初始化为类参数,n 和 d,的绝对值的最大公约数。然后再 被用于 numer 和 denom 的初始化。通过把 n 和 d 整除它们的最大公约数g,每个 Rational 都将被构造成它的最简形式.

def main(args: Array[String]): Unit = {

    val r2 = new Rational(11,33)
println(r2)
}

打印结果为:

1/3

定义操作符

我们可以用+定义方法名,你可以同样实现一个*方法以实现乘 法:

def +(that: Rational): Rational =
new Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
)
def *(that: Rational): Rational =
new Rational(numer * that.numer, denom * that.denom)

+号或*号可以作为方法名。方法体是一样的。

有了这种方式定义的 Rational 类,你现在可以这么写了:

  def main(args: Array[String]): Unit = {

    val r1 = new Rational(1,5)
val r2 = new Rational(11,33)
println(r1+r2)
println(r1*r2)
}

结果输出如下:

8/15
1/15

方法重载

为了让 Rational 用起来更方便,可以在类上增加能够执行分数和整数之 间的加法和乘法的新方法。既然已经到这里了,还可以再加上减法和除法。

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)

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

  def main(args: Array[String]): Unit = {

    val r1 = new Rational(1,5)
val r2 = new Rational(11,33)
println(r1+r2)
println(r1*3)
}

输出结果为:

8/15
3/5

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

隐式转换

现在你能写 r * 2 了,或许你想交换操作数,就像 2 * r 这样。这里的问题是 2 * r 等同于 2.*(r),因此这是在整数 2 上的方法调用。但 Int 类没有带 Rational 参数的乘法

然而,Scala 里有另外一种方法解决这个问题:你可以创建一个在需要的时候能自动把整 数转换为分数的隐式转换。试着把这行代码加入到解释器:

def main(args: Array[String]): Unit = {

    implicit def intToRational(x: Int) = new Rational(x)
val r1 = new Rational(1,5)
println(3*r1)
}

这行代码定义了从 Int 到 Rational 的转换方法。方法前面的 implicit 修饰符告诉编译器 若干情况下自动调用它。请注意隐式转换要起作用,需要定义在作用范围之内。如果你把隐式方法定义放在类 Rational 之内,它就不在解释器的作用范围。现在,你要在解释器内直接定义它。

运行结果如下:

3/5

scala编程(六)——函数式对象的更多相关文章

  1. Scala学习笔记——函数式对象

    用创建一个函数式对象(类Rational)的过程来说明 类Rational是一种表示有理数(Rational number)的类 package com.scala.first /** * Creat ...

  2. Scala 编程---类和对象

    类是对象的蓝图.一旦你定义了类,你就可以用关键字new从类的蓝图里创建对象.比方说,如果给出了类的定义: class ChecksumAccumulator { // class definition ...

  3. Scala编程入门---函数式编程之集合操作

    集合的函数式编程: 实战常用: //map案例实战:为List中的每个元素都添加一个前缀. List("leo","Jen","peter" ...

  4. Scala编程入门---函数式编程

    高阶函数 Scala中,由于函数时一等公民,因此可以直接将某个函数传入其他函数,作为参数.这个功能是极其强大的,也是Java这种面向对象的编程语言所不具备的. 接收其他函数作为函数参数的函数,也被称作 ...

  5. Scala编程--函数式对象

    本章的重点在于定义函数式对象,也就是说,没有任何可变状态的对象的类.作为运行的例子,我们将创造若干把分数作为不可变对象建模的类的变体.在这过程中,我们会展示给你Scala面向对象编程的更多方面:类参数 ...

  6. Scala函数与函数式编程

    函数是scala的重要组成部分, 本文将探讨scala中函数的应用. scala作为支持函数式编程的语言, scala可以将函数作为对象即所谓"函数是一等公民". 函数定义 sca ...

  7. (数据科学学习手札48)Scala中的函数式编程

    一.简介 Scala作为一门函数式编程与面向对象完美结合的语言,函数式编程部分也有其独到之处,本文就将针对Scala中关于函数式编程的一些常用基本内容进行介绍: 二.在Scala中定义函数 2.1 定 ...

  8. Scala 基础(5)—— 构建函数式对象

    有了 Scala 基础(4)—— 类和对象 的前提,现在就可以来构建一个基于 Scala 的函数式对象. 下面开始构造一个有理数对象 Rational. 1. 主构造方法和辅助构造方法 对于每一个类的 ...

  9. Scala 编程(二)类和对象

    类,字段和方法 类是对象的蓝图.一旦定义了类,就可以用关键字new从类的蓝图里创建对象,类的定义: class ChecksumAccumulator { // class definition go ...

  10. Scala学习(六)---Scala对象

    Scala中的对象 摘要: 在本篇中,你将会学到何时使用Scala的object语法结构.在你需要某个类的单个实例时,或者想为其他值或函数找一个可以挂靠的地方时,你就会用到它.本篇的要点包括: 1. ...

随机推荐

  1. POJ - 3279 Fliptile(反转---开关问题)

    题意:有一个M*N的网格,有黑有白,反转使全部变为白色,求最小反转步数情况下的每个格子的反转次数,若最小步数有多个,则输出字典序最小的情况.解不存在,输出IMPOSSIBLE. 分析: 1.枚举第一行 ...

  2. Kettle无法下载以及点击无反应的问题

    最开始用于解决MySQL转移数据到ORACLE的问题,尝试了几种方法. 1.直接从Mysql导出csv文件.这种方式最直接简单,但是问题是数据大的话,容易出现数据对不齐的情况,导入这个时候就会出现错误 ...

  3. 手机与Arduino蓝牙串口通讯实验及完整例程

    安卓手机与Arduino之间采用蓝牙串口通讯,是很多智能装置和互动装置常用的控制方法,简单而有效,无需网络环境,很实用的技术. 实验采用Arduino UNO板,加了一块1602LCD屏做显示(因为只 ...

  4. 日志处理中一些shell命令技巧

    日志处理中一些shell命令技巧 阴差阳错的做的日志分析,前途未卜的这段日子,唯一还有点意思的可能就是手动的处理大量日志.总结一下. 日志文件的输入是动则几个G的文本.从N个这样的文件中得到一个列表, ...

  5. VUE- 解决插件报错问题

    VUE- 解决插件报错问题 解决办法:先删除node_modules整个文件夹,然后cnpm cache clean,然后cnpm install. cnpm  cache clean时可能会提示 这 ...

  6. vivado下创建基本时序周期约束

    创建基本时钟周期约束.(验证我们的设计能否在期望的频率上运行) (学习记录,晚一点会做实验传上来的.) 时钟基本概念:https://blog.csdn.net/wordwarwordwar/arti ...

  7. 干货 | AI人脸识别之人脸搜索

    本文档将利用京东云AI SDK来实践人脸识别中的人脸搜索功能,主要涉及到分组创建/删除.分组列表获取.人脸创建/删除.人脸搜索,本次实操的最终效果是:创建一个人脸库,拿一张图片在人脸库中搜索出相似度最 ...

  8. 代码杂谈-python函数

    发现函数可以设置属性变量, 如下 newfunc.func , newfunc.args def partial(func, *args, **keywords): """ ...

  9. 吴裕雄--天生自然 JAVASCRIPT开发学习: 闭包

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  10. Java中的四种引用类型比较

    1.引用的概念 引用这个概念是与JAVA虚拟机的垃圾回收有关的,不同的引用类型对应不同的垃圾回收策略或时机. 垃圾收集可能是大家感到难于理解的较难的概念之一,因为它并不能总是毫无遗漏地解决Java运行 ...