【Scala】Scala之Classes and Properties


一、前言

  前面學習了控制結構,下面學習Scala的Class和Properties。

二、Class&Properties

  盡管Scala和Java很類似,但是對類的定義、類構造函數、字段可見性控制等則不相同,Java更為冗長,Scala精煉。本章將通過Scala的構造函數工作原理來理解Scala的類和字段,當申明類構造函數參數和使用var、val、private關鍵字來修飾類的字段時,Scala編譯器將會為你生成代碼,根據字段修飾符不同,Scala編譯器會生成不同的存取函數,本章也會展示如何重寫這些函數,另外,Scala將會根據你的值為變量賦予相應的類型。

  2.1 創建主構造函數

  1. 問題描述

  你需要為一個類創建主構造函數,你很快就會發現其和Java大不相同。

  2. 解決方案

  Scala的主構造函數是如下的結合

  · 構造函數參數

  · 類主體中調用的方法

  · 類主體中執行的語句和表達式

  Scala類主體中聲明的字段與Java類似,他們在類初始化時被賦值,下面的類演示構造函數的參數、類字段和類中的語句

  

  由於類主體中的方法也是構造函數的一部分,當創建一個Person實例時,你會看到println的輸出和printHome和printFullName函數調用的輸出

  3 討論

  如果你是從Java到Scala,你會發現在Scala中聲明一個主構造函數時非常不同的,在Java中,在不在主構造函數中都是非常明顯的,但是Scala模糊了這個區別,一旦你理解了這個方法,你將會更精確的定義類構造函數,由於構造函數中的firstName和lastName被聲明為var類型,因此其是可變的,Scala會為其生成存取函數

  

  HOME字段時private和val的,其在Java中等效於final和private的,其不能被其他對象直接訪問,並且其值不能被改變,使用scalac進行編譯后,再用javap進行反編譯結果如下

  

  可以看到包含了很多_$eq方法,這是var字段的語法糖,對用戶透明,對於如下類 

class Person {
    var name = ""
    override def toString = s"name = $name"
}

  由於name是var類型的,因此Scala會為其生成存取函數,即將會生成name_$eq函數,有了該函數,你可以修改name字段 

p.name = "Ron Artest"

  實際上,你調用的是如下方法  

p.name_$eq("Ron Artest")

  可以看到Scala的代碼更為精煉,在聲明字段時需要注意Scala是否會生成該存取函數

  2.2 控制構造函數字段的可見性

  1. 問題描述

  你想控制Scala中的類中作為構造函數參數的字段的可見性

  2. 解決方案

  構造函數中字段的可見性由該字段是否被聲明為val或var或無val、var,是否為字段添加了private決定,解決方案如下

    · 如果字段被聲明為var,Scala會為其生成getter和setter方法

    · 如果字段被聲明為val,Scala會為只其生成getter方法

    · 如果字段無var或var,Scala將不會生成getter或setter方法

    · var和val字段可以使用private關鍵字修飾,這樣可以防止生成getter或setter方法

  如果字段被聲明為var,則表示該字段的值是可變的,所以Scala為其生成getter和setter函數,下面示例中的字段被聲明為var,則表示可變

  

  如果字段被聲明為val,則表示該字段的值是不可變的,如同Java的final,因此,其不會有setter方法,但是有getter方法

  

  當構造函數中的參數沒有使用var或者val聲明時,其可見性變得十分嚴格,Scala不會為其生成setter或getter方法

  

  除了將變量定義為val或var之外,還可以使用private關鍵字修飾字段,private關鍵字可以防止Scala生成getter或setter方法,因此該字段只能在類內部訪問

  

  在類外訪問name字段時,發生異常,而在類函數中訪問時正常

  3. 討論

  當字段定義為val時,則不會產生setter,若定義為var時,則會產生setter和getter,private關鍵可以提供其他的靈活性。case類中的構造器中的參數與上述情況不太相同,如果你沒有使用var或val修飾字段,如 

case class Person(name: String)

  此時你也可以訪問該字段,就像該字段被定義為val一樣

  

  2.3 定義輔助構造器

  1. 問題描述

  你要定義類的一個或多個輔助構造函數,以便用戶不同方法來創建對象實例

  2. 解決方案

  將輔助構造函數定義為名字為this的類的方法,你可以定義多個輔助構造函數,但是它們需要有不同的函數簽名(參數列表),每個構造函數必須調用前面定義的多個構造函數中的一個。

