4.8 配置設定塊或函數給字段
- 使用代碼塊或者調用一個函數初始化類裡的字段
4.8.1 解決方案
- 設定字段等于需要的代碼塊或者函數
class Foo { // set 'text' equal to the result of the block of code val text = { var lines = "" try { lines = io.Source.fromFile("/etc/passwd").getLines.mkString } catch { case e: Exception => lines = "Error happened" } lines } println(text) } object Test extends App { val f = new Foo }
- 上面配置設定代碼塊給text字段和println語句都在類Foo的主體部分中,他們都是類的構造函數,當建立一個類的執行個體時他們會被執行。
- 同樣方式。配置設定方法或者函數給類字段
class Foo { import scala.xml.XML // assign the xml field to the result of the load method val xml = XML.load("http://example.com/foo.xml") // more code here ... }
4.8.2 讨論
- 如果定義一個字段為lazy,意味着不會立馬執行直到字段被擷取調用:
class Foo { val text = io.Source.fromFile("/etc/passwd").getLines.foreach(println) } object Test extends App { val f = new Foo }
- 上面代碼忽略潛在錯誤,如果運作在Unix系統,會列印/etc/passwd檔案
class Foo { lazy val text = io.Source.fromFile("/etc/passwd").getLines.foreach(println) } object Test extends App { val f = new Foo }
- 上面聲明成lazy字段的代碼編譯執行後,沒有任何輸出。因為text字段不會初始化直到它被擷取。
4.9 設定未初始化的var字段類型
- 問題: 想要設定一個未初始化var字段類型,是以開始寫代碼如下,然後如何完成表達式。
var x =
4.9.1 解決方案
- 一般來說,定義字段為Option。對于具體的類型,比如String和numeric字段,可以指定預設的初始化值,下面的address字段可以定義成Option,初始化如下。
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)
- 使用Some[Address]指派
val p = Person("alvinalexander", "secret") p.address = Some(Address("Talkeetna", "AK", "99676"))
- 如想要擷取address字段,有很多方法可見20.6章。可以使用foreach循環列印:
p.address.foreach { a => println(a.city) println(a.state) println(a.zip) }
- 如果address未指派,那麼address是一個None,調用foreach沒有問題,循環會自動跳出。如果已經指派,那麼address是一個Some[Address],循環會進入然後列印。
4.9.2 讨論
- 很容易建立一個Int和Double字段
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
- 檢視更多
- Option class
- 不要設定字段為null,更多見20.5章:“Eliminate null Values from Your Code”
- 20.6章:“Using the Option/Some/None Pattern”
4.10 當繼承類時處理構造函數參數
- 問題:當繼承一個基類時,需要處理基類聲明的構造函數參數以及子類新的參數
4.10.1 解決方案
- 往常一樣使用val或者var構造函數參數聲明基類,當定義子類構造函數時,去掉兩個類中相同字段前的val或者var聲明,當定義子類新的構造函數參數時使用val或者var聲明。
class Person (var name: String, var address: Address) { override def toString = if (address == null) name else s"$name @ $address" } case class Address (city: String, state: String)
- 子類Employee
class Employee (name: String, address: Address, var age: Int) extends Person (name, address) { // rest of the class }
- 建立Employee執行個體
val teresa = new Employee("Teresa", Address("Louisville", "KY"), 25)
- 輸出
scala> teresa.name res0: String = Teresa scala> teresa.address res1: Address = Address(Louisville,KY) scala> teresa.age res2: Int = 25
4.10.2 讨論
- 了解Scala編譯器如何轉換你的代碼有助于了解子類構造函數參數如何工作,下面代碼放到檔案Person.scala中:
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" }
- 上面字段是var變量,Scala編譯器生成了擷取器和修改器,編譯Person.scala反編譯Person.class如下:
$ javap Person Compiled from "Person.scala" public class Person extends java.lang.Object implements scala.ScalaObject{ public java.lang.String name(); public void name_$eq(java.lang.String); public Address address(); public void address_$eq(Address); public java.lang.String toString(); public Person(java.lang.String, Address); }
- 新的問題:如果定義一個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類裡的name和address聲明了getter和setter方法,解決方法是不使用var進行聲明:
// this is correct class Employee (name: String, address: Address) extends Person (name, address) { ... }
- 把下面代碼編譯反編譯,檔案名是Person.scala,反編譯Employee.class
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) { // code here ... }
- 反編譯結果如下:
$ javap Employee Compiled from "Person.scala" public class Employee extends Person implements scala.ScalaObject{ public Employee(java.lang.String, Address); }
- Employee繼承Person,Scala不為name和address字段生成getter和setter方法,Employee類繼承了Person類的行為。
4.11 調用超類構造函數
- 問題:想要控制當建立子類構造函數時調用的父類構造函數
4.11.1 解決方案
- 這有一個問題,你可以控制子類的主構造函數調用的父類構造函數,但是不可以控制子類的輔助構造函數調用的父類構造函數。
- 下面例子,定義一個Dog類去調用Animal類的主構造函數:
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") } // call the two-arg constructor class Dog (name: String) extends Animal (name, 0) { println("Dog constructor called") }
4.11.2 輔助構造函數
- 輔助構造函數的第一行必須調用目前類的另一個構造函數,不可能調用父類的構造函數
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) } }
4.12 使用抽象類(Abstract Class)
- 問題:Scala有特質(trait),而且特質比抽象類更靈活,那麼什麼時候使用抽象類
4.12.1 解決方案
- 以下兩點使用抽象類:
- 建立一個需要構造函數參數的基類
- Scala代碼會被Java代碼調用
- 特質不允許有構造函數參數:
// this won't compile trait Animal(name: String) // this compile abstract class Animal(name: String)
- 17.7章解決特質實作的方法不能被Java代碼調用的問題
4.12.2 讨論
- 一個類智能繼承一個抽象類。
- 聲明抽象方法:
def speak // no body makes the method abstract
- 抽象方法不需要使用abstract關鍵詞,去除方法的body就會變成抽象方法。這和在特質裡定義抽象方法是一緻的。
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) }
- 子類繼承之後需要實作抽象方法或者繼續聲明成抽象的,不實作方法會報出“class needs to be abstract”錯誤
scala> class WidgetController(db: Database) extends BaseController(db) <console>:9: error: class WidgetController needs to be abstract, since: method setServerName in class BaseController of type (serverName: String)Unit is not defined method getStatus in class BaseController of type => String is not defined method connect in class BaseController of type => Unit is not defined class WidgetController(db: Database) extends BaseController(db) ^
- 因為類隻能繼承一個抽象類,當決定使用特質還是抽象類時一般使用特質,除非基類需要構造函數參數
4.13 在抽象基類(或特質)中定義屬性
- 問題:在抽象基類(或特質)中定義抽象或具體屬性可供所有子類引用
4.13.1 解決方案
- 可以在抽象類或者特質裡聲明val和var字段。這些字段可以是抽象或者有具體實作。
4.13.2 抽象的val和var字段
- 下面抽象類有抽象的val和var字段,一個簡單的具體方法:
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" }
- 子類繼承抽象類,然後為抽象的字段指派,注意這些字段還是指定成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中示範調用:
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
4.13.3 讨論
- 抽象類(或特質)裡抽象字段的運作如下:
- 一個抽象的var字段會自動生成getter和setter方法
- 一個抽象的val字段會自動生成getter方法
- 當在抽象類或特質裡定義一個抽象字段,Scala編譯器不會在結果代碼裡建立一個字段,隻會根據val或者var生成相應的方法
- 上面的代碼通過 scalac -Xprint:all,或者反編譯Pet.class檔案,會發現沒有greeting或者age字段。反編譯輸出如下:
import scala.*; import scala.runtime.BoxesRunTime; public abstract class Pet { public abstract String greeting(); public abstract int age(); public abstract void age_$eq(int i); public void sayHello() { Predef$.MODULE$.println(greeting()); } public String toString(){ // code omitted } public Pet(String name){} }
- 是以當你在具體的類裡給這些字段提供具體的值時,必須重新定義字段為val或者var。因為在抽象類或者特質裡這些字段實際并不存在,是以override關鍵詞并不需要。
- 另一個結果,可以在抽象基類使用def定義無參取代使用val定義,然後可以在具體類裡定義成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) }
4.13.4 抽象類裡具體的val字段
- 抽象類裡定義一個具體的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
- 可以反編譯Animal和Dog類,greeting字段聲明成如下:
private final String greeting = "Hello";
- 抽象類中字段聲明成final val那麼具體子類中就不能重寫這個字段的值:
abstract class Animal { final val greeting = "Hello" // made the field 'final' } class Dog extends Animal { val greeting = "Woof" // this line won't compile }
4.13.5 抽象類裡具體var字段
- 可以在抽象類或特質為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" //調用setter方法 age = 2 }
- 這些字段在抽象基類裡聲明并指派,反編譯Animal類如下:
private String greeting; private int age; public Animal(){ greeting = "Hello"; age = 0; } // more code ...
- 因為在Animal基類裡這個字段已經聲明并且初始化,是以在具體子類裡沒有必要重新聲明字段。
- Dog類使用 scalac -Xprint:all 編譯:
class Dog extends Animal { def <init>(): Dog = { Dog.super.<init>(); Dog.this.greeting_=("Woof"); Dog.this.age_=(2); () } }
- 因為這個字段在抽象類裡是具體的,他們隻需要在具體子類裡重新指派即可
4.13.6 不要使用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.
4.14 Case類生成樣本代碼
- 問題: 在match表達式。actor或者其他使用case類生成樣本代碼的情況,生成包括擷取器,修改器,apply,unapply,toString, equals和hashCode等等方法。
4.14.1 解決方案
- 定義一個case類如下:
// name and relation are 'val' by default case class Person(name: String, relation: String)
- 定義一個case類會生成很多樣本代碼,有以下好處:
- 生成apply方法,是以不需要使用new關鍵詞去建立這個類的執行個體
- case類構造函數參數預設聲明成val,會自動生成擷取器方法,聲明成var會自動生成擷取器和修改器
- 生成預設的toString方法
- 生成unapply方法,可以在比對表達式輕松使用case類
- 生成equals和hashCode方法
- 生成copy方法
- 定義case類,建立一個新的執行個體時不需要使用new關鍵詞
scala> case class Person(name: String, relation: String) defined class Person // "new" not needed before Person scala> val emily = Person("Emily", "niece") emily: Person = Person(Emily,niece)
- 構造函數預設聲明成val,是以會自動生成擷取器方法,但不會生成修改器方法:
scala> emily.name res0: String = Emily scala> emily.name = "Fred" <console>:10: error: reassignment to val emily.name = "Fred" ^
- 構造函數參數聲明成var,會自動生成擷取器和修改器方法:
scala> case class Company (var name: String) defined class Company scala> val c = Company("Mat-Su Valley Programming") c: Company = Company(Mat-Su Valley Programming) scala> c.name res0: String = Mat-Su Valley Programming scala> c.name = "Valley Programming" c.name: String = Valley Programming
- Case類有一個預設的toString方法實作:
scala> emily res0: Person = Person(Emily,niece)
- 自動生成提取器方法(unapply),當需要在比對表達式提取資訊時很好用(構造器從給定的參數清單建立一個對象, 而提取器卻是從傳遞給它的對象中提取出構造該對象的參數):
scala> emily match { case Person(n, r) => println(n, r) } (Emily,niece)
- 自動生成equals和hashCode方法,執行個體可以如下方法比較:
scala> val hannah = Person("Hannah", "niece") hannah: Person = Person(Hannah,niece) scala> emily == hannah res1: Boolean = false
- 自動建立copy方法,當需要clone一個對象時很有幫助,在運作過程中還可以改變一些字段:
scala> case class Employee(name: String, loc: String, role: String) defined class Employee scala> val fred = Employee("Fred", "Anchorage", "Salesman") fred: Employee = Employee(Fred,Anchorage,Salesman) scala> val joe = fred.copy(name="Joe", role="Mechanic") joe: Employee = Employee(Joe,Anchorage,Mechanic)
4.14.2 讨論
- case類主要目的是建立“不可變的記錄”,這樣可以很容易的在模式比對表達式裡使用。
4.14.3 生成的代碼
- 檔案Person.scala:
case class Person(var name: String, var age: Int)
- 編譯後會建立兩個class檔案,Person.class和Person$.class
$ scalac Person.scala
- 反編譯Person.class
$ javap Person //結果 Compiled from "Person.scala" public class Person extends java.lang.Object ↵ implements scala.ScalaObject,scala.Product,scala.Serializable{ public static final scala.Function1 tupled(); public static final scala.Function1 curry(); public static final scala.Function1 curried(); public scala.collection.Iterator productIterator(); public scala.collection.Iterator productElements(); public java.lang.String name(); public void name_$eq(java.lang.String); public int age(); public void age_$eq(int); public Person copy(java.lang.String, int); public int copy$default$2(); public java.lang.String copy$default$1(); public int hashCode(); public java.lang.String toString(); public boolean equals(java.lang.Object); public java.lang.String productPrefix(); public int productArity(); public java.lang.Object productElement(int); public boolean canEqual(java.lang.Object); public Person(java.lang.String, int); }
- 反編譯Person$.class
$ javap Person$ //結果 Compiled from "Person.scala" public final class Person$ extends scala.runtime.AbstractFunction2 ↵ implements scala.ScalaObject,scala.Serializable{ public static final Person$ MODULE$; public static {}; public final java.lang.String toString(); public scala.Option unapply(Person); public Person apply(java.lang.String, int); public java.lang.Object readResolve(); public java.lang.Object apply(java.lang.Object,java.lang.Object); }
- 去掉case,然後編譯反編譯如下:
public class Person extends java.lang.Object{ public java.lang.String name(); public void name_$eq(java.lang.String); public int age(); public void age_$eq(int); public Person(java.lang.String, int); }
- 如果不需要那麼多額外的函數,考慮使用正常的類。如果隻想建立一個不适用new關鍵詞建立執行個體的類,如下使用:
val p = Person("Alex")
- 此時,可以建立一個apply方法。詳細看6.8章
- 檢視更多
- A Tour of Scala: Extractor Objects
4.15 定義一個equals方法(對象相等)
- 問題: 類中定義一個equals方法比較對象執行個體
4.15.1 解決方案
- 和Java一樣,定義一個equals(和hashCode)方法比較兩個執行個體,和Java不同的是,然後可以使用 == 方法比較兩個執行個體是否相等。
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 } }
- 上面例子顯示的是一個修改後的hashCode方法。
- 使用 == 方法比較兩個執行個體:
import org.scalatest.FunSuite class PersonTests extends FunSuite { // these first two instances should be equal val nimoy = new Person("Leonard Nimoy", 82) val nimoy2 = new Person("Leonard Nimoy", 82) val shatner = new Person("William Shatner", 82) val ed = new Person("Ed Chigliak", 20) // all tests pass test("nimoy == nimoy") { assert(nimoy == nimoy) } test("nimoy == nimoy2") { assert(nimoy == nimoy2) } test("nimoy2 == nimoy") { assert(nimoy2 == nimoy) } test("nimoy != shatner") { assert(nimoy != shatner) } test("shatner != nimoy") { assert(shatner != nimoy) } test("nimoy != null") { assert(nimoy != null) } test("nimoy != String") { assert(nimoy != "Leonard Nimoy") } test("nimoy != ed") { assert(nimoy != ed) } }
- 上面的測試建立在ScalaTest FunSuite,和JUnit單元測試類似
4.15.2 讨論
- Java中 == 操作符比較引用相等,Scala中 == 是比較兩個執行個體是否相等的方法。
- 當使用繼承時依舊可以繼續使用上面的方法
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 } //上面case that: Employee保證that是Employee類型,that.canEqual(this)保證this也是Employee類型 override def hashCode:Int = { val ourHash = if (role == null) 0 else role.hashCode super.hashCode + ourHash } }
- 上面的代碼使用canEqual,equals,hashCode相同方式,而且是一緻的,尤其是比較子類執行個體和其父類執行個體
class EmployeeTests extends FunSuite with BeforeAndAfter { // these first two instance should be equal val eNimoy1 = new Employee("Leonard Nimoy", 82, "Actor") val eNimoy2 = new Employee("Leonard Nimoy", 82, "Actor") val pNimoy = new Person("Leonard Nimoy", 82) val eShatner = new Employee("William Shatner", 82, "Actor") test("eNimoy1 == eNimoy1") { assert(eNimoy1 == eNimoy1) } test("eNimoy1 == eNimoy2") { assert(eNimoy1 == eNimoy2) } test("eNimoy2 == eNimoy1") { assert(eNimoy2 == eNimoy1) } test("eNimoy != pNimoy") { assert(eNimoy1 != pNimoy) } test("pNimoy != eNimoy") { assert(pNimoy != eNimoy1) } }
4.15.3 理論
- Scaladoc表述:“這個方法的任何實作都應該是等價關系”,等價關系應該有以下3個特征:
- 自反性(reflexive):Any類型的執行個體x,x.equals(x)傳回true
- 對稱性(symmetric):Any類型的執行個體x和y,x.equals(y)和y.equals(x)傳回true
- 傳遞性(transitive):AnyRef的執行個體x,y和z,如果x.equals(y)和y.equals(z)傳回true,那麼x.equals(z)也傳回true
- 是以如果重寫equals方法,應該确認你的實作保留了等價關系
- 檢視更多
- How to Write an Equality Method in Java
- Eric Torreborre shares an excellent canEqual example on GitHub
- “Equivalence relation” defined on Wikipedia
- The Scala Any class
4.16 建立内部類
- 希望建立一個類作為内部類并且保持在公開API之外,或者否則封裝你的代碼
4.16.1 解決方案
- 在一個類裡面聲明另一個類
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類的使用者不需要擔心Thing的實作就能獲得things集合
object ClassInAClassExample extends App { val p = new PandorasBox p.addThing("Evil Thing #3") p.addThing("Evil Thing #4") p.things.foreach(println) }
4.16.2 讨論
- Scala和Java不同,“不同于Java語言内部類是封閉類的成員,Scala中内部類和外部對象(object)綁定”:
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
- 更多用法,對象裡包括類,類裡包括對象:
object InnerClassDemo2 extends App { // class inside object println(new OuterObject.InnerClass().x) // object inside class println(new OuterClass().InnerObject.y) } object OuterObject { class InnerClass { var x = 1 } } class OuterClass { object InnerObject { val y = 2 } }
- 檢視更多
- A Tour of Scala: Inner Classes