Scala类,对象,包,继承

  • 类中的字段自带getter,setter方法。
  • 每个类都有一个主构造器,这个构造器和类定义“交织”在一起,它的参数直接成为类的字段,主构造器执行类体重所有的语句。
  • 辅助构造器是可选的,它们叫做this。

简单类和无参方法

class Counter{
private var value = 0

def increment(){ 
    value += 1}

def current = value

}

在scala中,类并不声明为public,scala源文件可以包含多个类,所有这些类都具有public属性。

val myCounter = new Counter
myCounter.increment()//改值器一般使用()调用
myCounter.current//取值器一般不使用()

带getter/setter的属性

scala会自动对每个字段都提供getter/setter方法。比如下面这个类。

class Person{
    var age = 0
}

在scala中,getter/setter分别叫做age,age_=

val fred = new Person
println(fred.age)//调用getter:,即fred.age()
fred.age = 21  //调用setter: 即fred.age_(21)

你当然也可以自己重新定义getter/setter:

class Person{
    private var myage = 0

    def age = myage

    def age_= (newValue : Int){
    if(newValue > myage) myage = newValue
    }
}

object Personextends App{
  val fred = new Person
  println(fred.age)
  fred.age = 30
  println(fred.age)
}

scala对每个字段生成getter/setter方法听起来有些恐怖,不过你可以控制这个过程。

  • 如果字段是私有的,则getter/setter方法也是私有的。
  • 如果属性的值在对象构建完成后不再改变,可以使用val字段,则只有getter方法被生成而没有setter。
class Message{
val timeStamp = new java.util.Date
}
  • 如果你不需要任何任何getter/setter,可以将字段声明为private[this],称为对象私有字段,不允许本对象访问别的对象的该字段。
class Person{
    private[this] var value= 0
}

总结一下,当你实现属性时,有下面四个选择:

  • var foo: scala自动合成一个getter/setter
  • val foo: scala自动合成一个getter
  • 由你来定义foo,foo_=方法

主构造器

如果一个类没有显示定义主构造器,则自动拥有一个无参的主构造器,这样的构造器仅仅是简单的执行类体中的所有语句而已。

class Person(val name : String,private var age : Int){
}
//这段代码将生命并初始化为如下字段:
val name : String
private var age: Int

构造参数也可以是普通的方法参数。不带val和var,这样的参数如何处理取决于他们在类中如何被使用。

  • 如果不带val和var的参数至少被一个方法使用,它将被升格为字段:
class Person(name : String,age : Int){
def description = name + " is "+age+ " years old"
}

上述代码声明并初始化了不可变字段name和age,而这两个字段都是对象私有的,等同于private[this] val的效果。

  • 否则,该参数不被保存成字段,它仅仅是一个可以被主构造器中的代码访问的普通参数。

总结一下,关于主构造器参数生成的字段和方法:

  • name : String,对象私有字段,如果没有方法使用name,则没有该字段。
  • private val/var name : String,私有字段,私有的getter/setter方法
  • val /var name :String,私有字段,共有的getter/setter方法。

有时候你可能会需要将主构造器设定为私有的,此时用户需要通过辅助构造器来构造对象。

class Person private (val id : Int){
...
}

辅助构造器

  • 辅助构造器名称为this
  • 每个辅助构造器必须以一个对先前已定义的其他辅助构造器或主构造器的调用开始。
class Person{
private var name = ""
private var age = 0

def this (name : String){
this()
this.name = name
}

def this(name : String,age: Int){
this(name)
this.age = age
}

}

Object

当你需要某个类的单个实例,或者想为其他值或者函数找一个可以挂靠的地方时,你会需要object。

  • 用对象作为单例或存放工具方法。
  • 类可以拥有一个同名的伴生对象。
  • 对象可以扩展类或特质。
  • 对象的apply方法通常用来构造伴生类的新实例。
  • 如果你不想显式定义main方法,可以扩展App特质的对象。
  • 你可以通过扩展Enumeration对象来实现枚举。