class Pizza (var crustSize: Int, var crustType: String) {
    // one-arg auxiliary constructor
    def this(crustSize: Int) {
        this(crustSize, Pizza.DEFAULT_CRUST_TYPE)
    }
    // one-arg auxiliary constructor
    def this(crustType: String) {
        this(Pizza.DEFAULT_CRUST_SIZE, crustType)
    }
    // zero-arg auxiliary constructor
    def this() {
        this(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE)
    }
    override def toString = s"A $crustSize inch pizza with a $crustType crust"
}

object Pizza {
    val DEFAULT_CRUST_SIZE = 12
    val DEFAULT_CRUST_TYPE = "THIN"
    
    def main(args: Array[String]): Unit = {
        val p1 = new Pizza(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE)
        val p2 = new Pizza(Pizza.DEFAULT_CRUST_SIZE)
        val p3 = new Pizza(Pizza.DEFAULT_CRUST_TYPE)
        val p4 = new Pizza
        println(p1)
        println(p2)
        println(p3)
        println(p4)
    }
}

  運行結果 

A 12 inch pizza with a THIN crust
A 12 inch pizza with a THIN crust
A 12 inch pizza with a THIN crust
A 12 inch pizza with a THIN crust

  3. 討論

  關鍵點如下

    · 輔助構造函數時以this為名字的類方法

    · 每個輔助構造函數必須以對先前定義的構造函數的調用開始

    · 每個輔助構造函數必須有不同的函數簽名

    · 構造函數通過this來調用其他構造函數

  上述示例中的所有輔助構造函數都調用了主構造函數,但是不是只能調用主構造函數,還可調用輔助構造函數,如下所示 

def this(crustType: String) {
    this(Pizza.DEFAULT_CRUST_SIZE)
    this.crustType = Pizza.DEFAULT_CRUST_TYPE
}

  同時,crustSize和crustType都是主構造函數中的參數,其可以使得Scala為其生成setter和getter函數,你也可以使用如下方法,其需要更多代碼 

class Pizza () {
    var crustSize = 0
    var crustType = ""
    def this(crustSize: Int) {
        this()
        this.crustSize = crustSize
    }
    def this(crustType: String) {
        this()
        this.crustType = crustType
    }
    
    // more constructors here ...
    override def toString = s"A $crustSize inch pizza with a $crustType crust"
}

  case類是一個可以生成很多樣板代碼的特殊類,為case類添加輔助構造器與常規類的做法不相同,這是因為它們不是真正的構造函數:它們是類的伴生對象中的apply
方法,為了進行驗證,先定義一個Person的case類

  

  在val p = new Person("leesf", 25)代碼后,由於語法糖的存在,Scala編譯器將其轉化為了如下代碼

val p = Person.apply("leesf", 25)

  這是伴生對象中的apply方法,如果你想要為case類添加構造函數,你只需要在伴生對象中添加apply方法即可

// the case class
case class Person (var name: String, var age: Int)

// the companion object
object Person {
    def apply() = new Person("<no name>", 0)
    def apply(name: String) = new Person(name, 0)
}

  測試代碼如下 

object CaseClassTest extends App {
    val a = Person() // corresponds to apply()
    val b = Person("leesf") // corresponds to apply(name: String)
    val c = Person("dyd", 25)
    println(a)
    println(b)
    println(c)
    // verify the setter methods work
    a.name = "leesf"
    a.age = 25
    println(a)
}

  運行結果  

Person(<no name>,0)
Person(leesf,0)
Person(dyd,25)
Person(leesf,25)

  2.4 定義私有主構造函數

  1. 問題描述

  你想要生成類私有的主構造函數,如強制執行單例模式

  2. 解決方案

  為了讓主構造器私有,需要在類名和構造函數所接受的任何參數之間插入private關鍵字 

class Order private { ... }
class Person private (name: String) { ... }

  在REPL中,定義了私有類后,不能創建類實例

  

  3. 討論

  在Scala中強制單例模式的一種簡單方法就是使主構造器變為private,然后在伴生對象中添加getInstance方法 

