Scala類與對象


類簡介

簡介

類是對象的藍圖。一旦你定義了類,就可以用關鍵字new根據類的藍圖創建對象。在類的定義里,可以放置字段和方法,這些被籠統地稱為成員。對於字段,不管是val還是var定義的,都是指向對象的變量。對於方法,用def定義,包含了可執行代碼。字段保留了對象的狀態或數據,而方法使用這些數據執行對象的運算工作。當類被實例化的時候,運行時環境會預留一些內存來保留對象的狀態映像——即變量的內容。

示例

創建類示例:

class SumAccumulator {
    var sum = 0
}

然后實例化兩次:

val acc: SumAccumulator = new SumAccumulator
val csa: SumAccumulator = new SumAccumulator 

這時內存里對象的狀態映像如下:

 

由於字段sum是var類型而非val,所以sum可以被賦予新的Int類型的值:例如

acc.sum = 3

此時映像會變成:

 

圖中同時存在兩個sum變量,一個在acc指向的對象里,另一個在csa指向的對象里。字段的另一種說法是實例變量(instance variable),因為每一個實例都有自己的變量集。

盡管acc和csa都是val類型,但是還是可以修改acc指向的對象的值(上面的sum)。val類型對象對acc(或csa)的限制僅限於不可以把它們再次賦值給其他對象。例如,下面的嘗試將會失敗:

//編譯不過,因為acc是val
acc = new ChecksumAccumuulator

 

因此,我們可以得出結論,acc將始終指向初始化的Checksuumaccumulator對象,但是對象包含的字段可以隨時改動。

Scala類的重要特性

訪問修飾符

  • Public是Scala默認的訪問級別

為了保證對象的健壯性,可以把類中字段變為私有的(private)以阻止外界直接對它訪問。因為私有字段只能被定義成在同一類里的方法訪問,所有跟新字段的代碼將鎖定在類里。要聲明字段是私有的,可以把訪問修飾符private放在字段的前面。

代碼示例:

class SumAccumulator {
    private var sum = 0
} 

使用private修飾后,任何從類外部對sum的訪問都將失敗

val acc: SumAccumulator = new SumAccumulator
acc.sum = 3 //編譯不通過,因為sum是私有的

 

方法

1. Scala方法參數類型

Scala方法參數中的類型都是val而不是var類型

def add(a: Int, b: Int): Int = {
    a = a + b //編譯錯誤:Reassignment to val (就是對val類型賦值的錯誤)
    a
}    

2. Scala方法中return

Scala方法中的return可以去掉,從而簡化代碼

代碼示例:

def numSum(num:Int):Int ={
    val sum = num + 10
    sum //省略return關鍵字,簡化代碼
 
}

此時返回值就是sum

3. Scala方法中的“=”號

Scala方法中的“=”號非常重要:因為Scala編譯器可以把任何類型轉換為Unit如果定義的函數中”=”號忘記了添加,那么Scala會默認它返回值為Unit類型。若你本來想返回一個String類型的值,由於沒有添加”=”,String類型返回值會被轉換為Unit類型而丟棄掉。

代碼示例:

def printStr() {
    "這是String類型的返回值"
}

返回結果為:   ()

正確代碼示例:

def printStr(): String = {
    "這是String類型的返回值"
}

 返回結果為:    這是String類型的返回值

4. Scala方法表達式

假如某個方法僅計算單個結果的表達式,這可以去掉花括號,如果結果表達式很短,甚至可以把它放在def的同一行里。

代碼示例:

def numSum(num:Int):Int = num + 10

5. Scala中分號推斷

scala程序中,語句末尾的分號通常是可選的,若一行僅有一個語句也可以不加分號。不過,如果一行包含多條語句時,分號則是必須的。

不加分號:

if(x < 2)
println("too small")
else
println("ok")

必須加分號:

val x = 10; println(x)  //兩個語句,必須加分號

Scala通常的風格是把操作符放在行尾而不是行頭:

錯誤示例:

val x = 10;
val y = 3
val z = x //它會被編譯器識別為z = x ; +y 兩個語句
+y

