类和对象

Table of Contents

1 定义类

class Name(arg1: String, arg2: String) {
……
}

1.1 构造方法

类方法外的代码会被 Scala 编译器放到构造函数中,不用像其他语言单独写构造函数:

class Hello {
  println("hello world")
}

上面的 println 函数会被放到构造函数中执行。

1.2 字段

class Hello(msg: String) {
  def say(): String = msg
}

val hello = new Hello("hello world")
println(hello.say)

上面代码里,msg 可以在方法里直接访问,等同于 Python 里需要做一个 self.msg = msg 语句,其实也可以用 this.msg 来引用,其中 this 表示对象自身。

但是,这样就会出问题:

class Hello(msg: String) {
  def say(): String = msg
  def say1(hello: Hello): String = hello.msg
}

运行会报错:

error: value msg is not a member of this.Hello
  def say1(hello: Hello): String = hello.msg

因为 say1 的 hello 参数没有对象引用,编译器不知道有 msg 这个字段。解决方法就是定义字段:

class Hello(msg: String) {
  val message: String = msg // 但不能和 msg 重复
  def say(): String = message
  def say1(hello: Hello): String = hello.message
}
val hello = new Hello("hello world")
println(hello.say)

上面代码可以看到如果要使用类参数 msg 作为字段,就需要显示定义另个不同名的。为了解决这个问题可以在类参数中预先定义好:

class ShowMsg(
  val show: String
) {
  println(show)
}

(new ShowMsg("hello"))

字段名前加 private 关键字,定义私有字段。

1.3 定义方法

class Message {
  def send(msg: String) {
    println(msg)
  }
}

val msg = new Message
msg.send("hello world")

如果方法不需要传递参数,可定义无括号的方法,并且调用时也不用加括号:

class Message {
  def sayHello {
    println("hello world")
  }
}

val msg = new Message
msg.sayHello

用到的场景:如果函数表示返回某个属性,就可以不用带括号。这样看上去更像读属性。

方法前加 private,定义私有方法。

1.4 覆盖方法

在 def 关键字前加 override 关键字用来覆盖父类的方法:

override def functionName

1.5 多态

Scala 可以根据参数类型(函数标签)来实现多态。

先决条件(precondition)

class Hello(msg: String) {
  require(msg != "")
}

如果 require 判断不为 true 的话,抛出 java.lang.IllegalArgumentException 异常。

1.6 辅助构造函数

为了满足“可变参数”的类:

class Plus(a: Int, b: Int) {
  def this(a: Int) {
    this(a, 1)
  }
  val num1: Int = a
  val num2: Int = b
  def show {
  println(num1, num2)
  }
}

val p1 = new Plus(1)
p1.show
val p2 = new Plus(1, 2)
p2.show

1.7 单例对象

单例对象构造时不需要带参数,也不需要使用 new 来创建对象。类似其他语言中的静态方法。使用 object 关键字来构造单例对象:

object Hello {
  def say(msg: String) {
    println(msg)
  }
}

Hello.say("hi")

1.8 伴身对象(companion object)

一个单例对象和一个类同名,这个单例对象被成为伴身对象,这个类叫伴身类(companion class),不与伴身共名的单例对象叫孤立对象(standalone object)。如下代码:

class Message {
  def send(msg: String) {
    println(msg)
  }
}

object Message {
  def sayHello() {
    println("hello world")
  }
}

val msg = new Message
Message.sayHello() // sayHello 是伴身类的方法

1.9 匿名类

构造一个匿名类:

(new {
  def hello = println("hello world")
}).hello

如果要带继承功能的话:

// 表示新的匿名类是 Thread 的子类。
new Thread {
...
}

1.10 反射

查看对象属于哪个类:

[object].getClass

列出对象所有方法:

[object].getClass.getMethods.map(l => println(l.getName))

2 方法调用

调用对象的一个方法:

object.方法名(参数)

调用方法也可以省略“句号”:

scala> class Hello {
     | def say(msg: String){
     | println(msg)
     | }}
defined class Hello

scala> val hello = new Hello()
hello: Hello = [email protected]

scala> hello.say("hehe")
hehe

scala> hello say "hehe"
hehe

最后句“say”成了操作符。如果有多个参数,就加括号调用:

scala> "hello" indexOf ("o", 1)
res5: Int = 4

