【转】Scala学习——特质

原文链接 http://nerd-is.in/2013-08/scala-learning-traits/

原文发表于http://nerd-is.in/2013-08/scala-learning-traits/

Scala特质可以给出特质的缺省实现

不支持多重继承

Scala也还是不支持多重继承。

如果几个类有某些共通的方法或者字段,那么从它们多重继承时,

就会出现麻烦。所以Java被设计成不支持多重继承,但可实现任意多的接口。

接口只能包含抽象方法,不能包含字段。

而Scala中的特质,可以同时拥有抽象方法和具体方法,类可以实现多个特质。

当做接口使用的特质

1

2

3

4

5

6

7

8

9

10

trait Logger {

def log(msg: String)

}

class ConsoleLogger extends Logger {

def log(msg: String) { println(msg) }

}

// 使用with添加额外的特质

class ConsoleLogger extends Logger with Cloneable with Serializable

所有的Java接口都可以作为Scala特质来使用。

与Java一样,Scala类只能有一个超类,可以有任意数量的特质。

在解读时,Logger with Cloneable with Serializable先被当成一个整体,然后类extends这个整体。

带有具体实现的特质

特质中的方法不需要一定是抽象的,可以有具体的实现。

1

2

3

4

5

6

7

8

9

10

11

trait ConsoleLogger {

def log(msg: String) { println(msg) }

}

class SavingsAccount extends Account with ConsoleLogger {

def withdraw(amount: Double) {

if (amount > balance) log("Insufficient funds")

else balance -= balance

}

...

}

在Scala中,我们说ConsoleLogger的功能被混入了SavingsAccount类。

特质拥有具体行为的一个弊端是,当特质改变时,所有混入了该特质的类都必须重新编译。

带有特质的对象

在构造对象时,可以混入该对象所具有的特质的子类型。

那么在调用这个对象所具有的特质方法时,将会执行子类型的方法。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

trait Logged {

def log(msg: String) { }

}

class SavingsAccount extends Account with Logged {

def withdraw(amount: Double) {

if (amount > balance) log("Insufficient funds")

else ...

}

...

}

trait ConsoleLogger extends Logged {

override def log(msg: String) { println(msg) }

}

val acct = new SavingsAccount with ConsoleLogger

// 当调用acct的log方法时,执行的是ConsoleLogger特质的log方法

叠加在一起的特质

可以为类或对象添加多个相互调用的特质,调用将会从最后一个特质开始。

这个功能对需要分阶段加工处理某个值的场景很有用。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

// 给日志消息加上时间

trait TimestampLogger extends Logged {

override def log (msg: String) {

super.log(new java.util.Date() + " " + msg)

}

}

// 截断过长的的日志

trait ShortLogger extends Logged {

val maxLength = 15

override def log(msg: String) {

super.log(

if (msg.length <= maxLength) msg

else msg.substring(0, maxLength - 3) + "...")

}

}

val acct1 = new SavingsAccount with ConsoleLogger with

TimestampLogger with ShortLogger

val acct2 = new SavingsAccount with ConsoleLogger with

ShortLogger with TimestampLogger

上面定义的两个特质中,都调用了super.log。这个super.log与类中的含义并不一样,

不是调用了父级的方法,而是调用了特质层级中的下一个特质。具体是调用了哪一个特质,

是根据特质的添加顺序来决定的。一般来说,特质从最后一个开始被处理。

后面会说明当特质的顺序不是简单的链而是任意形态的树/图时的细节(在这里书中用的形容词是血淋淋…)。

如果从acct1取款,首先调用的ShortLogger的log方法,由于方法中调用了super.log,

于是又去调用下一个特质的log方法(也就是TimestampLogger的log)。

于是输出结果会是完整的时间,以及不完整的msg(被ShortLogger截断了)。

那么如果从acct2取款,顺序将会不一样,最终输出的结果会是不完整的时间。

如果要控制特定特质的方法被调用,可以在方括号中给出特质或类的名称。

这里给出的名称,必须是直接的超类型。 super[ConsoleLogger].log(...)