单例对象

scala没有静态方法或静态字段,你可以用object这个语法结构来达到相同的目的。对象定义了某个类的单个实例。

object Account{
private var lastNumber = 0
def newUniqueNumber() = {lastNumber += 1;lastNumber}
}

object ClassExample{
  def main(args : Array[String]){
    println("this is a new account")
    println(Account.newUniqueNumber())//1
    println(Account.newUniqueNumber())//2
    println(Account.newUniqueNumber())//3
  }
}

当你的应用程序需要一个新的唯一账号时,调用Account.newUniqueNumber()。对象的构造器在该对象第一次被使用时调用。以后的每次调用不会产生新的对象,而只是对原对象的修改,如果一个对象从未被使用,那么其构造器不会被执行。

对象本质上可以拥有类所有的特性----它甚至可以扩展其他类和特质,只有一个例外,你不能提供构造器参数。它的主要用途是:

  • 作为存放工具函数和常量的地方。
  • 高效的共享单个不可变实例。

伴生对象

在java中,你通常会用到既有实例方法,又有静态方法的类,scala是通过类和与类同名的“伴生对象”来实现的:

class Account{
val id = Account.newUniqueNumber()
private var balance = 0.0
def deposit(amount Double){balance += amount}
}

object Account{
private var lastNumber = 0
def newUniqueNumber() = {lastNumber += 1;lastNumber}
}

类和它的伴生对象可以相互访问私有特性。但是伴生对象并不在伴生类作用域中,因此必须用Account.newUniqueNumber()来访问伴生对象的方法。

扩展类或特质的对象

object可以扩展类以及一个或多个特质,使用extends关键字。

apply方法

通常我们会定义和使用对象的apply方法,这样可以省去很多new关键字。例如

Array(Array(1,2),Array(3,4))
Array(100)//调用Array对象的apply方法,得到一个含单个元素(100)的数组。
new Array(100)//调用构造器this(100),结果包含了100个null元素。
class Account private(val id : Int,initialBalance : Double){
private var balance = initialBalance
...
}

object Account{
def apply(initialBalance : Double)=new Account(new newUniqueNumber(),initialBalance)

def newUniqueNumber() = {lastNumber += 1;lastNumber}
}

val acct = Account(1000.0)

应用程序对象

每个scala程序都必须从一个对象的main函数开始。

object Hello{
def main(args : Array[String]){
...
}
}

除了每次提供自己的main方法外,也可以扩展App特质。如果你需要命令行参数,也可以通过args属性得到:

object Hello extends App{
if(args.length > 0) println("hello "+args(0))
else println("hello world")
}
}

枚举

与java/c++不同,scala没有枚举类型,你需要扩展Enumeration类的对象并以Value方法调用初始化枚举中的所有可选值:

object TrafficLightColor extends Enumeration{
val Red,Yellow,Green = Value
//定义三个字段,然后用Value调用初始化。每次调用Value方法都返回内部类的新实例,该内部类叫做Value。
}
val Red = Value(0,"Stop")//传入ID和名称
val Yellow = Value(10)//传入ID,默认名称为字段名
val Green = Value("Go")//不指定ID,则用前一个ID值+1,从零开始。

定义完成以后你就可以用TrafficLightColor.Red来引用枚举值了。如果你不想这么冗长繁琐,可以用import TrafficLightColor._来引入枚举值。

import TrafficLightColor._
def doWhat(color : TrafficLightColor)={
if(color == Red) "stop"
else if(color == Yellow) "hurry up"
else "go"
}

for(c <- TrafficLightColor.values) println(c.id + " : "+c)
//枚举类型提供了id方法来返回ID值。名称通过toString方法返回。

TrafficLightColor(0)
TrafficLightColor.withName("Red")
//你可以通过ID值和名称来查找定位枚举对象。

