Scala練習八繼承
Scala繼承 |
摘要:
在本篇中,你將瞭解到Scala的繼承與Java和C++最顯著的不同。要點包括:
1. extends、final關鍵字和Java中相同
2. 重寫方法時必須用override
3. 只有主構造器可以呼叫超類的主構造器
4. 你可以重寫欄位
在本篇中,我們只探討類繼承自另一個類的情況。繼承特質的內容後面會詳細介紹
擴充套件類 |
擴充套件關鍵字
Scala擴充套件類的方式和Java一樣,使用extends關鍵字:
class Employee extends Person {
var salary=0.0
………
}
和Java一樣,你在定義中給出子類需要而超類沒有的欄位和方法,或者重寫超類的方法
不可擴充套件關鍵字
和Java一樣,你可以將類宣告為final,這樣它就不能被擴充套件。你還可以將單個方法或欄位宣告為final,以確保它們不能被重寫。注意這和Java不同,在Java中,final欄位是不可變的,類似Scala中的val。
重寫方法 |
重寫修飾符
在Scala中重寫一個非抽象方法必須使用override修飾符。例如:
public class Person {
………
override def toString = getClass.getName+ " [name=" + name+"]"
}
override修飾符可以在多個常見情況下給出有用的錯誤提示,包括:
■ 當你拼錯了要重寫的方法名
■ 當你不小心在新方法中使用了錯誤的引數型別
■ 當你在超類中引入了新的方法,而這個新的方法與子類的方法相抵觸
最後一種情況是易違約基類問題的體現,超類的修改無法在不檢查所有子類的前提下被驗證。假定程式設計師Alice定義了一個Person類,在Alice完全不知情的情況下,程式設計師Bob定義了一個子類Student,和一個名為id的方法,返回學生ID。後來,Alice也定義了一個id方法,對應該人員的全國範圍的ID。當Bob拿到這個修玫後,Bob的程式可能會出問題,但在Alice的測試案例中不會有問題,因為Student物件返回的不再是預期的那個ID了。
呼叫超類方法
在Scala中呼叫超類的方法和Java完全一樣,使用super關鍵字:
public class Employee extends Person {
………
override def toString=super.toString+"[salary="+ salary+"]"
}
super.toString會呼叫超類的toString方法,亦即Person.toString
型別轉換 |
測定類並轉換
要測試某個物件是否屬於某個給定的類,可以用islnstanceOf方法。如果測試成功,你就可以用aslnstanceOf方法將引用轉換為子類的引用:
if ( p.islnstanceOf[Employee]) {
val s : p.asInstanceOf[Employee] // s的型別為Employee
}
如果p指向的是Employee類及其子類的物件,則p.islnstanceOf[Employee]將會成功。如果p是null,則p.islnstanceOf[Employee]將返回false,且p.aslnstanceOf[Employee]將返回null。如果p不是一個Employee,則p.aslnstanceOf[Employee]將丟擲異常
如果你想要測p指向的是一個Employee物件但又不是其子類的話,可以用:
if ( p.getClass==classOf[Employee] )
classOf方法定義在scala.Predef物件中,因此會被自動引入。
型別轉換與模式匹配
下表顯示了Scala和Java的型別檢查和轉換的對應關係
不過,與型別檢查和轉換相比,模式匹配通常是更好的選擇。例如:
p match {
case s : Employee => … //將s作為Employee處理
case _ => // p不是Employee
}
關於模式匹配後面會詳細介紹
受保護的欄位和方法 |
和java或C++-樣,你可以將欄位或方法宣告為protected。這樣的成員可以被任何子類訪問,但不能從其他位置看到。與Java不同,protected的成員對於類所屬的包而言,是不可見的。如果你需要這樣一種可見性,則可以用包修飾符。
Scala還提供了一個protected[this]的變體,將訪問許可權定在當前的物件,類似介紹過的private[this]
超類構造器 |
呼叫父類構造器
一個類有一個主構造器和任意數量的輔助構造器,而每個輔助構造器都必須以對先前定義的輔助構造器或主構造器的呼叫開始。這樣做帶來的後果是,輔助構造器永遠都不可能直接呼叫超類的構造器。子類的輔助構造器最終都會呼叫主構造器,只有主構造器可以呼叫超類的構造器。主構造器是和類定義交織在一起的,呼叫超類構造器的方式也同樣交織在一起。這裡有一個示例:
class Employee ( name: String, age: Int, val salary: Double ) extends Person (name, age)
這段程式碼定義了一個子類:
class Employee ( name: String, age: Int, val salary: Double ) extends Person (name, age)
和一個呼叫超類構造器的主構造器:
class Employee ( name: String, age: Int, val salary: Double ) extends Person (name, age)
將類和構造器交織在一起可以給我們帶來更精簡的程式碼。把主構造器的引數當做是類的引數可能更容易理解。本例中的Employee類有三個引數:name、age和salary,
其中的兩個被"傳遞"到了超類。
Scala與Java比較
在Java中,與上述定義等效的程式碼就要囉嗦得多:
public classEmployee extends Person{ // Java
private double salary;
public Employee ( String name,int age,double salary ) {
super( name, age)
this.salary = salary;
}
}
需要注意的是,在Scala的構造器中,你不能呼叫super(params),不像Java,可以用這種方式來呼叫超類構造器。Scala類可以擴充套件Java類。這種情況下,它的主構造器必須呼叫Java超類的某一個構造方法。例如:
class Square(x: Int, y: Int, width: Int) extends java.awt.Rectangle (x,y,width, width)
重寫欄位 |
重寫欄位和方法
Scala的欄位由一個私有欄位和取值器/改值器方法構成。你可以用另一個同名的val欄位重寫一個val或不帶引數的def。子類有一個私有欄位和一個公有的getter方法,而這個getter方法重寫了超類的getter方法。例如:
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" // …或類名
}
重寫抽象方法
該示例展示了工作機制,但比較做作。更常見的案例是用val重寫抽象的def,就像這樣:
abstract class Person {
def id: Int // 每個人都有一個以某種方式計算出來的ID
}
class Student (override val id: Int) extends Person // 學生ID通過構造器輸入
注意如下限制:
■ def只能重寫另一個def
■ val只能重寫另一個val或不帶引數的def
■ var只能重寫另一個抽象的var
在類中用var沒有問題,因為你隨時都可以用getter/setter對來重新實現。不過,擴充套件你的類的程式設計師就沒得選了。他們不能用getter/setter來重寫var。換句話說,如果你給的是var,所有的子類都只能被動接受。
匿名子類 |
和Java一樣,你可以通過包含帶有定義或重寫的程式碼塊的方式建立一個匿名的子類,比如:
val alien = new Person ("Fred") {
def greeting = "Greet4ings, Earthling! My name is Fred. "
}
從技術上講,這將會創建出一個結構型別的物件。該型別標記為Person{ def greeting: String }。你可以用這個型別作為引數型別的定義:
def meet (p: Person { def greeting: String}) {
println(p.name + "says: " + p.greeting)
}
抽象類 |
抽象類&抽象方法
和Java一樣,你可以用abstract關鍵字來標記不能被例項化的類,通常這是因為它的某個或某幾個方法沒有被完整定義。例如:
abstract class Person(val name: String) {
def id: Int // 沒有方法體,這是一個抽象方法
}
在這裡我們說每個人都有一個ID,不過我們並不知道如何計算它。每個具體的Person子類,都需要給出id方法。在Scala中,不像Java你不需要對抽象方法使用abstract關鍵字,你只是省去其方法體。但和Java一樣,如果某個類至少存在一個抽象方法,則該類必須宣告為abstract
重寫超類抽象方法
在子類中重寫超類的抽象方法時,你不需要使用override關鍵字。
class Employee (name: String) extends Person(name) {
def id=name.hashCode // 不需要override關鍵字
}
抽象欄位 |
定義抽象欄位
除了抽象方法外,類還可以擁有抽象欄位。抽象欄位就是一個沒有初始值的欄位。例如:
abstract class Person {
val id : Int // 沒有初始化,這是一個帶有抽象的getter方法的抽象欄位
var name : String // 另一個抽象欄位,帶有抽象的getter和setter方法
}
該類為id和name欄位定義了抽象的getter方法,為name欄位定義了抽象的setter方法。
重寫欄位
生成的Java類並不帶欄位,具體的子類必須提供具體的欄位,例如:
class Employee ( val id: Int) extends Person { // 子類有具體的id屬性
var name=… // 和具體的name屬性
}
和方法一樣,在子類中重寫超類中的抽象欄位時,不需要override關鍵字。除此之外,你可以隨時用匿名型別來定製抽象欄位:
val fred = new Person {
val id=1729
var name="Fred"
}
構造順序和提前定義 |
構造順序
當你在子類中重寫val並且在超類的構造器中使用該值的話,其行為並不那麼顯而易見。有這樣一個示例:動物可以感知其周圍的環境。簡單起見,我們假定動物生活在一維的世界裡,而感知資料以整數表示。動物在預設情況下可以看到前方10個單位:
class Creature {
val range : Int=10
val env: Array[Int] = new Array[Int] ( range)
}
不過螞蟻是近視的:
class Ant extends Creature {
override val range=2
}
面臨問題
我們現在面臨一個問題:range值在超類的構造器中用到了,而超類的構造器先於子類的構造器執行。確切地說,事情發生的過程是這樣的:
1. Ant的構造器在做它自己的構造之前,呼叫Creature的構造器
2. Creature的構造器將它的range欄位設為10
3. Creature的構造器為了初始化env陣列,呼叫range()取值器
4. 該方法被重寫以輸出(還未初始化的)Ant類的range欄位值
5. range方法返回0。這是物件被分配空間時所有整型欄位的初始值
6. env被設為長度為0的陣列
7. Ant構造器繼續執行,將其range欄位設為2
雖然range欄位看上去可能是10或者2,但env被設成了長度為0的陣列。這裡的教訓是你在構造器內不應該依賴val的值。
解決方案
在Java中,當你在超類的構造方法中呼叫方法時,會遇到相似的問題。被呼叫的方法可能被子類重寫,因此它可能並不會按照你的預期行事。事實上,這就是我們問題的核心所在range表示式呼叫了getter方法。有幾種解決方式:
1. 將val宣告為final。這樣很安全但並不靈活
2. 在超類中將val宣告為lazy。這樣很安全但並不高效
3. 在子類中使用提前定義語法
提前定義語句
所謂的"提前定義"語法,讓你可以在超類的構造器執行之前初始化子類的val欄位。這個語法簡直難看到家了,估計沒人會喜歡。你需要將val欄位放在位於extends關
鍵字之後的一個塊中,就像這樣:
class Ant extends {
override val range=2
} with Creature
注意:超類的類名前的with關鍵字,這個關鍵字通常用於指定用到的特質。提前定義的等號右側只能引用之前已有的提前定義,而不能使用類中的其他欄位或方法。
提示:可以用-Xcheckinit編譯器標誌來除錯構造順序的問題。這個標誌會生成相應的程式碼,以便在有未初始化的欄位被訪問的時候丟擲異常,而不是輸出預設值。
說明:構造順序問題的根本原因來自Java語言的一個設計決定,即允許在超類的構造方法中呼叫子類的方法。在C++中,物件的虛擬函式表的指標在超類構造方法執行的時候被設定成指向超類的虛擬函式表。之後,才指向子類的虛擬函式表。因此,在c++中,我們沒有辦法通過重寫修改構造方法的行為。Java設計者們覺得這個細微差別是多餘的,Java虛擬機器因此在構造過程中並不調整虛擬函式表。☆☆☆
Scala繼承層級 |
下圖展示了Scala類的繼承層級:
■ 與Java中基本型別相對應的類,以及Unit型別,都擴充套件自AnyVal
■ 所有其他類都是AnyRef的子類,AnyRef是Java或.NET虛擬機器中Object類的同義詞。
■ AnyVal和AnyRef都擴充套件自Any類,而Any類是整個繼承層級的根節點
■ Any類定義了islnstanceOf、aslnstanceOf方法,以及用於相等性判斷和雜湊碼的方法
■ AnyVal並沒有追加任何方法,它只是所有值型別的一個標記
■ AnyRef類追加了來自Object類的監視方法wait和notify/notifyAII。同時提供了一個帶函式引數的方法synchronized。這個方法等同於Java中的synchronized塊。例如:
account.synchronized{ account.balance+=amount }
■ 所有的Scala類都實現ScalaObject這個標記介面,這個介面沒有定義任何方法
■ 在繼承層級的另一端是Nothing和Null型別。
■ Null型別的唯一例項是null值。可以將null賦值給任何引用,但不能賦值給值型別的變數
■ Nothing型別沒有例項。它對於泛型結構時常有用。
■ 空列表Nil的型別是List[Nothing],它是List[T]的子型別,T可以是任何類
注意:Nothing型別和Java或C++中的void完全是兩個概念。在Scala中,void由Unit型別表示,該型別只有一個值,那就是()。雖然,Unit並不是任何其他型別的超型別。但是,編譯器依然允許任何值被替換成()。考慮如下程式碼:
def printAny (x: Any) { println (x) }
def printUnit(x: Unit) { println (x) }
printAny ("Hello") // 將列印Hello
printUnit ( "Hello") // 將"Hello"替換成(),然後呼叫printUnit(()),打印出()
物件相等性 |
定義equals方法
在Scala中,AnyRef的eq方法檢查兩個引用是否指向同一個物件。AnyRef的equals方法呼叫eq。當你實現類的時候,應該考慮重寫equals方法,以提供一個自然的、與你的實際情況相稱的相等性判斷。舉例來說,如果你定義class Item (val description : String,val price : Double),你可能會認為當兩個物件有著相同描述和價格的時候它們就是相等的。以下是相應的equals方法定義:
final override def equals(other: Any) = {
val that = other.aslnstanceOf[Item]
if (that == null)
false
else
description == that.description && price == that.price
}
我們將方法定義為final,是因為通常而言在子類中正確地擴充套件相等性判斷非常困難。問題出在對稱性上。你想讓a.equals(b)和b.equals(a)的結果相同,儘管b屬於a的子類。與此同時還需注意的是,請確保定義的equals方法引數型別為Any。以下程式碼是錯誤的:
final def equals (other: Item) = { … }
這是一個不相關的方法,並不會重寫AnyRef的equals方法。
定義hashCode
當你定義equals時,記得同時也定義hashCode。在計算雜湊碼時,只應使用那些你用來做相等性判斷的欄位。拿Item這個示例來說,可以將兩個欄位的雜湊碼結合起來:
final override def hashCode=13*description.hashCode+17*price.hashCode
提示:你並不需要覺得重寫equals和hashCode是義務。對很多類而言,將不同的物件看做不相等是很正常的。舉例來說,如果你有兩個不同的輸入流或者單選按鈕,則完全不需要考慮他們是否相等的問題。
在應用程式當中,你通常並不直接呼叫eq或equals,只要用—操作符就好。對於引用型別而言,它會在做完必要的null檢查後呼叫equals方法