scala> "hello" length
warning: there was one feature warning; re-run with -feature for details
res0: Int = 5

3 构造一个Scala应用程序

构造一个 Application,必须有一个 object 的类,并且有 main 方法,代码如下:

object Hello {
  def main(args: Array[String]) {
    println("hello world")
  }
}

这里和 Java 不同的是不必将这段代码保存在 Hello.scala 中,也就是说 public 属性的类的类名可以不和文件名相同。

然后运行 scalac 来编译:

$ scalac x.scala

编译后会在当前目录下产生 Hello.class 和 Hello$.class 文件。执行以下命令来运行 Application:

$ scala Hello
hello world

在编译的时候还可以用 fsc 命令,这是 Scala 编译器的一个快速版(fast Scala compiler):

$ fsc x.scala

首次执行时,会启动 daemon,以后的编译速度就会快很多(因为不用等待 Java runtime 启动)。关闭 fsc daemon 可以执行:

$ fsc -shutdown
[Compile server exited]

3.1 继承App类

可以继承App类来让代码更精简:

object Hello extends App {
    println("hello world")
}

如果不需要访问参数(args),就可以用这种方式省去对main函数的定义。另外如果在明确需要main运行在多线程上,这种方式是不可取的。

4 组合和继承

4.1 抽象类(abstract)和扩展类

抽象类定义一些方法,由扩展类来实现这些方法,抽象类是不能直接实例化成一个对象呢:

abstract class Show {
  def show(msg: String): String
}

class ShowMsg extends Show {
  def show(msg: String) = {println(msg);msg}
}

(new ShowMsg).show("hello")

其实 extends 表示继承关系。

4.2 方法和字段的覆盖

在 Java 中,名字有 4 个命名空间:字段、方法、类型和包,但在 Scala 中只有两个,types(类和 Trait 名)、Values(字段、方法、包和单例对象),所以以下定义 Scala 编译器会报错的:

class Wont {
  private var f = 0
  def f = 1 // error: method f is defined twice
}

4.3 调用父类构造函数

当子类被实例化的时候,父类的构造函数也会被执行。如果要覆盖父类的字段,可用 override 指明。如果父类需要参数初始化,可以在继承的时候就指定:

class Show(msg: String) {
  println("Init Show")
}

class ShowMsg extends Show("hi") {
  println("Init ShowMsg")
}

new ShowMsg()

4.4 多态和动态绑定

abstract class Say {
  def say
}

class SayHello extends Say {
  def say = println("hello")
}

class SayHi extends Say {
  def say = println("hi")
}

class SayBye extends Say {
  def say = println("bye")
}

def say(s: Say) = s.say

say(new SayHello)
say(new SayHi)
say(new SayBye)

利用多态可以定义工厂方法:

class Is {
  def whatIs(x: Int) = println("Is Int")
  def whatIs(x: String) = println("Is String")
}

val whatIs = new Is
whatIs.whatIs(1)
whatIs.whatIs("1")

4.5 定义常量成员

定义的常量成员是不会被覆盖的:

class Say(msg: String) {
  def say = println(msg)
  final def sayHello = println("hello")
}

class SayHi() extends Say("") {
  override def say = println("hi")
  override def sayHello = println("hello1") // method sayHello cannot
					    // override final member
}

(new SayHi).say

5 类的层次结构

根类叫 Any,Any 有两个子类:

  • AnyVal,是 Scala 中每个值类的父类,比如 Long、Int 等;
  • AnyRef,Scala 中引用类(reference class)的父类,其实就是 java.lang.Object 的别名。

5.1 底层类

有两个,scala.Null 和 scala.Nothing,用来处理“边界情况”。

  • Null 是 null 引用对象类型,它也是每个引用对象类型的子类;
  • Nothing 是任意类型的子类(它在最底层)。

因为 Nothing 是所有类型的子类,所以可以用来做异常处理:

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

// 这个函数返回Int,但可以返回Nothing
def testError(testValue: Int): Int = {
  if(testValue != 0)
    testValue
  error("hehe")
}

testError(0)

null 是 Null 的唯一对象,可以赋给任意 AnyRef 对象:

var s: String = null

但不可赋给 AnyVal 对象。

5.2 类型转换

判断类型

1.isInstanceOf[Int] // true
"hello world".isInstanceOf[Int] // false
"hello world".isInstanceOf[String] // true
"hello world".isInstanceOf[Any] // true