class Brain private {
    override def toString = "This is the brain."
}

object Brain {
    val brain = new Brain
    def getInstance = brain
}

object SingletonTest extends App {
    val brain = Brain.getInstance
    println(brain)
}

  運行結果: 

This is the brain.

  其中伴生類對象中的getInstance是靜態方法

  在Java中,你可以在類中定義靜態方法來創建文件工具類,但在Scala中,你只需要將這些方法放在對象中即可 

object FileUtils {
    def readFile(filename: String) = {
        // code here ...
    }
    def writeToFile(filename: String, contents: String) {
        // code here ...
    }
}

  說明:可以通過FileUtils.readFile和FileUtils.writeToFile來調用相應的方法

  2.5 為構造函數參數提供默認值

  1. 問題描述

  你想要為構造器參數提供默認值,給其他類調用構造函數時可決定是否指定該參數

  2. 解決方案

  在構造函數聲明中給予參數默認值,這里有一個簡單的聲明,Socket類有一個構造函數參數名為timeout,默認值為10000 

class Socket (val timeout: Int = 10000)

  由於構造函數提供了默認參數,因此在調用時可以不指定timeout的值

  

  也可以指定timeout的值

  

  如果你更喜歡調用構造函數時使用命名參數的方法,你也可以用如下方法調用構造函數

  

  3. 討論

  默認參數可以有效消除輔助構造函數,如下的構造函數相當於兩個構造函數(不指定參數值和指定參數值)

class Socket (val timeout: Int = 10000)

  若沒有默認參數這一特性,則需要定義一個輔助構造函數 

class Socket(val timeout: Int) {
    def this() = this(10000)
    override def toString = s"timeout: $timeout"
}

  你可以為多參數的構造函數提供多個默認值 

class Socket(val timeout: Int = 1000, val linger: Int = 2000) {
    override def toString = s"timeout: $timeout, linger: $linger"
}

  盡管你只定義了一個構造函數,但實際上有三個構造函數

  

  在創建對象時你也可以提供參數名

  

  2.6 覆蓋默認的訪問器和修改器

  1. 問題描述

  你想要覆蓋Scala為你生成的訪問器和修改器

  2. 解決方案

  這是一個技巧的問題,如果你想堅持用Scala的命名約定時,你不能重寫Scala為你生成的getter和setter方法,例如Person類有name參數,以下代碼將不會被編譯

  

  為了解決這個問題,可以構造函數的字段名,如將name改為_name

  

  name方法為訪問器,name_=方法為修改器

  

  如果你不想使用Scala為你定義訪問器和修改器的名字,你可以自由更改,如改為getName、setName。

  3. 討論

  如果你將類構造器的字段設置為var,則Scala會自動生成getter和setter方法,對於如下類 

class Stock (var symbol: String)

  首先使用scalac進行編譯,然后使用javap Stock進行反編譯,結果如下

  

  可以看到Scala生成了symbol方法和symbol_$eq方法,可以直接通過方法名調用,對於symbol_$eq而言,還可以使用symbol_=來調用

  

  如果本想對字段加private,然而沒有加,如下類 

class Stock (var _symbol: String) {
    def symbol = _symbol
    def symbol_= (s: String) {
        this.symbol = s
        println(s"symbol was updated, new value is $symbol")
    }
}

  同樣經過編譯和反編譯,結果如下

  

  可以看到姿勢的_symbol函數和_symbol_$eq函數都變為public了,而加private修飾時,其結果如下

  

  2.7 防止生成getter和setter方法

  1. 問題描述

  當你定義類的構造函數參數為var時,Scala將會自動生成setter和getter方法,若是val,則生成getter方法,但是你既不需要setter,也不需要getter

  2. 解決方案

  將字段使用private或者private[this]修飾,如下面示例  