包和引入

  • 包也可以像内部类一样嵌套。
  • 文件顶部不带花括号的包声明在整个文件范围内有效。
  • 引入语句可以出现在任何位置。
  • java.lang,scala,Predef总是被引入。

同一个包可以被定义在多个文件中。例如在Employee.scala文件中定义了一个:

package com{
    package horstmann{
        package impatient{
            class Employee
            ...
            }
        }
    }

而在另一个名为Manager.scala的文件中可能会包含:

package com{
    package horstmann{
        package impatient{
            class Manager
            ...
            }
        }
    }

包作用域

你可以访问上层作用域中的名称。

package com{
    package horstmann{
        object Utils {
            def function{}
            }

        package impatient{
            class Employee{
            ...
            def function2{Utils.funtion}//所有父包的内容都在作用域内。
            }
            }
        }
    }

文件顶部标记法

package com.horstmann.impatient
package people

class Person
...

//等同于
package com.horstmann.impatient{
    package people{
        class Person
    ...
        }
    }

这样做的结果是文件的所有内容都属于com.horstmann.impatient.people,但com.horstmann.impatient包的内容是可见的,可以被直接引用。

引入

引入的唯一目的是你不需要使用长名称。

import java.awt.Color._
val c1 = RED //Color.RED

import语句可以出现在任何地方,效果一直延伸到包含该语句的块末尾。这是个很有用的特性,尤其是对通配符而言,从多个源引入大量名称总是让人担心的。

默认引入

每个scala程序都是隐式的从以下代码开始的:

import java.lang._
import scala._
import Predef._

继承

  • extends,final关键字和java中相同。
  • 重写非抽象方法时必须使用override。
  • 只有主构造器可以调用超类主构造器。
  • 你可以重写字段。
  • 在scala中调用超类的方法和java相同,使用super关键字。

类型检查和转换

if (p.isInstanceOf[Employee]){
    //如果p是Employee类型或其子类,则会成功
    val s = p.asInstanceOf[Employee]
    //相当于java中的(Employee) p
    ...
}

if(p.getClass == classOf[Employee])
//classOf函数定义在scala.Predef对象中。

与类型检查相比,更常用的做法是模式匹配:

p match {
case s : Employee => ...//将s作为Employee处理
case _ => //将s作为非Employee处理
 }

超类的构造

子类只有主构造器可以调用超类的构造器,这是因为scala的类定义和构造器是交织在一起的。

class Employee(name : String , age : Int , val salary : Double) extends Person(name , age)

抽象类

你可以用abstract来标记不能被实例化的类,它的某个方法没有被完整定义:

abstract class Person(val name : String){
    def id : Int//没有方法体,抽象方法
}

在子类中重写超类的抽象方法时,不要使用override关键字:

class Employee(name : String) extends Person(name){
    def id = name.hashCode
}

抽象字段

除了抽象方法外,类还可以有抽象字段,抽象字段就是没有初始值的字段。

abstract class Person{
    val id : Int//这是一个带有抽象的getter方法的抽象字段。
    var name : String//另一个抽象字段,带有抽象的getter和setter方法。
}

具体的子类必须提供具体的字段:

class Employee(val id : Int) extends Person{//子类有具体的id属性。
    var name = ""//和具体的name属性。
}

重写字段和方法

子类可以用override关键字重写父类的字段和方法。

class Person(val name : String){
    override def toString = getClass.getName + "[name="+name+"]"
}

class SecretAgent(codename : String) extends Person(codename){
    override val name = "secret"
    override val toString = "secret"
}

对象相等性

Scala中所有的类都继承自Any类,类似于java的Object类。 当你实现类的时候,应该考虑重写equals方法,以提供一个自然的淤泥的实际情况相符的相等性判断。

final override def equals(other : Any) = {
val that = other.asInstanceOf[Item]
if(that == null) false 
else decription == that.description && price == that.price
}

注意参数的类型必须是Any,这才是对AnyRef的equals方法的重写,否则将会被视为一个不相关方法。

Comments !