转换Any类型为具体类型

val testType: Any = 1 // 类型为 Any
testType.asInstanceOf[Int] // 类型为 Int

6 隐式转换(implicit)

当函数调用遇到参数类型不一致的时候,Scala 会进行类型隐式转换,如果没有找到合适的隐式转换,才会报错。如下代码:

class Num(n: Int) {
  def + (a: Int) = a + n
}

当执行如下时可以正常工作:

var n = new Num(10)
n + 1

但是如果参数传递的不是整数时,Scala 就会报错:

var n = new Num(10)
var m = new Num(100)
n + m
//error: type mismatch;

这个时候我们有两个办法来完成类型转换:1、多重方法,但需要在类里增加新的函数。2、在+方法里判断类型,也需要修改函数细节,除此之外可以用隐式转换:

implicit def numToInt(n: Num) = 0

var n = new Num(10)
var m = new Num(100)
println(n + m) // 10

这里,当出现类型不匹配的时候,Scala 会尝试去调用 numToInt 函数来完成类型转换。

关键字 implicit 标记为一个隐式转换,当编译器需要类型转换时,会从当前作用域中寻找 implicit 规则定义。注意这里函数名是什么不重要(implicit 在命名时,可以用任意合法的命名),重要的是函数要是一个“类型1 to 类型2”类型的函数。

我们还定义一个多类型对象的显示函数:

def showObject(obj: String): Unit = println(obj)
implicit def convert(obj: Int): String = obj.toString
implicit def convert(obj: Boolean): String = if (obj) "true" else "false"
implicit def convert(obj: Double): String = obj.toString

showObject("hello")
showObject(123)
showObject(true)
showObject(123.0)

上面代码如果没有 implicit 声明:

def showObject(obj: String): Unit = println(obj)
showObject("hello")
showObject(123)

Scala 运行时会报错:

 found   : Int(123)
 required: String
showObject(123)
           ^
one error found

7 case类和模式匹配

7.1 样本类

case class Hello(msg: String)

这样做有几个特点:

  1. 不需要 new 关键字来创建对象,如:Hello("test");
  2. 内部已经隐式将参数添加成字段来,就不用在类参数部分用val了。所以可以直接访问字段:Hello("test").msg;
  3. 自动添加了 toString、hashCode 和 equals 方法。如:print(Hello("test"))。

case类可以用来保存数据,如下例:

case class Person(name: String, email: String)
val p = Person("lux", "[email protected]")
println(p.name, p.email)

7.2 模式匹配

模式匹配可以用来匹配值、函数类型、类成员。最简单的常量模式,类似其他语言中 switch-case 语法:

args(0) match {
  case "a" => println("1")
  case "b" => println("2")
  case _ => println("?") // 表示除此之外的条件
}

模式匹配的语法:selector match { … }。

根据表达式来匹配,如下:

abstract class Hi
case class Hello() extends Hi
case class Say(msg: String) extends Hi

def selectTo(expr: Hi): String = expr match {
  case Say("hi") => "hehe"
  case Say("hello") => "hi"
  // 通配匹配
  case _ => "other"
}