class Stock {
    var delayedPrice: Double = _
    private var currentPrice: Double = _
}

  使用scalac進行編譯,再使用javap進行反編譯,結果如下

  

  可以看到delayedPrice字段生成了getter和setter方法,而currentPrice字段則沒有生成getter和setter方法

  3. 討論

  將字段定義為private,這樣其只能被同類的所有實例訪問,例如,所有的Stock實例可以互相訪問currentPrice字段,如下面示例的結果為true

  

  將字段定義為private[this]將會使字段變為對象私有,只有包含它的對象能夠訪問,同類的實例將無法訪問其他實例的字段

  

  可以看到在this對象的方法中無法訪問that對象的price字段

  2.8 將代碼塊或函數賦值給字段

  1. 問題描述

  你想要使用代碼塊或通過調用函數初始化類中的字段

  2. 解決方案

  設置字段等於代碼塊或函數,如果算法需要運行一段時間,則可以設置為lazy,下面示例中,text被設置成等於代碼塊,它要么返回文件中包含的文本,要么返回錯誤信息,這取決於該文件是否存在和是否能被讀取 

class Foo {
    // set 'text' equal to the result of the block of code
    val text = {
        var lines = ""
        try {
            lines = io.Source.fromFile("F:/test.txt").getLines.mkString
        } catch {
            case e: Exception => lines = "Error happened"
        }
        lines
    }
    println(text)
}

object Test extends App {
    val f = new Foo
}

  運行結果 

This is a test file.

  對text的賦值和println都在Foo類構造器中,當新生實例時會被執行,因此,編譯運行該示例時要么返回文件中的內容,要么發生錯誤,在catch中被捕獲

  3. 討論

  當字段被定義成lazy時,只有當被訪問時才會被初始化  

class Foo {
    val text =
        io.Source.fromFile("F:/test.txt").getLines.foreach(println)
}

object Test extends App {
    val f = new Foo
}

  運行結果

This is a test file.

  當字段使用lazy修飾時

class Foo {
    lazy val text =
        io.Source.fromFile("F:/test.txt").getLines.foreach(println)
}

object Test extends App {
    val f = new Foo
}

  無任何結果輸出

  2.9 設置未初始化的var字段類型

  1. 問題描述

  你想要設置未初始化的var字段的類型,你可能會這樣寫,但是不知道如何結尾 

var x =

  2. 解決方案

  通常,將字段定義為Option,對於有些類型,如String和numeric字段,你可以指明初始值,例如,你想要用戶在社交網中注冊,注冊使用到的信息只有username和password,因此,你可以有如下類定義

case class Person(var username: String, var password: String)

  之后,你也需要用戶的其他信息,如age, first name, last name, address,可以有如下定義  

case class Person(var username: String, var password: String) {
    var age = 0
    var firstName = ""
    var lastName = ""
    var address = None: Option[Address]
}

case class Address(city: String, state: String, zip: String)

  即使用Option[Address]類型的None作為缺省值,可以使用如下 

val p = Person("leesf", "secret")
p.address = Some(Address("WH", "HB", "43000"))

  當需要訪問address的字段時,可以如下 

p.address.foreach { a =>
    println(a.city)
    println(a.state)
    println(a.zip)
}

  當p.address為None時,則會跳過,為Some時,則會打印相應信息

  3. 討論

  編譯器有時會自動給出類型,如下所示  

var i = 0 // Int
var d = 0.0 // Double

  當需要自定義類型時,可以如下  

var b: Byte = 0
var c: Char = 0
var f: Float = 0
var l: Long = 0
var s: Short = 0

  2.10 處理繼承時的構造器參數

  1. 問題描述

  你想要擴展基類,並且需要使用基類中聲明的構造函數參數,以及子類中的新參數

  2. 解決方案

  還是使用var和val來定義構造函數參數,當定義子類構造函數時,父類構造函數中的字段不再使用var修飾,在子類構造函數中定義新的字段

class Person (var name: String, var address: Address) {
    override def toString = if (address == null) name else s"$name @ $address"
}

  首先定義了Person基類,然后定義Employee子類 

class Employee (name: String, address: Address, var age: Int)
  extends Person (name, address) {
    // rest of the class
}

  Employee繼承Person類,不再使用var修飾name和address,但是age在父類中不存在,則使用var修飾,其中Address類如下  

case class Address (city: String, state: String)

  可以使用如下方法創建子類  

val lee = new Employee("Leesf", Address("JZ", "HB"), 25)

  3. 討論

  為了理解構造函數參數在子類中是如何工作的,也可幫助理解Scala是如何翻譯你的代碼的,由於Person的參數被定義為var  

