Scala中的類 |
摘要:
在本篇中,你將會學習如何用Scala實現類。如果你瞭解Java或C++中的類,你不會覺得這有多難,並且你會很享受Scala更加精簡的表示法帶來的便利。本篇的要點包括:
1. 類中的欄位自動帶有getter方法和setter方法
2. 你可以用定製的getter/setter方法替換掉欄位的定義,而不必修改使用類的客戶端,這就是所謂的"統一訪問原則"
3. 用@BeanProperty註解來生成JavaBeans的getXxx/setXxx()方法
4. 每個類都有一個主要的構造器,這個構造器和類定義"交織"在一起。它的引數直接成為類的欄位。主構造器執行類體中所有的語句
5. 輔助構造器是可選的,它們叫做this
簡單類和無參方法 |
簡單類
Scala類最簡單的形式看上去和Java或c+++的很相似:
class Counter {
private var value=0 //你必須初始化欄位
def increment() {value+=1} //方法預設是公有的
def current()=value
}
在Scala中,類並不宣告為public。Scala原始檔可以包含多個類,所有這些類都具有公有可見性。使用該類需要做的就是構造物件並按照通常的方式來呼叫方法:
val myCounter=new Counter // 或new Counter()
myCounter.increment()
println (myCounter.current) // 1
無參方法
呼叫無參方法比如current時,你可以寫上圓括號,也可以不寫:
myCounter.current //OK
myCounter.current() //同樣OK
應該用哪一種形式呢,我們認為對於改值器方法,即改變物件狀態的方法使用(),而對於取值器方法不會改變物件狀態的方法去掉()是不錯的風格。這也是我們在示例中的做法:
myCounter.increment() //對改值器使用()
println (myCounter.current) //對取值器不使用()
你可以通過以不帶()的方式宣告current來強制這種風格:
class Counter {
def current=value //定義中不帶()
}
這樣一來類的使用者就必須用myComter.current,不帶圓括號。
帶getter和setter的屬性 |
Java中的欄位屬性
編寫Java類時,我們並不喜歡使用公有欄位:
public class Person{ // 這是Java
public int age; //Java中不鼓勵這樣做
}
使用公有欄位的話,任何人都可以寫入fred.age,讓Fred更年輕或更老。這就是為什麼我們更傾向於使用getter和setter方法:
public class Person{ //這是Java
private int age;
public int getAge() { return age; }
public void setAge{int age) { this.age=age; }
}
像這樣的一對getter/setter通常被稱做屬性(property),我們會說Person類有一個age屬性。這到底好在哪裡呢,僅從它自身來說,並不比公有欄位來得更好。任何人都可以呼叫fred.setAge(21),讓他永遠停留在21歲。不過如果這是個問題,我們可以防止它發生:
public void setAge (nt newValue) {// 不能變年輕
if ( newValue>age )
age=newValue;
}
之所以說getter和setter方法比公有欄位更好,是因為它們讓你可以從簡單的get/set機制出發,並在需要的時候做改進。需要注意的是:僅僅因為getter和setter方法比公有欄位更好,並不意味著它們總是好的。通常,如果每個客戶瑞都可以對一個物件的狀態資料進行獲取和設定,這明顯是很糟糕的。下面,會向展示如何用Scala實現屬性。但要靠你自己決定,可以取值和改值的欄位是否是合理的設計
Scala中的欄位屬性
Scala對每個字端都提供getter和setter方法。在這裡,我們定義一個公有欄位:
class Person {
var age=0
}
Scala生成面向JVM的類,其中有一個私有的age欄位以及相應的getter方法和setter方法。這兩個方法是公有的,因為我們沒有將age宣告為private。而對私有欄位而言,getter和setter方法也是私有的。
Scala中的getter和setter
在Scala中,getter和setter分別叫做age和age_=例如:
println (fred.age) // 將呼叫方fred.age()
fred.age= 21 // 將呼叫fred.age=(21)
如果想親眼看到這些方法,可以編譯Person類,然後用javap檢視位元組碼:
scalac Person.scala
javap -private Person
Compiled from "Person.scala"
public class Person extends java.lang.Object implements scala.ScalaObject {
private int age;
public int age()
public void age_$eq(int)
public Person()
}
正如你看到的那樣,編譯器建立了age和age_$eq方法。=號被翻譯成$eq,是因為JVM不允許在方法名中出現=
說明:在Scala中,getter和setter方法並非被命名為getXxx和setXxx,不過它們的用意是相同的。後面會介紹如何生成Java風格的getXxx和setXxx方法,以使得你的Scala類可以與Java工具實現互操作
Scala中的自定義getter和setter
在任何時候你都可以自己重新定義getter和setter方法。例如:
class Person {
private var privateAge =0 // 變成私有並改名
def age = privateAge
def age_= (newValue: Int) {
if (newValue > privateAge)
privateAge=newValue // 不能變年輕
}
}
你的類的使用者仍然可以訪問fred.age,但現在Fred不能變年輕了:
fred.age = 30
fred.age = 21
println (fred.age) // 30
頗具影響的Eiffel語言的發明者Bertrand Meyer提出了統一訪問原則,內容如下:"某個模組提供的所有服務都應該能通過統一的表示法訪問到,至於它們是通過儲存還是通過計算來實現的,從訪問方式上應無從獲知"。在Scala中,fred.age的呼叫者並不知道age是通過欄位還是通過方法來實現的。當然了,在JVM中,該服務總是通過方法來實現的,要麼是編譯器合成,要麼由程式設計師提供。
還需注意的是:Scala對每個欄位生成getter和setter方法聽上去有些恐怖,不過你可以控制這個過程如下:
■ 如果欄位是私有的,則getter和setter方法也是私有的
■ 如果欄位是val,則只有getter方法被生成
■ 如果你不需要任何getter或setter,可以將欄位宣告為private[this]
只帶getter的屬性 |
Scala類中的常量
有時候你需要一個只讀屬性,有getter但沒有setter。如果屬性的值在物件構建完成後就不再改變,則可以使用val欄位:
class Message {
val timeStamp=new java.util.Date
……
}
Scala會生成一個私有的final欄位和一個getter方法,但沒有setter。
私有欄位的getter和setter
不過,有時你需要這樣一個屬性,客戶端不能隨意改值,但它可以通過某種其他的方式被改變。前面中的Counter類就是個很好的例子,從概念上講,counter有一個current屬性,當increment方法被呼叫時更新,但並沒有對應的setter
class Counter {
private var value=0 //你必須初始化欄位
def increment() {value+=1} //方法預設是公有的
def current()=value
}
需要注意的是,你不能通過val來實現這樣一個屬性,val永不改變。你需要提供一個私有欄位和一個屬性的getter方法,像這樣:
class Counter {
private var value=0 //你必須初始化欄位
def increment() {value+=1} //方法預設是公有的
def current=value //宣告中沒有()
}
在getter方法的定義中並沒有(),因此,你必須以不帶圓括號的方式來呼叫:
val n=myCounter.current // myCounter.current()這樣的呼叫方式是語法錯誤
總結
總結一下,在實現屬性時你有如下四個選擇:
■ var foo: Scala自動合成一個getter和一個setter
■ val foo: Scala自動合成一個getter
■ 由你來定義foo和foo_=方法
■ 由你來定義foo方法
但在Scala中,你不能實現只寫屬性,即帶有setter但不帶getter的屬性。當你在Scala類中看到欄位的時候,記住它和Java或c++中的欄位不同。它是一個私有欄位,加上getter方法(對val欄位而言)或者getter和setter了法(對var欄位而言)
對私有欄位 |
類私有欄位
在Scala中Java和C++也一樣,方法可以訪問該類的所有物件的私有欄位。例如:
class Counter {
private var value=0
def increment () {value+=1 }
def isLess (other: Counter) = value < other.value // 可以訪問另一個物件的私有欄位
}
之所以訪問other.value是合法的,是因為othert也同樣是Cormter物件。
物件私有欄位
除此之外Scala允許我們定義更加嚴格的訪問限制,通過private[this]這個修飾符來實現:
private [this] var value=0 // 類似某個物件.value這樣的訪問將不被允許
這樣一來,Counter類的方法只能訪問到當前物件的value欄位,而不能訪問同樣是Counter型別的其他物件的該欄位。這樣的訪問有時被稱為物件私有的,這在某些OO語言,比如SmaIITalk中十分常見。對於類私有的欄位,Scala生成私有的getter和setter方法。但對於物件私有的欄位,Scala根本不會生成getter或setter方法。
許可權指定
Scala允許你將訪問權賦予指定的類,private[類名]修飾符可以定義僅有指定類的方法可以訪問給定的欄位。這裡的類名必須是當前定義的類,或者是包含該類的外部類。在這種情況下,編譯器會生成輔助的getter相setter方法,允許外部類訪問該欄位。這些類將會是公有的,因為JVM並沒有更細粒度的訪問控制系統,並且它們的名稱也會隨著JVM實現不同而不同。
Bean屬性 |
正如你在前面所看到的,Scala對於你定義的欄位提供了getter和setter方法。不過,這些方法的名稱並不是Java工具所預期的。JavaBeans規範
www.oracle.com/technetwork/java/javase/tech/index-jsp-138795.html
把Java屬性定義為一對getFoo/setFoo方法或者對於只讀屬性而言單個getFoo方法。許多Java工具都依賴這樣的命名習慣。當你將Scala欄位標註為@BeanProperty時,這樣的方法會自動生成。例如:
import scala.reflect.BeanProperty
class Person {
@BeanProperty var name: String=_
}
將會生成四個方法:
■ name:String
■ name_=(newValue: Strmg):Unit
■ getName():String
■ setName(newValue: String): Unit
下表顯示了在各種情況下哪些方法會被生成:
如果你以主構造器引數的方式定義了某欄位,並且你需要JavaBeans版的getter和setter方法,像如下這樣給構造器引數加上註解即可:
class Person (@BeanProperty var name: String)
輔助構造器 |
和Java或C++一樣,Scala可以有任意多的構造器。不過Scala類有一個構造器比其他所有構造器都更為重要,它就是主構造器(primary constructor)。除了主構造器之外,類還可以有任意多的輔助構造器( auxiliary constructor)我們將首先討論輔助構造器,這是因為它們更容易理解。它們同Java或C++的構造器十分相似,只有兩處不同。
■ 輔助構造器的名稱為this。在Java或C++中,構造器的名稱和類名相同,當你修改類名時就不那麼方便了
■ 每一個輔助構造器都必須以一個對先前已定義的其他輔助構造器或主構造器的呼叫開始
這裡有一個帶有兩個輔助構造器的類。
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
}
}
和Java、C++一一樣,類如果沒有顯式定義主構造器則自動擁有一個無參的主構造器即可。你可以以三種方式構建物件:
val p1 = new Person //主構造器
val p2 = new Person("Fred")//第一個輔助構造器
val p3 = new Person ("Fred",42) //第二個輔助構造器
主構造器 |
主構造器的引數直接放置在類名之後
在Scala中,每個類都有主構造器。主構造器並不以this方法定義,而是與類定義交織在一起
class Person ( val name:String, val aqe:Int) {
// (…)中的內容就是主構造器的引數
}
主構造器的引數被編譯成欄位,其值被初始化成構造時傳入的引數。在本例中name和age成為Person類的欄位。如new Person("Fred",42)這樣的構造器呼叫將設定name和age欄位。我們只用半行Scala就完成了七行Java程式碼的工作:
public class Person{ //這是Java
private String name;
private int age;
public Person(String name,int age) {
this.name=name
this.age=age
}
public String name() {return this.name;}
public int age() {raturn this.age;}
}
主構造器會執行類定義中的所有語句。例如在以下類中:
class Person (val name: String, val age: Int) {
println ("Just constructed anther person")
def description=name+"is"+age+"years old"
}
println語句是主構造器的一部分。每當有物件被構造出來時,上述程式碼就會被執行。當你需要在構造過程當中配置某個欄位時這個特性特別有用。例如:
class MyProg {
private val props=new Properties
props.load ( new FileReader ( "myprog.properties" ) ) // 上述語句是主構造器的一部分
}
類名之後沒有引數
如果類名之後沒有引數,則該類具備一個無參主構造器。這樣一個構造器僅僅是簡單地執行類體中的所有語句而已。你通常可以通過在主構造器中使用預設引數來避免過多地使用輔助構造器。例如:
class Person (val name:String="",val age: Int =0 )
主構造器引數
主構造器的引數可以採用下表中列出的任意形態
例如:
class Person (val name : String, privite 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欄位的效果。否則,該引數將不被儲存為欄位。它僅僅是一個可以被主構造器中的程式碼訪問的普通引數。嚴格地說,這是一個具體實現相關的優化。
主構造器引數生成欄位
下表總結了不同型別的主構造器引數對應會生成的欄位和方法:
如果主構造器的表示法讓你困惑,你不需要使用它。你只要按照常規的做法提供一個或多個輔助構造器即可,不過要記得呼叫this(),如果你不和其他輔助構造器串接的話。話雖如此,許多程式設計師還是喜歡主構造器這種精簡的寫法。Martin Odersky建議這樣來看待主構造器:在Scala中,類也接受引數,就像方法一樣。當你把主構造器的引數看做是類引數時,不帶val或var的引數就變得易於理解了,這樣的引數的作用域涵蓋了整個類。因此,你可以在方法中使用它們。而一旦你這樣做了,編譯器就自動幫你將它儲存為欄位。
類定義與主構造器
Scala設計者們認為每敲一個鍵都是珍貴的,因此他們讓你可以把類定義和主構造器結合在一起。當你閱讀一個Scala類時,你需要將它們分開。舉例來說,當你看到如下程式碼時:
class Person (val name: String) {
var age=0
def description=name+"is"+age+"years old"
}
把它拆開成一個類定義:
class Person (val name: String) {
var age = 0
def description = name+"is"+age+"years old"
}
和一個構造器定義:
class Person(val name: String) {
var age = 0
daf description= nama+"is"+age+"years old"
}
如果想讓主構造器變成私有的,可以像這樣放置private關鍵字:
class Person private ( val id: Int ) { … }
這樣一來類使用者就必須通過輔助構造器來構造Person物件了
巢狀類 |
Scala內嵌類
在Scala中,你幾乎可以在任何語法結構中內嵌任何語法結構。你可以在函式中定義函式,在類中定義類。以下程式碼是在類中定義類的一個示例:
import scala.collection.mutable.ArrayBuffer
class Network {
class Member(val name: String) {
val contacts = new ArrayBuffer[Member]
}
private val members=new ArrayBuffer[Member]
def join(name: String) ={
val m=new Member(name)
members+=m
m
}
}
在Scala中,每個例項都有它自己的Member類,就和它們有自己的members欄位一樣,考慮有如下兩個網路:
val chatter = new Network
val myFace = new Network
也就是說,chatter.Member和myFace.Member是不同的兩個類。這和Java不同,在Java中內部類從屬於外部類。Scala採用的方式更符合常規,舉例來說,要構建一個新的內部物件,你只需要簡單的new這個類名:new chatter.Member。而在Java中,你需要使用一個特殊語法:chatter.new Member()。拿我們的網路示例來講,你可以在各自的網路中新增成員,但不能跨網新增成員:
val fred = chatter.join("Fred")
val wilma=chatter.join ("Wilma")
fred.contacts+=wilma //OK
val barney=myFace.join ("Barney") // 型別為myFace .Member
fred.contacts+=barney // 不可以這樣做,不能將一個myFace.Member新增到chatter.Member元素緩衝當中
Scala內嵌類訪問
對於社交網路而言,這樣的行為是講得通的。如果你不希望是這個效果,有兩種解決方式。首先,你可以將Member類移到別處,一個不錯的位置是Network的伴生物件。
object Network {
class Member (val name: String) {
val contacts=new ArrayBuffer[Member]
}
}
class Network{
private val members = new ArrayBuffer[Network.Member]
}
或者,你也可以使用型別投影Network#Member,其含義是"任何Network的Member"。例如:
class Network {
class Member (val name: String) {
val contacts = new ArrayBuffer[Network#Member]
}
}
如果你只想在某些地方,而不是所有地方,利用這個細粒度的"每個物件有自己的內部類"的特性,則可以考慮使用型別投影。
內嵌類訪問外部類
在內嵌類中,你可以通過外部類.this的方式來訪問外部類的this引用,就像Java那樣。如果你覺得需要,也可以用如下語法建立一個指向該引用的別名:☆☆
class Network(val name: String){ outer=>
class Member (val name: String) {
def dascription=name+"inside"+outer.name
}
}
class Network { outer=>語法使得outer變數指向Network.this。對這個變數,你可以用任何合法的名稱。self這個名稱很常見,但用在巢狀類中可能會引發歧義。這樣的語法和"自身型別"語法相關,將會後面內容繼續介紹
如果,您認為閱讀這篇部落格讓您有些收穫,不妨點選一下右下角的【推薦】。
如果,您希望更容易地發現我的新部落格,不妨點選一下左下角的【關注我】。
如果,您對我的部落格所講述的內容有興趣,請繼續關注我的後續部落格,我是【Sunddenly】。本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。