println(selectTo(Say("hello")) // hi

上面代码表示可以根据参数类型来匹配。

根据参数类型匹配:

def say(msg: Any): Any = {
  msg match {
    case i:Int => i
    case s: String => s
  }
}

7.3 通配模式

上面代码 case _就是一个通配模式,表示“除此之外”的情况。通配匹配还可以用在表达式中,如:

abstract class Hi
case class Hello() extends Hi
case class Say(msg: String) extends Hi

def selectTo(expr: Hi): String = expr match {
  case Say(_) => "hehe"
  case _ => "other"
}

println(selectTo(Say("hello"))) // 不管参数是什么,都返回“hehe”

7.4 序列模式

可用数组、列表、元祖等等。

Array(2, 2, 3) match {
  case Array(1, _, _) => println("1开头")
  case Array(2, _, _) => println("2开头")
  case Array(3, _*) => println("3开头")
}

7.5 类型模式

def testType(obj: Any) {
  obj match {
    case a: Int => println("Int")
    case b: String => println(b)
  }
}

testType(1) // Int
testType("hello") //hello

7.6 Java 类型擦除问题

这样写是不行的:

case m: Map[String, Int] => println("yes")

由于 Java 的泛型会擦除类型信息,所以会报错,如果要这么写,启动 Scala 的时候要关闭类型擦除:

$ scala -unchecked

否者要修改成这样:

case m: Map[_, _] => println("yes")

7.7 变量绑定

abstract class Hi
case class Hello() extends Hi
case class Say(msg: String) extends Hi

def selectTo(expr: Hi) = expr match {
  // 把匹配到的部分绑定在变量 e 上面
  case Say(e @ _) => "Say: " + e
}

println(selectTo(Say("hello")))

7.8 封闭(sealed)类

模式匹配要考虑到所有匹配情况,特别是在没有默认处理的地方。可以用 sealed 关键字让在没有定义好所有匹配的情况下让编译器报错:

sealed abstract class Hi
case class Say(msg: String) extends Hi
case class Hello(who: String) extends Hi
def selectTo(expr: Hi) = expr match {
  // 这里少一个都不行,否则编译器会报错
  case Say(msg) => println(msg)
  case Hello(who) => println(who)
}

selectTo(Say("hello"))

当然,用 @unchecked 注解可以让编译器闭嘴:

sealed abstract class Hi
case class Say(msg: String) extends Hi
case class Hello(who: String) extends Hi

def selectTo(expr: Hi) = (expr: @unchecked) match {
  case Say(msg) => println(msg)
//  case Hello(who) => println(who)
}

selectTo(Say("hello"))

上面代码可以通过编译。

7.9 Option 类型

Scala 为可选值定义了 Option 类型,可以是 Some(x) 形式,x 是值,也可以是 None 对象,表示缺失。

val aMap = Map("a" -> 1, "b" -> 2)

def show(x: Option[Any]) = x match {
  case Some(i) => i
  case None => "?"
}

println(show(aMap get "a")) // 1
println(show(aMap get "c")) // ?

7.10 模式解构

Tuple 解构:

scala> val (a, b) = (123, "abc")
a: Int = 123
b: String = abc

List 解构:

scala> val List(a, b, c) = List(1, 2, 3)
a: Int = 1
b: Int = 2
c: Int = 3

可以用一个变量保存余下所有元素:

scala> val a :: rest = List(1, 2, 3)
a: Int = 1
rest: List[Int] = List(2, 3)

样本类解构:

// 如果知道样本类的解构,也可以用模式匹配:
case class Say(from: String, msg: String)
val exp = Say("lx", "hi")
// 注意类型
val Say(from, msg) = exp

println(from) // lx
println(msg)  // hi

for中解构:

for ((k, v) <- Map("a" -> 1, "b" -> 2)) {
  println(k)
}

7.11 解构

object Email {
  def apply(user: String, domain: String) = user + "@" + domain
  def unapply(str: String): Option[(String, String)] = {
    val parts = str split "@"
    if (parts.length == 2)
      Some(parts(0), parts(1))
    else
      None
  }
}

"[email protected]" match {
  case Email(user, domain) => println(user, domain)
}

8 范型

class Info[T] {
  // =_表示如果是整型,就赋值为 0,布尔型就赋值为 false
  //引用类型就赋值为 null
  private var info: T = _

  def set(value: T) {info = value}
  def get: T = info
}

// 由于是范型,这里必须指定类型
val info = new Info[String]
info.set("test")
println(info.get)

9 特质(trait)

Trait 分装了方法和字段的定义。一个类可以混入一个或多个 Trait,有点类似 Java 的 Interface,但比 Interface 更加强大。在大项目中,可以按行为和特征定义多个 Trait,然后在类中混入不同的 Trait,比起 Java 的 Interface 要灵活得多。

trait Hello {
  def sayHello {
    println("hello")
  }
}

class SayHello extends Hello {
  override def toString = "SayHello"
}

以上代码 SayHello 类是 AnyRef 的子类,并且“混入”了 Hello 类。如果在继承类的同时又要混入其他 Trait,可以使用 with 关键字,并且可以用多个 with(类似多重继承):

trait Hello {
  def sayHello {
    println("hello")
  }
}

trait Hi {
  def sayHi {
    println("hi")
  }
}

class SayHello extends Hello with Hi{
  override def toString = "SayHello"
}

val say = new SayHello
say.sayHello
say.sayHi
println(say)

如果多个不相关类的重用,就用特质(把瘦接口变成胖接口)。