class Person (var name: String, var address: Address) {
    override def toString = if (address == null) name else s"$name @ $address"
}

  Scala編譯器將會生成getter和setter

  

  此時,如果Employee繼承Person,在Employee構造函數中如何處理name和address字段,加入沒有新的參數,那么至少有兩種選擇

// Option 1: define name and address as 'var'
class Employee (var name: String, var address: Address)
    extends Person (name, address) { ... }
// Option 2: define name and address without var or val
class Employee (name: String, address: Address)
    extends Person (name, address) { ... }

  由於Scala已經在Person類中生成了setter和getter函數,一種解決辦法就是在Employee的構造函數的參數不用var修飾  

// this is correct
class Employee (name: String, address: Address)
    extends Person (name, address) { ... }

  由於Employee的構造函數中並未使用var修飾,因此Scala不會為字段生成getter和setter方法,驗證代碼如下 

case class Address (city: String, state: String)

class Person (var name: String, var address: Address) {
    override def toString = if (address == null) name else s"$name @ $address"
}

class Employee (name: String, address: Address)
    extends Person (name, address) {
}

  使用javap反編譯Employee,結果如下

  

  可以看到Scala並未為Employee類生成任何setter和getter方法

  2.11 調用父類構造函數

  1. 問題描述

  當創建子類構造函數時,你想控制父類構造函數

  2. 解決方案

  你可以通過子類的主構造函數來控制父類構造函數,但是不能通過子類的輔助構造函數來控制父類構造函數,當定義子類時,你可以通過定義extends部分來控制子類所調用的父類構造器,例如,Dog類被定義為調用Animal類,其有name參數 

class Animal (var name: String) {
    // ...
}

class Dog (name: String) extends Animal (name) {
    // ...
}

  如果Animal類有多個構造函數,Dog子類可以調用任意的父類構造函數  

// (1) primary constructor
class Animal (var name: String, var age: Int) {
    // (2) auxiliary constructor
    def this (name: String) {
        this(name, 0)
    }
    override def toString = s"$name is $age years old"
}

// calls the Animal one-arg constructor
class Dog (name: String) extends Animal (name) {
    println("Dog constructor called")
}

  由於輔助構造函數的第一行必須要調用其他構造函數,因此,輔助構造函數不能調用父類構造函數,下面代碼中,Employee類的主構造含可以調用Person類的任意構造函數,但是Employee的輔助構造函數只能調用其他構造函數

case class Address (city: String, state: String)
case class Role (role: String)

class Person (var name: String, var address: Address) {
    // no way for Employee auxiliary constructors to call this constructor
    def this (name: String) {
        this(name, null)
        address = null
    }
    override def toString = if (address == null) name else s"$name @ $address"
}

class Employee (name: String, role: Role, address: Address)
    extends Person (name, address) {
    def this (name: String) {
        this(name, null, null)
    }
        
    def this (name: String, role: Role) {
        this(name, role, null)
    }
    
    def this (name: String, address: Address) {
        this(name, null, address)
    }
}

  所以無法通過子類輔助構造函數來控制父類構造函數,因為輔助構造函數必須要調用其他構造函數,所有的輔助構造函數最終會調用主構造函數,最終由主構造函數調用輔助構造函數

  2.12 何時使用抽象類

  1. 問題描述

  Scala有traits特性,並且traits比抽象類更靈活,那么何時使用抽象類呢

  2. 解決方案

  以下兩種原因需要使用抽象類

    · 你想要創建一個包含構造函數參數的基類

    · 代碼將會被Java代碼調用

  考慮第一種情況,traits不允許構造函數參數,如下所示  

trait Animal(name: String) // this won't compile

  當基類構造函數必須含有參數時應該使用抽象類  

abstract class Animal(name: String)

  考慮第二種情況是你的代碼將會被Java調用,traits中實現的方法無法被Java訪問

  3. 討論

  當構造函數有參數時,需要使用抽象類而非traits,但是,一個類只能繼承一個抽象類,其與Java類似,當類中抽象方法時,該類就是抽象,方法體未定義的方法就是抽象方法  

