本文是對Scala語言面向對象編程的學習總結(上篇),共包括如下章節:
- 概述
- 類的定義
- 包(package)
- 成員訪問控制
- 對象
- 構造函數
- 小結
參考資料:
1、如果要了解scala開發環境的搭建,可參考《Scala學習筆記(1)-快速起步》 。
2、如果要了解scala語言的基本語法,可參考《Scala學習筆記(2)-基礎語法》。
3、如果要了解scala語言的面向函數的編程知識,可參考《Scala學習筆記(5)-函數式編程》。
一、概述
Scala是一種純面向對象的編程語言,其所有的值都是對象。Scala面向對象的機制和java,c++類似,一些通用的面向對象的特點都支持,也有些自己特有的特性。下面章節會詳細介紹。
二、類的定義
(一)基本概念
類是面向對象編程中的基本單元,類是創建對象的藍圖(或模板)。同java一樣,scala中的類通過關鍵字class定義,對象使用new關鍵字創建。
定義一個最簡單的類,只需要關鍵字class和類名。如下面例子:
class User
雖然scala不要求類名的首字母大寫,但建議大寫。定義了類之后,就可以創建對象,同java一樣,創建對象使用new關鍵字,如下面代碼:
var user = new User
println(user)
可以看出,User類沒有顯示的定義構造函數,有一個默認的空的構造函數。創建對象時,可以省略()。也就是說,var user = new User() 也是合法的。
面向對象編程的最基礎特征就是封裝,也就是將數據和操作放到一個類中,我們在面向對象的設計中,最重要的工作就是根據業務模型來設計類,將數據和匹配的操作封裝到一個類中。其中的數據和操作都稱為類的成員,而數據一般稱為類的成員變量,操作稱為類的成員方法。
下面我們來看看scala中如何定義成員變量和方法。
(二)成員變量
我們來看一個簡單例子:
class Person{
val name:String=""
var age:Int=20
}
上面定義了一個Person類,類中有2個成員變量,其中name變量用關鍵字val來定義,age變量用關鍵字var來定義的。對于scala類中的成員變量聲明,有如下特點:
1、var和val區別是,val定義的成員變量的值后續不能再改變。
2、成員變量聲明需要要初始化(否則就要定義為抽象類,關于抽象類的概念后面章節會介紹),如上面例子。如果變量有初始值,就可以省去類型定義,因為scala可以自動推斷。如下面定義類。
class Person{
val name=""
var age=20
}
3、對于var定義的成員變量,可以用占位符_來代替具體的值,如下面定義。
class Person{
val name=""
var age:Int = _
}
用占位符_來替換實際的值進行初始化,則該變量的值就是該類型的默認值,比如對于Int型的,就是0;對于引用型的(AnyRef子類的),如String,則默認值為null。
除了在類中定義成員變量外,還可以通過構造函數來定義成員變量,這個在后面章節會介紹。
(三)成員方法
在scala中,使用def關鍵字定義方法。 def后跟方法名,參數列表,返回值類型和方法體。語法格式如:
def 函數名(參數列表):返回值類型 = {
方法體代碼
}
下面看一個方法定義的例子:
def add(first:Int, second:Int):Int = {
val sum = first+second
return sum
}
上面定義了一個標準方法。
相比java中的方法,scala的方法定義比較靈活,有很多需要注意的細節,下面一一介紹:
1、scala的參數是不可變的,即不能給它重新賦值。
2、return關鍵字可省略,在scala中,所有語句都是bu表達式,表達式總是會返回一個值。省略了result, 方法體內最后一個被執行的語句作為表達式所返回的值就是方法的返回值。如:
def add(first:Int, second:Int) :Int= {
val sum = first+second
sum*2
}
3、如果方法體中只有一個語句,則可省略{},如:
def add(first:Int, second:Int) :Int = first+second
4、如果可以通過方法體最后一個表達式推斷出返回值類型,則方法的返回值類型聲明可以省略。如:
def add(first:Int, second:Int) = first+second
需要注意的是:如果方法中出現了return語句,則方法聲明必須顯示的定義返回值類型,如下面的代碼會編譯報錯:
def add(first:Int, second:Int) = first+second
如果省略了返回值類型,則也可以 = ,但不能省略{}。如下面語法也是正確的:
def add(first:Int, second:Int) = {
if(first>second)
return first+second
else
return first-second
}
5、Unit類型
在c/c++及java中,如果方法沒有返回值,則方法的返回值聲明為void。我們看看同樣代碼在scala中是什么情況。如下面方法定義:
def hello() = {
println("hello,world")
}
如果按照java語言來看,這個方法是沒有返回值的。我們來測試下,在scala交互式程序中定義和調用該方法,執行過程如下:
scala> def hello() = {
| println("hello,world")
| }
hello: ()Unit
scala> val re = hello()
hello,world
re: Unit = ()
scala> println(re)
()
對比前面例子的執行輸出。根據上面的執行輸出,可以看出hello方法是有返回值的,返回值類型為 Unit ,具體的值為()。Unit是scala中的一種數據類型,表示沒有值,其有唯一的實例()。在scala中,方法如果沒有返回具體的值,那返回的類型就設置為Unit類型。
(四)不帶()的方法定義
如果一個方法沒有參數,則方法名后面的()可以省略,如下面是正確的代碼:
class Demo{
def name = "tom"
}
上面代碼中的Demo類中定義了一個方法name,注意是方法,不是變量。這時name方法沒有帶()。對于不帶()的方法,在調用時也不能帶(),如name()這樣調用是錯誤的,只能name這樣調用。
如果 def name()= "tom"這樣定義時帶(),則調用時既可以帶(),也可以不帶()。看到這里,會覺得scala的語法棉花糖太多了。
不帶()的方法,還有一個特點時,子類可以重寫該方法,不僅可以用方法去重寫,也可以用變量去重寫。如下面代碼:
class Child extends Demo{
override val name="jack"
}
上面Demo類的子類Child重寫了name方法,但重寫的時候定義的是變量,而不是方法(當然重寫成方法也是沒問題的)。
Scala作者建議,如果一個方法在邏輯上表達一種屬性的返回值,那么在定義方法時盡量使用不帶括號的寫法,因為這樣看上去更像一個類的屬性,而不像一個方法。需要注意的是,由于不帶括號的方法比帶括號的方法在使用上更嚴格,因此將來要把一個帶括號的方法定義改為不帶括號的方法定義就比較麻煩,因為需要先將所有帶括號的方法調用,比如name(), 統統改為不帶括號的。
(五)方法參數的默認值
scala的方法參數支持設置默認值,對于有默認值的參數,調用方法時如果不傳入參數值,則使用默認值。如下面例子:
object Hello {
def main(args: Array[String]){
test()
test("jack")
test("jack",20)
}
def test(name:String="tom",age:Int=3)={
println(name,age)
}
}
上面代碼中定義的test方法,有兩個缺省值的參數。main方法中的三種調用方式都是正確的。
如果只有部分參數有默認值,則沒有有默認值的參數建議放在參數列表的前面,否則在不設置有默認值參數時需要以帶參數名的方式來調用方法。如下面例子:
object Hello {
def main(args: Array[String]){
test("jack",20)
test(age=20)
}
def test(name:String="tom",age:Int):Unit={
println(name,age)
}
}
scala的方法支持在調用時帶參數名調用,這樣傳入參數的順序就不必要按照方法定義參數的順序傳入。
(六)方法返回多個值
在Java/c++中我們知道,方法的返回值只能有一個值,如果我們需要返回多個值,只能返回一個對象(對象中有多個成員變量),或者返回一個數組,或集合對象。但在scala中,利用元組類型,yuanzu可以直接返回多個值,讓代碼編寫起來更加簡單,我們先看一個例子:
object Hello {
def main(args: Array[String]){
var (name,age) = test
println(name)
println(age)
}
def test={
("tom",12)
}
}
上面例子代碼中,test方法返回一個元組類型的值,在main方法中通過 var (name,age) = test 來提取test方法返回的元組中的所有元素。
(七)this關鍵字
方法是類的一部分,在方法的實現代碼中,可以直接訪問本類中的其它成員。this關鍵字代表對象本身,可以通過this來引用成員,這與java中的this關鍵字作用是一樣的。
當成員名和局部變量不沖突時,可以省去this關鍵字。如下面例子:
class Person{
var name:String="tom"
def show={
println(name)
}
def setName(name:String)={
this.name = name;
}
}
上面代碼中show方法直接使用了成員變量name,省去了this關鍵字。而setName方法中因為存在同名的局部變量,所以需要通過this關鍵字來引用成員變量。
(八)方法重載
同Java一樣,scala也支持方法重載,即一個類中存在多個同名的方法,但參數不同。調用時,scala會根據傳入的參數不同調用匹配的方法。如下面例子:
class Person{
var name:String="tom"
var age:Int=10
def setData(name:String)={
this.name = name;
}
def setData(name:String,age:Int)={
this.name = name;
this.age = age;
}
}
上面代碼中有兩個同名的方法setData,但它們的參數列表不同。
三、對象
(一)單例對象
在java中,在類中可以通過static關鍵字定義靜態的成員變量和方法,靜態成員是歸屬類的,與對象無關。可以直接通過類名來訪問。
在scala中,并不支持靜態成員。但是scala可以通過定義單例對象來實現同樣的功能。這里的單例對象不是如java中是一種設計模式,在scala中是一種具體的語法。
定義單例對象不使用class關鍵字,而是使用object關鍵字。我們先看一個簡單例子:
object User{
var name="tom"
}
val name = User.name
User.name = "jack"
上面代碼使用object關鍵字定義了一個單例對象,然后直接通過對象名即可訪問對象中的成員,類似Java中的靜態變量。在scala中,單例對象不是一個類,它就是一個對象(類似在java中自己實現的一個單例),只不過不需要由自己創建,而是由scala運行環境幫創建,并且在整個運行過程中只有一份,我們也不能通過new關鍵字來針對單例對象再創建一個對象。
在scala中,一個單獨可執行的scala程序,至少需要一個單例對象,且該單例對象中有一個 def main(args: Array[String]) 這樣的main方法,main方法就是程序的入口,類似Java中的靜態main方法作為java程序的入口。
(二)伴生類與伴生對象
在Java中,一個類中即可以定義靜態成員,也可以有實例成員。但在scala中,把這兩個分開了。靜態成員的功能由單例對象來實現。
在scala中,可以定義同名的單例對象和類。如果存在同名的單例對象和類,則該對象和類之間就會產生關系。在scala中,會把單例對象稱為同名類的伴生對象,會把類稱為同名單例對象的伴生類。如果一個單例對象沒有同名的伴生類,我們一般稱這個對象為孤立對象,反之稱為伴生對象。
舉個例子,假設我們定義了object(單例對象)Person和class(類)Person,則 object Person稱為class Person的伴生對象,而class Person則稱為object Person的伴生類。
伴生類和伴生對象有如下幾個注意的地方:
1、每個類都可以有伴生對象,伴生類與伴生對象寫在同一個文件中;
2、伴生類中和伴生對象可互相訪問其private字段。關于成員訪問控制,在下一章節會詳細介紹。
四、包(package)
(一)基本概念
scala中的package功能,同Java中的包、c++中的命名空間一樣,用于大型工程代碼的組織,同時解決命名沖突的問題。
scala中的package與java中的package很類似,如下面例子:
package com.demo
class Test{
def show ={
println("test")
}
}
上面定義的Test類,位于com.demo包下,包路徑加類名才是類的唯一標識。其它包中的類使用Test類時,可以使用com.demo.Test來引用,如:
object Hello {
def main(args: Array[String]){
var test = new com.demo.Test
test.show
}
}
顯然,如果在一個文件中多次使用到Test類,com.demo.Test這樣的寫法比較臃腫。同Java一樣,scala支持import語句來簡化代碼的編寫。如下面例子:
import com.demo.Test
object Hello {
def main(args: Array[String]){
var test = new Test
test.show
}
}
上面代碼通過import com.demo.Test導入了指定的類,這樣我們就可以使用Test類時不用帶包路徑。
如果我們希望將一個package中的所有類都導入,而不需要顯示的導入每個類,在java中用來代替具體的類名,如import com.demo. ,在scala中,是用_代替*。如下面例子:
import com.demo._
object Hello {
def main(args: Array[String]){
var test = new Test
test.show
}
}
對于單例對象,我們還可以將對象中的成員導入,這樣使用時可以直接使用成員名,不用寫單例對象名。類似java中的靜態導入。如下面例子:
package com.demo
object Test{
def show ={
println("test")
}
}
上面代碼定義了Test單例對象,我們希望直接使用其中的show方法,而不需要Test.show這樣使用,方法如下:
import com.demo.Test.show
object Hello {
def main(args: Array[String]){
show
}
}
同樣,我們可以使用import com.demo.Test._ 來導入Test對象中的所有成員,而不需要一個個的導入具體的成員。
(二)import的高級特性
import除了最常見的使用外(如上面例子),還有一些特殊的使用場景。
1、缺省導入
對于scala包中的所有類,不需要顯示的import,scala會自動導入。比如我們用到的基礎數據類型 Boolean , Int類型都位于scala包中,但我們使用時并沒有顯示導入。
對于scala.Predef(是一個單例對象)中的所有成員,scala會自動導入,不需要顯示導入。比如String類型,就是scala.Predef中的一個成員,我們可以直接使用。
2、重命名
如果同時需要使用兩個不同包中的同名的類,普通的import方式就會存在沖突。比如:
import java.util.HashMap
import scala.collection.mutable.HashMap
上面兩個語句都導入了相同名字的HashMap,如果我們代碼中直接使用HashMap,就會報錯。這時,我們當然可以使用全路徑來區分。但scala提供了一種重命名的方式,舉例如下:
import java.util.{ HashMap => JavaHashMap }
import scala.collection.mutable.HashMap
object Hello {
def main(args: Array[String]){
var map1:HashMap[String,String]=null
var map2:JavaHashMap[String,String]=null
}
}
上面代碼中第一個import語句將 HashMap 重命名為JavaHashMap ,這樣使用時就不會沖突了。
五、成員訪問控制
(一)回顧下Java中的方式
成員訪問控制是指訪問類成員時的權限控制。我們先回顧下Java中的成員訪問控制,在java中,對類成員的訪問有4種級別的控制,分別是:
private:私有成員,表示被private關鍵字修飾的成員只能被同一個類中的方法訪問。
默認方式:包內成員,表示沒有加任何修飾符的成員可以被同一個包(package)中的任何類的任何方法訪問。
protected:保護成員,表示被protected關鍵字修飾的成員可以被包內訪問,也可以被子類訪問。
public:公開成員,表示被public關鍵字修飾的成員可以在任何地方被訪問。
可以看出,java的上述4種級別的控制權限嚴厲程度從高到低,private最嚴格,只能被本類中方法訪問;public最寬松,可以在任何地方被訪問。private和public也是最常用的兩種權限控制級別。
我們再來看scala的訪問權限控制,與java類似,但也有些不同的地方。
(二)private方式
由private關鍵字修飾的類成員只能被該類的成員以及該類的伴生對象(后面會介紹)訪問,這點同java的prviate級別一樣。如下面例子:
class Demo{
private val value="hello"
def show{
println(value)
}
}
object Hello {
def main(args: Array[String]){
var demo = new Demo
demo.value=10 //會報編譯錯誤
}
}
上面例子中,Demo類中定義了一個private的成員變量value,value可以被類中的show方法訪問。但不能被外部類及非伴生單例對象訪問,如在Hello單例對象的main方法中訪問,會報編譯錯誤。
(三)protected方式
由protected關鍵字修飾的成員只能在該類及其子類中訪問,外部不能訪問,類似java中的protected權限控制。
(四)默認方式
沒有加任何關鍵字修飾的成員,可以在任何地方被使用,類似java中的public關鍵字,這點與java的默認方式完全不同。我們前面的很多例子都是沒加任何修飾符。
(五)private[this]方式
在scala中還存在使用private[this]修飾的成員,如下面代碼:
class Demo{
private[this] val value="hello"
}
被private[this]修飾的成員只能在類的內部使用。看到這里可能覺得奇怪,這與單獨的prviate關鍵字修飾有啥區別呢?
prvivate[this]修飾和prviate修飾非常類似,它們之間的最主要區別在于伴生對象的訪問權限控制。
class Demo{
private var name="tom"
private[this] var value="hello"
}
object Demo{
def show{
val demo = new Demo
demo.name = "jack"
demo.value = "world" //會報編譯錯誤
}
}
上面代碼中,Demo單例對象是Demo類的伴生對象,在單例對象中,可以訪問Demo類(即其伴生類)的private成員,但不能訪問private[this]成員,可以看出,prviate[this]比private的權限控制更嚴格。
同樣,我們看下伴生類中使用伴生對象的例子:
object Demo{
private var name="tom"
private[this] var value="hello"
}
class Demo{
def show{
Demo.name = "jack"
Demo.value = "world" //編譯報錯
}
}
在伴生類中,訪問伴生對象,可以直接訪問private成員,但不能訪問private[this]成員,會報編譯錯誤。
說明:scala還有包作用域的訪問控制方式,但需要改變packge和類的編寫方式,不太常用,這里就不作介紹。
六、構造函數
(一)基本概念
同java一樣,scala類也有構造函數,創建對象時會調用構造函數。我們先回顧下java的構造函數特點,一個java類可以顯示的定義0個或多個構造函數,有如下特點:
1)構造函數名必須與類名一樣,無返回值
2)如果沒有顯示定義構造函數,則java會自動幫創建一個默認的構造函數(沒有參數)
3)如果定義了構造函數,則java不會幫創建默認的構造函數
4)如果定義了多個構造函數,這些構造函數屬于重載,需要滿足方法重載的要求(即參數列表不能完全一致),這些構造函數無主次之分
5)創建對象時,java根據所調用的構造函數傳入的參數來決定調用哪個構造函數
scala的構造函數與java類似,但也有些差別,最主要的差別是:scala的構造函數分為主構造函數和輔助構造函數,有且只有一個主構造函數,可以有0個或多個輔構造函數。
(二)主構造函數介紹
下面我們來看下主構造函數的定義。在Scala中,每個類都有主構造函數。但在前面的例子中,我們定義的Person類中沒有定義主構造函數。這如java一樣,這時由scala自動幫生成了一個默認的不帶參數的主構造函數。
在scala中顯示定義帶參數的主構造函數,不是去單獨定義一個構造函數方法,而是和類的定義交織在一起。如下面例子:
class Person(var name:String,var age:Int){
}
下面我們來創建和使用Person對象,如下面例子:
scala> val per = new Person("tom",20)
per: Person = Person@57b33c29
scala> val name = per.name
name: String = tom
scala> per.name="jack"
per.name: String = jack
通過上面的例子代碼可以看出,我們要定義帶參數的主構造函數,只需在class后面的類名后面加上相應的參數,而且這些參數自動會變成類的成員變量(我們不能再在類中定義同名的成員變量了)。這比在java中完成同樣的功能所需代碼簡潔很多。
這里還有一個問題,我們知道,在Java中我們可以在構造函數中編寫代碼。但scala的主構造函數沒有對應的方法,如果我們希望在調用主構造函數創建對象時也能執行一些初始化操作,那代碼放在哪里呢? 在scala中,我們可以在類的主體(即{})中添加任意的代碼,這樣在創建對象時,從開始到結尾所有代碼都會被執行。如下面例子:
class Person{
val name=""
var age:Int = _
println("i am created")
age=12
println("name="+name+",age="+age)
}
下面我們創建一個對象,在scala命令行中執行會看到如下結果。
scala> var obj = new Person
i am created
name=,age=12
obj: Person = Person@2f2dc407
scala> val age = obj.age
age: Int = 12
可以看出,類中的這些代碼都被執行了。
我們在聲明主構造函數時,同普通的方法一樣,可以給參數設置默認值,這樣在創建對象時可以不傳入參數值。如下面例子:
scala> class Person(var name:String="tom",var age:Int=20){ }
defined class Person
scala> val per1 = new Person
per1: Person = Person@503fa290
scala> println(per1.name+"="+per1.age)
tom=20
scala> val per2 = new Person("jack",10)
per2: Person = Person@7beba1a8
scala> println(per2.name+"="+per2.age)
jack=10
上面代碼給主構造函數的參數設置了默認值,這時我們在創建對象時可以不傳入參數值,成員變量的初始值就是設置的默認值。當然我們也可以在創建對象時傳入值,這樣成員變量的值就是創建對象時傳入的值。
為主構造函數設置參數默認值,如果有多個參數,也可以只給部分參數設定默認值。如:
class Person(var name:String,var age:Int=12){ }
這樣創建對象可以不傳入有默認值的參數,如
new Person("jack")
當然,如果是前面參數有默認值,則創建對象時需要指定參數名,如:
class Person(var name:String="tom",var age:Int){ }
new Person(age=12)
(三)輔助構造函數
上面我們介紹了scala類的主構造函數,主構造函數有且只有一個。對于scala類,還可以顯示的定義0個或多個輔助構造函數。
對于輔助構造函數有如下要求:
1)以this作為方法名定義(不同于java中的以類名定義)
2)每一個輔助構造函數的第一行代碼必須是對主構造函數或其它的輔助構造函數的調用,通過this方法調用。這意味著主構造函數一定會被輔構造函數調用(直接或間接)。
3)輔助構造函數的參數不能加var或val標記。
4)輔助構造函數的參數不會成為成員變量,其作用是用來給成員變量傳值的。
class Person{
var name=""
var age:Int =0
def this(name:String)={
this()
this.name = name
}
def this(name:String,age:Int)={
this(name)
this.age = age
}
}
創建對象時,scala會自動根據傳入的參數來調用相匹配度的主構造函數或輔助構造函數。所以scala的各個構造函數之間要符合重載的要求。下面的創建對象都是正確的:
scala> var p = new Person
p: Person = Person@54972f9a
scala> var p = new Person("A")
p: Person = Person@525aadf2
scala> var p = new Person("A",12)
p: Person = Person@4a577b99
(四)主構造函數中的成員訪問控制
前面介紹主構造函數我們已經知道,可以通過主構造函數方便的為類設置成員變量,如下面例子:
class Person( var name:String)
上面代碼定義了Person,通過主構造函數聲明了一個成員變量name。這種方式下,name的權限都是默認方式,即公開方式。
如果我們希望設置name的訪問權限為private或protected,則只需在var關鍵字前面加上相應的關鍵字即可,如:
class Person( private var name:String)
如果我們希望name的訪問權限為private[this]方式,即完全私有的,則一種方式是加上private[this]修飾符,還有一種更簡單的方式是不使用var或val關鍵字定義,這樣聲明的成員變量就是private[this]的方式,如:
class Person(name:String){
def show{
println(name)
}
}
(五)構造函數的私有化
默認情況下,構造函數都是公有化的,在創建對象時可以直接調用。如果我們希望某個構造函數私有化,只在類的內部使用而不對外開放。這時只需加上private關鍵字修飾符。
1、對于主構造函數,private關鍵字加在類名和()之間。
2、對于輔助構造函數,private關鍵字加在def之前即可。
七、小結
本文對scala面向對象的編程的基本概念和特點進行了總結,涉及類的定義、單例對象、成員訪問控制、構造函數等內容。面向對象的編程還有很多重要特性,如繼承、多態等,這些會在下篇總結文章中介紹。