打印結果:10

正確示例:

val x = 10;
val y = 3
val z = x +
y
println(z)

打印結果:13

Scala分號推斷規則:

  1. 疑問行由一個不能合法作為語句結尾的字結束,如句點或中綴操作符。
  2. 下一行開始於不能作為語句開始的詞。
  3. 行結束於括號(...)或方框[...]內部,因為這些符號不能容納多個語句。

6. Scala無參方法

調用Scala類中無參方法時,可以寫上括號,也可以不寫。對於類中的取值器來說,去掉()是不錯的風格

代碼示例:

object exam1 {
def main(args: Array[String]): Unit = {
val per: Person = new Person
per.talk() //ok
per.talk //同樣ok
}
 
}
 
class Person {
def talk(): Unit = println("Talking")
}

如果你想強制使用這種風格,可以在聲明方法時不帶()

代碼示例:

per.talk() //此時這種寫法是錯誤的
per.talk //OK的
class Person {
def talk = println("Talking")
}

Singleton對象

Scala比Java更為面向對象的特點之一是Scala不能定義靜態成員,而是代之以定義單例對象(singleton Object)。除了用object關鍵字替換了class關鍵字以外,單例對象的定義看上去與類定義一致。

例子:ChecksumAccumulator.scala源碼:

class ChecksumAccumulator {
    private var sum = 0
    def add(b:Byte){sum += b}
    def checksum():Int = ~(sum & 0xFF) + 1
}
import scala.collection.mutable.Map
object ChecksumAccumulator {
    private val cache = Map[String, Int]()
    def calculate(s:String):Int=
        if(cache.contains(s))
            cache(s)
        else {
            val acc = new ChecksumAccumulator
            for(c <- s)
                acc.add(c.toByte)
            val cs = acc.checksum()
            cache += (s -> cs)
            cs
        }
}

上面源碼中的單例對象叫做ChecksumAccumulator,與前一個例子里的類同名。當單例對象與某個類共享同一個名稱時,它就被稱為是這個類的伴生對象(companion object)。類和它的伴生對象必須定義在一個源文件里。類被稱為是這個單例對象的伴生類(companion class)。類和它的伴生對象可以相互訪問其私有成員。

這段緩存代碼的說明如下:

類和單例對象間的差別是,單例對象不帶參數,而類可以。因為單例對象不是用new關鍵字實例化的,所以沒有機會傳遞給它實例化參數。每個單例對象都被實現為虛擬類(synthetic class)的實例,並指向靜態的變量,因為它們與Java靜態類有着相同的初始化語義。特別要指出的是,單例對象在第一次被訪問的時候才會被初始化。

不與伴生類共享名稱的單例對象被稱為獨立對象(standalone object)。它可以用在很多地方,例如作為相關功能方法的工具類,或者定義Scala應用的入口點。

Scala程序

  想要編寫能夠獨立運行的Scala程序,就必須創建有main方法(僅帶一個參數Array[String],且結果類型為Unit)的單例對象。任何擁有合適簽名的main方法的單例對象都可以用來作為程序的入口點。

Summer.scala文件源碼:

import ChecksumAccumulator.calculate

object Summer {
    def main(args:Array[String]) {
        for(arg <- args)
            println(arg + ": " + calculate(arg))
    }
}

要執行Summer應用程序,需要把以上的代碼寫入文件Summer.scala中,因為Summer使用了ChecksumAccumulator,所以還要把ChecksumAccumulator的代碼,上面的源碼(類及它的伴生對象),放在文件ChecksumAccumulator.scala中。

Scala和Java之間有一點不同,Java需要類名稱與源碼文件名同名,而在Scala對於源文件的命名沒有硬性規定。然而通常情況下如果不是腳本,推薦的風格是像在Java里那樣按照所包含的類名來命名文件,這樣程序員就可以比較容易地根據文件名找到類。