def speak // no body makes the method abstract

  不需要使用abstract關鍵字,如下示例 

abstract class BaseController(db: Database) {
    def save { db.save }
    def update { db.update }
    def delete { db.delete }
    // abstract
    def connect
    // an abstract method that returns a String
    def getStatus: String
    // an abstract method that takes a parameter
    def setServerName(serverName: String)
}

  其中connect、getStatus、setServerName為抽象方法,子類必須要實現或者是為抽象類

  2.13 在抽象基類(或traits)中定義屬性

  1. 問題描述

  你想要在抽象類或traits中定義具體的或抽象屬性,以便所有的子類都可以引用

  2. 解決方案

  你可以在抽象類或traits中定義var或val字段,該字段可以是抽象的或者是具體定義的,下面示例展示了Pet抽象類中的抽象val和var字段,並且有一個sayHello的具體方法,以及重寫的toString方法 

abstract class Pet (name: String) {
    val greeting: String
    var age: Int
    def sayHello { println(greeting) }
    override def toString = s"I say $greeting, and I'm $age"
}

  Dog和Cat類繼承Pet,並且提供了greeting和age的具體實現,其中,字段給再次定義成val或var

class Dog (name: String) extends Pet (name) {
    val greeting = "Woof"
    var age = 2
}

class Cat (name: String) extends Pet (name) {
    val greeting = "Meow"
    var age = 5
}

  可使用如下 

object AbstractFieldsDemo extends App {
    val dog = new Dog("Fido")
    val cat = new Cat("Morris")
    dog.sayHello
    cat.sayHello
    println(dog)
    println(cat)
    // verify that the age can be changed
    cat.age = 10
    println(cat)
}

  運行結果如下  

Woof
Meow
I say Woof, and I'm 2
I say Meow, and I'm 5
I say Meow, and I'm 10

  3. 討論

  你可以在抽象類中定義val或var的抽象字段,抽象字段在抽象類中的工作機制非常有趣

    · 抽象的var字段也會生成getter和setter方法

    · 抽象的val字段會生成getter方法

    · 當在抽象類或traits中定義抽象的var或val字段時,Scala並不會生成該字段,只會生成方法

  當在具體類中重寫父類抽象的var或val字段時,一定要再次注明val或var,因為父類中不存在該字段,不需要使用override關鍵字,當在抽象類中使用def來定義無參數的方法時,可以在子類中定義為val  

abstract class Pet (name: String) {
    def greeting: String
}

class Dog (name: String) extends Pet (name) {
    val greeting = "Woof"
}

object Test extends App {
    val dog = new Dog("Fido")
    println(dog.greeting)
}

  當在抽象類中定義具體的val字段時,你可以提供一個初始值,然后在子類中進行重寫 

abstract class Animal {
    val greeting = "Hello" // provide an initial value
    def sayHello { println(greeting) }
    def run
}

class Dog extends Animal {
    override val greeting = "Woof" // override the value
    def run { println("Dog is running") }
}

  在如下示例中,greeting在父類和子類中都定義了  

abstract class Animal {
    val greeting = { println("Animal"); "Hello" }
}

class Dog extends Animal {
    override val greeting = { println("Dog"); "Woof" }
}

object Test extends App {
    new Dog
}

  運行結果

Animal
Dog

  為了防止抽象類中的val字段被子類覆蓋,可以將其定義為final 

abstract class Animal {
    final val greeting = "Hello" // made the field 'final'
}

class Dog extends Animal {
    val greeting = "Woof" // this line won't compile
}

  你也可以在抽象類中定義var變量,並為其賦值,然后在具體子類中引用它們

abstract class Animal {
    var greeting = "Hello"
    var age = 0
    override def toString = s"I say $greeting, and I'm $age years old."
}

class Dog extends Animal {
    greeting = "Woof"
    age = 2
}

  greeting字段和age字段在Animal類中都被定義和初始化了,在Dog類中就沒有必要再重新聲明為var或val類型了

  在上述討論中,不應該使用null值,可以使用Option/Some/None來初始化字段  

trait Animal {
    val greeting: Option[String]
    var age: Option[Int] = None
    override def toString = s"I say $greeting, and I'm $age years old."
}

class Dog extends Animal {
    val greeting = Some("Woof")
    age = Some(2)
}

object Test extends App {
    val d = new Dog
    println(d)
}

  運行結果 

I say Some(Woof), and I'm Some(2) years old.

  2.14 使用樣例類生成樣板代碼

  1. 問題描述

  你需要使用樣例類來生成樣板代碼,如生成setter和getter方法,還有apply, unapply, toString, equalshashCode等方法

  2. 解決方案

  將類定義成樣例類,並且添加相應的構造函數參數

case class Person(name: String, relation: String)

  將類定義為樣例類后,會生成很多樣板代碼,如

    · 生成apply方法,所以不需要是new關鍵字就可以創建實例

    · 為構造函數參數生成getter方法,因為其默認是val的,當被定義為var時,會生成setter方法

    · 會生成缺省的toString方法

    · 生成unapply方法,在模式匹配中將會很方便

    · 生成equals和hashCode方法

    · 生成copy方法

  3. 討論

  樣例類在模式匹配中很實用,其默認參數為val,當創建樣例類時,Scala會為你生成很多樣板代碼

  2.15 定義equals方法(對象相等)

  1. 討論

  你想要定義一個equals方法來比較不同對象之間的相等性

  2. 解決方案

  如同Java一樣,你在類中定義equals和hashCode來比較兩個對象,但與Java不同的是,你使用 == 來比較兩個對象的相等性,下面定義了equals方法和hashCode方法 

class Person (name: String, age: Int) {
    def canEqual(a: Any) = a.isInstanceOf[Person]
    override def equals(that: Any): Boolean =
    that match {
        case that: Person => that.canEqual(this) && this.hashCode == that.hashCode
        case _ => false
    }
    
    override def hashCode:Int = {
        val prime = 31
        var result = 1
        result = prime * result + age;
        result = prime * result + (if (name == null) 0 else name.hashCode)
        return result
    }
}

  接着可以使用 == 來比較兩個Person對象是否相等

  3. 討論

  在Java中,==是比較兩個對象的引用是否相等,而在Scala中,其是根據用戶定義邏輯進行比較,其調用equals方法,在涉及繼承時,也可以使用如上方法 

class Employee(name: String, age: Int, var role: String)
    extends Person(name, age) {
    override def canEqual(a: Any) = a.isInstanceOf[Employee]
    override def equals(that: Any): Boolean =
        that match {
        case that: Employee =>
            that.canEqual(this) && this.hashCode == that.hashCode
        case _ => false
    }
    
    override def hashCode:Int = {
        val ourHash = if (role == null) 0 else role.hashCode
        super.hashCode + ourHash
    }
}

  2.16 創建內部類

  1. 問題描述

  你希望創建一個內部類,以將類排除在公共API之外,或者封裝您的代碼

  2. 解決方案

  將類聲明在另一個類中,如下示例  

class PandorasBox {
    case class Thing (name: String)
    
    var things = new collection.mutable.ArrayBuffer[Thing]()
    things += Thing("Evil Thing #1")
    things += Thing("Evil Thing #2")
    
    def addThing(name: String) { things += new Thing(name) }
}

  可以在PandorasBox內部使用things集合  

object ClassInAClassExample extends App {
    val p = new PandorasBox
    p.things.foreach(println)
}

  運行結果  

Evil Thing #1
Evil Thing #2

  並且可以調用addThing方法 

p.addThing("Evil Thing #3")
p.addThing("Evil Thing #4")

  3. 討論

  與Java的內部類屬於外部類不同,Scala中的內部類綁定到外部類的對象上

object ClassInObject extends App {
    // inner classes are bound to the object
    val oc1 = new OuterClass
    val oc2 = new OuterClass
    val ic1 = new oc1.InnerClass
    val ic2 = new oc2.InnerClass
    ic1.x = 10
    ic2.x = 20
    println(s"ic1.x = ${ic1.x}")
    println(s"ic2.x = ${ic2.x}")
}

class OuterClass {
    class InnerClass {
        var x = 1
    }
}

  運行結果  

ic1.x = 10
ic2.x = 20

三、總結

  本篇博文學習了Scala中類的相關知識點,對比Java學習還是相對簡單,也謝謝各位園友的觀看~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM