Scala中的類和構造器
Scala中的類
摘要
網路上很多資料講得不清不楚的,有些甚至是片面的錯誤的,看語言應該是直接用翻譯器將英文翻譯成中文。所以依照著網上的一些說法和自己的實驗重新將一些概念和定義講解一些。
scala編譯器會自動為類中的欄位新增getter方法和setter方法
可以自定義getter/setter方法來替換掉編譯器自動產生的方法
用@BeanProperty註解來生成JavaBeans的getXxx/setXxx()方法
每個類都有一個主要的構造器,這個構造器不是單獨宣告的建構函式,而是和類定義交織在一起。它的引數直接成為類的欄位。主構造器執行類宣告中所有的語句。
輔助構造器是可選的,它們叫做this。
簡單類和無參方法
簡單類
Scala類最簡單的形式看上去和Java或c+++的很相似:
class Counter {
private var value = 0 // 你必須初始化欄位
def increment() { value += 1 } // 方法預設是公有的
def current() = value
}
val myCounter = new Counter // 或new Counter()
myCounter.increment()
println(myCounter.current) // 1
無參方法
呼叫無參方法比如current時,你可以寫上圓括號,也可以不寫:
myCounter.current
myCounter.current()
應該用哪一種形式呢,我們認為對於改值器方法,即改變物件狀態的方法使用(),而對於取值器方法,它不會改變物件狀態的方法,所以去掉()。這也是我們在示例中的做法:
myCounter.increment() //對改值器使用()
println(myCounter.current) //對取值器不使用()
你可以通過以不帶()的方式宣告current來強制這種風格:
class Counter {
def current = value //定義中不帶()
}
這樣一來類的使用者就必須用myComter.current,不帶圓括號。
訪問級別
Java
修飾符 | Class | Package | Subclass | World |
public | Y | Y | Y | Y |
protected | Y | Y | Y | N |
no modifier | Y | Y | N | N |
private | Y | N | N | N |
Java的四種訪問級別在上面羅列了。成員變數、成員函式什麼的都是非常常見的使用方法,因為資料非常多,也就不詳細說明了。
唯一讓我覺得有趣的是
private class
的問題。雖然不知道在OOP中會不會有這樣的設計,但是我突然就想到了,便研究了一下。
對於一個.java的檔案,我們對於檔案的命名和檔案中類的命名有如下的規則:
- Java儲存的檔名必須與類名一致;
- 一個Java檔案中只能有一個頂層public類;
- 一個Java檔案中不能有一個頂層private類;
- 如果檔案中不止一個類,檔名必須與public類名一致;
- 如果檔案中不止一個類,而且沒有public類,檔名可與任一類名一致。
所謂的頂層類(外部類)指的是可以直接通過包來訪問的。
而private類是不能作為外部類的,因為沒有任何其他東西能夠訪問它。
private class SomePrivateClass{ ... }
會導致報錯。
使用的方式是在外面巢狀一個頂層類
public class OuterClass {
private class InnerClass {
...
}
...
}
Scala
在Scala中,沒有類似Java中那樣public
。它的預設修飾符,即no modifier
就是相當於public
。
修飾符 | Class | Companion | Subclass | Package | World |
no modifier | Y | Y | Y | Y | Y |
protected | Y | Y | Y | N | N |
private | Y | Y | N | N | N |
*: 表示頂層的protected
和private
成員是包內可見的。不是頂層的是不可見的。
這個頂層private
的包內可見性是我對這個問題產生興趣的關鍵。
package tprivate
import scala.beans.BeanProperty
private class Student { // 這樣定義是可以的
@BeanProperty
var age = 20
private var name = "clow"
def getName: String = this.name
def setName(value: String) {
this.name = value
}
}
object Demo {
def main(args: Array[String]): Unit = {
var instant = new Student //這樣是可以訪問的,或者在同一個包的其他檔案也是可以訪問的
println(instant.getAge())
}
}
package test
import tprivate.Student
object Testprivate {
def main(args: Array[String]): Unit = {
var instant = new Student // 這樣是無法訪問的
println(instant.getAge())
}
}
Scala中的欄位屬性
Scala對每個字端都會自動提供get和set方法。
例如,我們定義一個公有欄位:
class Person {
var age = 0
}
Scala編譯器生成能夠在JVM上執行的類,其中會有一個私有的age欄位以及相應的公有getter方法和setter方法。
若是我們將age宣告為private。Scala編譯器產生的getter和setter方法也是私有的。(關於這一點網上很多的說法都是不嚴謹的。)
Scala中的getter和setter
在Scala中,getter和setter分別叫做age和age_。
println (fred.age) // 將呼叫方fred.age()
fred.age= 21 // 將呼叫fred.age_=(21)
如果想親眼看到這些方法,可以編譯Person類,然後用java反編譯器編譯會.java檔案,或者用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,是因為JVM不允許在方法名中出現=。
所以在使用age
的時候,其實不是像C++那樣直接訪問變數,而是呼叫了相應的無引數方法。
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
Bertrand Meyer提出了統一訪問原則(Uniform access principle),內容如下:”某個模組提供的所有服務都應該能通過統一的表示法訪問到,至於它們是通過儲存還是通過計算來實現的,從訪問方式上應無從獲知”。
在Scala中,fred.age的呼叫者並不知道age是通過欄位還是通過方法來實現的。而C++就不是這樣,成員函式和成員變數的訪問是不同的。
還需注意的是:Scala對每個欄位生成getter和setter方法聽上去有些恐怖,不過你可以控制這個過程如下:
- 如果欄位是私有的,則getter和setter方法也是私有的
- 如果欄位是val,則只有getter方法被生成
- 如果你不需要任何getter或setter,可以將欄位宣告為
private[this]
這幾點在設計的時候非常重要,網路上關於這幾點講解得有些不太明確。
Bean屬性
正如你在前面所看到的,Scala對於你定義的欄位提供了getter和setter方法。不過,這些方法的名稱並不是Java工具所預期的,可參見JavaBeans規範。
把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) {
...
}
主構造器(建構函式)
組成
Scala類的主要建構函式是以下的組合:
- 建構函式引數
- 在類的主體中呼叫的方法
- 語句和表示式在類的主體中執行
// scala
class Person(var firstName: String, var lastName: String) { // primary constructor
println("the constructor begins")
// some class fields
private val HOME = System.getProperty("user.home")
var age = 0
// some methods
override def toString = s"$firstName $lastName is $age years old"
def printHome { println(s"HOME = $HOME") }
def printFullName { println(this) } // uses toString
// some expression
printHome
printFullName
println("still in the constructor")
}
object Demo {
def main(args: Array[String]): Unit = {
var instant = new Person("jing", "su")
}
}
/*
the constructor begins
HOME = C:\Users\lenovo
jing su is 0 years old
still in the constructor
*/
在Scala中,每個類都有主構造器,不需要單獨宣告建構函式,不以this方法定義,主構造器與類的定義或者欄位宣告交織在一起。當你閱讀一個Scala類時,你需要將它們分開理解,一個是類的定義,一個是建構函式的定義。
主構造器的引數直接放置在類名之後,並直接被編譯成欄位,其值被初始化成構造時傳入的引數。
在本例中lastname和firstname成為Person類的欄位。
這樣的構造器相比於Java程式碼,節約了極大的工作量。
// java
public class Person {
private String firstName;
private String lastName;
private final String HOME = System.getProperty("user.home");
private int age;
public Person (String firstName, String lastName) {
super ();
this.firstName = firstName;
this.lastName = lastName;
System.out.println("the constructor begins");
age = 0;
printHome();
printFullName();
System.out.println("still in the constructor");
}
public String firstName() {
return firstName;
}
public String lastName() {
return lastName;
}
public int age() {
return age;
}
public void firstName_$eq(String firstName) {
this.firstName = firstName;
}
public void lastName_$eq(String lastName) {
this.lastName = lastName;
}
public void age_$eq(int age) {
this.age = age;
}
public String toString() {
return firstName + " " + lastName + " is " + age + " years old";
}
public void printHome() {
System.out.println(HOME);
}
public void printFullName() {
System.out.println(this);
}
}
無參主構造器
如果類名之後沒有引數,則該類具備一個無參主構造器。這樣一個構造器僅僅是簡單地執行類體中的所有語句而已。你通常可以通過在主構造器中使用預設引數來避免過多地使用輔助構造器。例如:
class Person(val firstName: String = "", val lastName: String = "")
主構造器引數
主構造器的引數可以採用下表中列出的任意形態
例如:
class Person (val firstName: String, privite var lastName: String)
這段程式碼將宣告並初始化如下欄位:
val firstName: String
private var lastName: String
構造引數也可以是普通的方法引數,不帶val或var。這樣的引數是不可變得,而且不帶修飾符的引數和private val
還是有區別的。
他們的存在方式取決於它們在類中如何被使用。
class Person(private val firstName: String, lastName: String)
對於如上的類,將之編譯並用javap -v Person
檢視Java欄位之後,我們就可以發現僅僅只有firstName
欄位。而lastName
僅僅是構造器引數,在構造完成之後,就會被垃圾回收。
如果不帶val或var的引數至少被一個方法所使用,它將被編譯器自動升格為欄位。例如:
class Person(private val firstName: String, lastName: String) {
def fullName = firstName + " " + lastName
}
// if we'd defined fullName as a val instead of a def,
// it'd only have one field
如果我們將firstName
宣告為物件私有(object-private),而不是類私有(class-private),那麼它和無修飾符的引數作用類似。具體可以查閱參考文獻[5]的5.2章節。
class Person(private[this] val firstName: String, lastName: String)
class Person(private[this] var firstName: String, lastName: String)
主構造器引數生成欄位
下表總結了不同型別的主構造器引數對應會生成的欄位和方法:
如果主構造器的表示法讓你困惑,你不需要使用它。你只要按照常規的做法提供一個或多個輔助構造器即可,不過要記得呼叫this(),如果你不和其他輔助構造器串接的話。
話雖如此,許多程式設計師還是喜歡主構造器這種精簡的寫法。Martin Odersky建議這樣來看待主構造器:在Scala中,類也接受引數,就像方法一樣。當你把主構造器的引數看做是類引數時,不帶val或var的引數就變得易於理解了,這樣的引數的作用域涵蓋了整個類。因此,你可以在方法中使用它們。而一旦你這樣做了,編譯器就自動幫你將它儲存為欄位。
輔助構造器
除了主構造器之外,類還可以有任意多的輔助構造器(auxiliary constructor)。
我之所以後討論輔助構造器,是因為主構造器更重要也更難理解。當明確地理解了Scala主構造器和Java、C++等建構函式的區別後,就能更加輕鬆地理解輔助構造器,因為輔助構造器同Java或C++的建構函式十分相似,只有兩處不同。
- 輔助構造器的名稱為this。而在Java或C++中,構造器的名稱和類名相同。
- 每一個輔助構造器都必須以一個對先前已定義的其他輔助構造器或主構造器的呼叫開始
這裡有一個帶有兩個輔助構造器的類。
和Java、C++一一樣,類如果沒有顯式定義主構造器則自動擁有一個無參的主構造器即可。你可以以三種方式構建物件:
class Person {
private var name = ""
private var age = 0
def this(name: String) { // 輔助構造器1
this() // 呼叫主構造器
this.name = name
}
def this (name: String, age: Int) { // 輔助構造器2
this(name) //呼叫輔助構造器1
this.age = age
}
}
object Testprivate {
def main(args: Array[String]): Unit = {
var instant1 = new Person //主構造器
var instant2 = new Person("sujing") //輔助構造器1
var instant3 = new Person("sujing", 3) //輔助構造器2
}
}
巢狀類
Scala內嵌類
在Scala中,你幾乎可以在任何語法結構中內嵌任何語法結構。你可以在函式中定義函式,在類中定義類。以下程式碼是在類中定義類的一個示例:
class Network {
private val members = new ArrayBuffer[Member]
def join(name: String) = {
val m = new Member(name)
members += m
m
}
class Member(val name: String) {
val contacts = new ArrayBuffer[Member]
}
}
在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這個名稱很常見,但用在巢狀類中可能會引發歧義。