在特质中重写抽象方法

1

2

3

trait Logger {

def log(msg: String)

}

现Logger特质中有一个抽象方法log,如果我们用原来的TimestampLogger特质,会无法编译。

因为Logger的log方法没有具体的实现,Scala会认为TimestampLogger依旧是抽象的。

于是我们需要加上abstract和override关键字:

1

2

3

4

5

trait TimestampLogger extends Logger {

abstract override def log(msg: String) {

super.log(new java.util.Date() + " " + msg)

}

}

当做富接口使用的特质

在Scala中,在特质中混用具体方法和抽象方法十分普遍;

而Java中需要声明一个接口和一个额外的扩展该接口的类。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

trait Logger {

def log(msg: String)

def info(msg: String) { Log("INFO: " + msg) }

def warn(msg: String) { Log("WARN: " + msg) }

def severe(msg: String) { log("SERVERE: " + msg) }

}

class SavingsAccount extends Account with Logger {

def withdraw(amount: Double) {

if (amount > balance) severe("Insufficient funds")

else ...

}

...

override def log(msg: String) { println(msg) }

}

特质中的具体字段

特质中的字段可以是具体的,也可以是抽象的。如果给出了初始值,字段就是具体的。

混入了该特质的类会自动获得特质的具体字段,但这些字段不是被继承的,

而是被简单加入到了子类中。这之间是有差别的。

从特质中加入的具体字段,在内存的模型上,是不属于继承的超类对象的,

是属于子类本身的。表述不太清楚,不过看书都应该能明白。

特质中的抽象字段

特质的抽象字段必须在具体的子类中进行重写。

特质构造顺序

特质也可以有构造器,由字段的初始化和其他特质体中的语句构成。

这些语句在任何混入该特质的对象在构造是都会被执行。

构造器的执行顺序:

  • 调用超类的构造器;
  • 特质构造器在超类构造器之后、类构造器之前执行;
  • 特质由左到右被构造;
  • 每个特质当中,父特质先被构造;
  • 如果多个特质共有一个父特质,父特质不会被重复构造
  • 所有特质被构造完毕,子类被构造。

构造器的顺序是类的线性化的反向。线性化是描述某个类型的所有超类型的一种技术规格。

初始化特质中的字段

特质不能有构造器参数,每个特质有一个无参数的构造器。

对于需要某种定制才有用的特质来说,这个局限是一个问题。

用文件日志生成器来说明,我们需要在使用特质时指定日志文件,但是特质不能使用构造参数。

可以考虑使用抽象字段来存放文件名:

1

2

3

4

5

6

7

8

9

trait  FileLogger extends Logger {

val filename: String

val out = new PrintStream(filename)

def log(msg: String) { out.println(msg); out.flush() }

}

val acct = new SavingsAccount with FileLogger {

val filename = "myapp.log"

}

但是这样却是行不通的。问题来自于构造顺序。FileLogger的构造器会先于子类构造器执行,

这里的子类是一个扩展了SavingsAccount且混入了FileLogger的匿名类实例。

在构造FileLogger时,就会抛出一个空指针异常,子类的构造器根本就不会执行。

这个问题的解决方法之一是使用提前定义这个语法(就是前面章节里提过的难看到家了的那个)。

1

2

3

4

5

6

7

8

9

10

11

// 使用提前定义,请注意这里的奇怪语法

val acct = new {

val filename = "myapp.log"

} with SavingsAccount with FileLogger

// 在类中使用提前定义

class SavingsAccount extends {

  val filename = "myapp.log"

} with Account with FileLogger {

...

}

另外一个方法是使用懒值:

1

2

3

4

5

trait FileLogger extends Logger {

val filename: String

lazy val out = new PrintStream(filename)

def log(msg: String) { out.println(msg) }

}

因为懒值在初次使用是才被初始化,所以out字段不会再抛出空指针异常。

在使用out字段时,filename也已经初始化了。

但是使用懒值不高效。

扩展类的特质

特质可以扩展类,这个类会自动成为所有混入该特质的类的超类。

1

2

3

4

5

6

7

trait LoggedException extends Exception with Logged {

def log() { log(getMessage()) }

}

class UnhappyException extends LoggedException {

override def getMessage() = "arggh!"

}

特质的超类Exception自动成为了混入了LoggedException特质的UnhappyException的超类。

Scala并不允许多继承。那么这样一来,如果UnhappyException原先已经扩展了一个类了该如何处理?

只要已经扩展的类是特质超类的一个子类就可以。

1

2

class UnhappyException extends IOException with LoggedException  // 可行

class UnhappyFrame extends JFrame with LoggedException  // 不可行

自身类型(L2)

当特质扩展类时,编译器能确保所有混入该特质的类都将这个被特质扩展的类作为超类。

除了这一个手段,Scala还可以用自身类型(self type)来确保这一点。

如果特质以 this: type =>开始定义(像在嵌套类一节中使用的用来指定外部类别名的语法),那么这个特质就只能被混入type指定的类型的子类。

1

2

3

4

trait LoggedException extends Logged {

this: Exception =>

def log() { log(getMessage()) }

}

这里的特质LoggedException并不扩展Exception类,而是自身拥有Exception类型,

意味着该特质只能被混入Exception的子类。

这样指定了自身类型之后,调用自身类型的方法就合法了(这里调用了Exception类的getMessage方法)。

相比而言,扩展类的特质和拥有自身类型的特质这两者很相似,

但某些情况下自身类型的写法比扩展类版本更灵活。自身类型可以解决特质间的循环依赖。

自身类型还可以处理结构类型(structural type)——这种类型只给出了类必须拥有的方法,而不是类的名称。

1

2

3

4

trait LoggedException extends Logged {

this: ( def getMessage(): String) =>

def log() { log(getMessage()) }

}

自身类型和结构类型会在书的18章详细介绍。

背后发生的

了解是如何将特质翻译成JVM的类和接口,有助于理解特质。

如果特质只有抽象方法,特质被转变成一个Java接口。

如果特质具有具体方法,Scala会创建一个伴生类,伴生类用静态方法存放特质的方法。

特质中的字段对应到接口中的抽象getter/setter,

当某个类实现特质时,字段被自动加入。

1

2

3

4

5

6

7

8

9

10

11

trait ShortLogger extends Logger {

val maxLength = 15

...

}

// 翻译成

public interface ShortLogger extends Logger {

public abstract int maxLength();

public abstract void weird_prefix$maxLength_$eq(int);

...

}

以weird开头的方法将会在伴生类的初始化方法$init$内被用来初始化字段。



这是《快学Scala》的第10章,内容是跟面向对象比较相关的特质,

但是内容已经不是随便学学玩玩的了。学Scala果然还是挺有挑战性的,

怪不得时常见到说Scala不好学太学术之类的论调。后面还有不少内容,

而且还包括自己以前没接触过的函数式编程范式,走一步看一步吧。

很多细节现在学了也不知道会在什么地方实用到,这么想来,

果然要了解设计的目的是很重要的,至少会明白适用在哪里。



本章练习参考

1.

1

2

3

4

5

trait RectangleLike {

this: java.awt.geom.Ellipse2D.Double =>

def translate(dx: Int, dy: Int) { this.x += dx; this.y += dy }

def grow(h: Int, v: Int) { this.height += h; this.width += v }

}

2.

1

2

3

4

5

6

7

8

class OrderedPoint extends java.awt.OrderedPoint

with scala.math.Ordered[java.awt.OrderedPoint] {

def compare(that: java.awt.OrderedPoint) = {

if ((this.x < that.x) || (this.x == that.x && this.y < that.y)) -1

else if (this.x == that.x && this.y == that.y) 0

else 1

}

}

最后几道练习不太容易,我觉得已经需要去看Java源码了。现在不在状态,放着吧。

时间: 2024-10-12 23:51:37

【转】Scala学习——特质的相关文章

【Scala】特质与特质的线性化

特质 Scala里相当于Java接口的是Trait(特征).实际上它比接口还功能强大.与接口不同的是,它还可以定义属性和方法的实现.Scala中特征被用于服务于单一目的功能模块的模块化中.通过混合这种特征(模块)群来实现各种应用程序的功能要求,Scala也是按照这个构想来设计的. 特质的构造顺序 特质也可以有构造器,由字段的初始化和其他特质体中的语句构成.这些语句在任何混入该特质的对象在构造时都会被执行. 构造器的执行顺序: 调用超类的构造器: 特质构造器在超类构造器之后.类构造器之前执行: 特

Scala学习笔记及与Java不同之处总结-从Java开发者角度

Scala与Java具有很多相似之处,但又有很多不同.这里主要从一个Java开发者的角度,总结在使用Scala的过程中所面临的一些思维转变. 这里仅仅是总结了部分两种语言在开发过程中的不同,以后会陆续更新一些切换后在开发过程中值得注意的地方.以下列举了部分,但令人印象深刻的Scala语言的不同之处,具体的代码演示样例及具体阐述见下文. ? Scala中可直接调用Java代码,与Java无缝连接. 语句能够不用";"结束.且推荐不适用";". 变量声明时以var或va

Scala学习笔记-环境搭建以及简单语法

关于环境的搭建,去官网下载JDK8和Scala的IDE就可以了,Scala的IDE是基于Eclipse的. 下面直接上代码: 这是项目目录: A是scala写的: package first import scala.collection.mutable.ListBuffer object A { def main(args: Array[String]) { print("Hello,Scala");//学习程序设计的第一句 println("---");//pr

Spark之Scala学习

1. Scala集合学习: http://blog.csdn.net/lyrebing/article/details/20362227 2. scala实现kmeans算法 http://www.thinksaas.cn/group/topic/93852/ 3. Spark之Scala学习网站 http://spark.apache.org/docs/latest/mllib-decision-tree.html 4. Spark wordcount开发并提交到集群运行: http://ww

scala学习(一)

开始学习scala,有一种学习java的感觉. 首先,从网站下载scala的包,下载后要安装,安装后把安装目录的bin放到环境变量psth里 cmd里,输入scala,如果出现下图,那么恭喜咯,安装成功咯~~ 1:变量 首先我们看下定义变量方法 var 可以定义变量,可以改变值 val 定义的变量不可以改变值,类似于java的final变量: 2:函数 如图定义了一个函数 def max(x:Int,y:Int):Int ={ if(x>y)x else y } def:表面定义函数 max :

scala学习(二)

接着上次的学习,今天学习scala的下面内容咯~~ 1·使用集(set)和映射(map) 学习过java的童鞋们,看见这两个肯定很开心咯,因为很眼熟哦. scala的集合,分为可变类型和不可变类型.array--可变:list保持不变 那么set和map呢,他们通过类继承的差别控制可变和不可变~~ 先看个set的列子吧: var jetSet = Set("zhangsan","lisi") jetSet += "wangwu" println(

scala学习三---文件里读取文本行

学习了scala的基本知识后,发现了scala是集函数式和指令式结合为一体的一种语言,代码更加简洁,但是对于用习惯了java的人来说,还真的不是一件易事~~ 今天学习scala脚本读取文本文件 列子如下: import scala.io.Source if(args.length>0){ for(line <- Source.fromFile(args(0)).getLines) print(line.length+" "+line) }else{ Console.err.

Scala学习网址

scala学习网址为:https://twitter.github.io/scala_school/zh_cn https://www.zhihu.com/question/26707124

Scala 学习

1,Scala学习 官方网网站: http://www.scala-lang.org/ http://www.scala-lang.org/download/ 可伸缩的语言是一种多范式的编程语言,一种类似java的编程,设计初衷是要集成面向对象编程和函数式编程的各种特性. Scala是在JVM上运行. Scala有几项关键特性表明了它的面向对象的本质.例如,Scala中的每个值都是一个对象,包括基本数据类型(即布尔值.数字等)在内,连函数也是对象.另外,类可以被子类化,而且Scala还提供了基于