Scala的腳步必須以結果表達式介紹。因此如果你嘗試以腳本方式執行Summer.scala,Scala解釋器將會報錯說Summer.scala不是以結果表達式結束的。正確做法是:需要用Scala編譯器真正的編譯這些文件,然后執行輸出的類文件,方式之一使用Scala的基本編譯器,scalac。

D:\work\workspace\scala>scalac ChecksumAccumulator.scala Summer.scala

D:\work\workspace\scala>

D:\work\workspace\scala>fsc ChecksumAccumulator.scala Summer.scala

D:\work\workspace\scala>

 

D:\work\workspace\scala>scala Summer of love
of: -213
love: -182

D:\work\workspace\scala>

Scala中的Application

為了使代碼更簡潔,Scala還提供了另外一種運行Scala程序的方式,那就是直接繼承scala.Application接口(Trait)。
直接繼承自Application的運行方式:
object RunAppWithoutMain extends Application {  
    println("runing scala app without main")  
}
之所以這里無須定義main方法,那是 因為在Application這個接口中定義了一個main方法,main方法在執行時會初始化RunAppWithoutMain這個對象,並執行它的主構造方法,而所有直接寫在對象中的代碼都會被scala編譯器收集到主構造方法中,於是就被運行了。
extends Application雖然比編寫main方法要方便,但是也有一些副作用。直接繼承自Application導致的副作用:
1. 無法接受命令行參數。因為args參數不會被傳入
2. 在Scala中,如果一個程序是多線程的,那么這個程序必須具有一個main方法。所以第二種寫法只能適用於單線程的程序
3. Application這個接口在執行一個程序的代碼之前,需要進行一些初始化。而某些JVM不會對這些初始化代碼進行優化。

所以第二種方法只適用於一些非常簡單的場合,大部分情況不推薦使用。
 

Scala類中getter和setter

getter和setter

Scala類中使用公有字段的話,任何人都可以修改這個字段,使得安全性大大降低。所以我們更傾向於使用getter和setter方法:

Scala對類中每個字段都提供了getter和setter方法,分別叫做age和age_=,

代碼示例:

創建一個exam1.scala文件,文件內容如下

class Person {
var age = 0
}

如果想查看這些方法,可以先編譯Person類,用scalac命令編譯,然后用javap查看字節碼:

D:\work\workspace\scala>scalac exam1.scala

D:\work\workspace\scala>javap Person
Compiled from "exam1.scala"
public class Person {
  public int age();
  public void age_$eq(int);
  public Person();
}

D:\work\workspace\scala>

 

 

你可以自己重新定義getter和setter方法。

代碼示例:

object exam1 {
 
    def main(args: Array[String]): Unit = {
        val per: Person = new Person
        per.age = 18
        per.age = 16 //由於在setter里面控制了age不能變小,所以執行結果age不會變
        println(per.age)
        per.age = 19 //使用setter,賦予大於原來的age
        println(per.age)
        }
    }
 
    class Person {
        private var privateAge = 0 //變成私有變量並改名
        def age = privateAge
        def age_=(newAge: Int) { // age_= 不能有空格
        if (newAge > privateAge) privateAge = newAge //使得年齡不能變小
    }
}

打印結果為:

18
19

Scala中每個字段生成getter和setter的限制:

  1. 如果字段是私有的,則getter和setter方法也是私有的。
  2. 如果字段是val,則只有getter方法被生成。
  3. 如果你不需要任何getter或setter,可以將字段聲明為private[this]

Scala在實現類中屬性時的四個選擇:

  1. 自動生成一個getter和setter。
  2. 自動生成一個getter。
  3. 自定義foo和foo_=方法。
  4. 自定義foo方法。

Bean屬性

JavaBean規范把Java屬性定義為一堆getFoo和setFoo方法。類似於Java,當你將Scala字段標注為 @BeanProperty時,getter和setter方法會自動生成。

代碼示例:

import scala.beans.BeanProperty
 
object exam1 {
    def main(args: Array[String]): Unit = {
        val per: Person = new Person
        per.name = "zhagnsan"
        per.setName("lisi") //BeanProperty生成的setName方法
        println(per.getName) //BeanProperty生成的getName方法
    }
}
 
 
class Person {
    @BeanProperty var name:String = _
}

打印結果為:

Lisi

上述類Person中由@BeanProperty生成了四個方法:

name: String
name_= (newValue: String): Unit
getName(): String
setName (newValue: String): Unit

圖示為針對字段生成的方法:

 

Scala類中構造器

和java或C++一樣,Scala也可以有任意多的構造器。不過,Scala類有一個構造器比其他所有構造器都更為重要,它就是主構造器。除了主構造器外,Scala類還可以有任意多的輔助構造器。

輔助構造器

Scala中輔助構造器和Java或C++十分相似,只有兩處不同:

  1. 輔助構造器的名稱為this。
  2. 每一個輔助構造器都必須以一個對先前已定義的其他輔助構造器或主構造器的調用開始

這里有一個帶有兩個輔助構造器的類。

代碼示例:

object exam1 {
    def main(args: Array[String]): Unit = {
        val per1: Person = new Person //主構造器
        val per2: Person = new Person("Bob") //第一個輔助構造器
        val per3: Person = new Person("Bob",18) //第二個輔助構造器
    }
}
 
    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
        }
    }    

主構造器

在Scala中,每個類都有主構造器。主構造器並不以this方法定義,而是與類定義交織在一起

主構造器的參數直接放在類名之后

代碼示例:

object exam1 {
def main(args: Array[String]): Unit = {
val per: Person = new Person("Bob", 18) //使用主構造器實例化對象
println(per.name + " : " + per.age)
}
}
 
class Person(val name: String, val age: Int) {
//產生私有的name和age,沒有setter
//.....
}

打印結果:

Bob : 18

上述簡短的Person類定義極大簡化了相同功能的Java代碼:

與上例相同功能的Java代碼示例:

class Person {
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() {
return this.age;
}
}

 

Scala主構造器定義的所有語句都會執行

代碼示例:

class Person(val name: String, val age: Int) {
//產生私有的name和age,沒有setter
println("主構造器定義的所有語句都會執行") //每次使用主構造器時都會執行
def description = name + "is" + age + "years old"
}

不同類型的主構造器參數對應會生成的字段和方法:

 

如果想讓主構造器變成私有的,可以像這樣放置private關鍵字:

class Person private (val name: String, val age: Int) {
//......
}

這樣一來,類用戶就必須通過輔助構造器來構造Person對象了.

Scala嵌套類

在Scala中,你幾乎可以在任何語法結構中內嵌任何語法構造。你可以在函數中定義函數,在類中定義類。

代碼示例:

class Network {
 
//在Network類中定義類Member
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
}
}

 

考慮有如下兩個網絡:

al chatter = new Network
val myFace = new Network
 

在Scala中,每個實例都有它自己的Member類(內部類),就和它們有自己的members(內部類)字段一樣。也就是說chatter.Member和myFace.Member是不同的兩個類

作用:拿網絡示例來說,你可以在各自的網絡中添加成員,但是不能跨網添加成員。

代碼示例:

val chatter = new Network
val myFace = new Network
val fred = chatter.join("Fred")
val wilma = chatter.join("wilma")
fred.contacts += wilma //OK
val barney = myFace.join("Barney")
 
//錯誤:不能將一個myFace.Member添加到chatter.Member元素緩沖中
fred.contacts += barney

在嵌套類中,你可以通過 外部類.this 的方式來訪問外部類的this引用,就像Java那樣。

class Network(val name: String) {
//在Network類中定義類Member
class Member(val name: String) {
//....
def description = name + "inside" + Network.this.name
}
}

如果你覺得需要,也可以使用如下語法建立一個指向該引用的別名:

class Network(val name: String) { outer =>
//在Network類中定義類Member
class Member(val name: String) {
//....
def description = name + "inside" + outer.name
}
}

上述語法使得outer變量指向Network.this。對這個變量,你可以使用任何合法的名稱。

轉自:https://blog.csdn.net/u011204847/article/details/51105362


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM