單例對象與類同名時,這個單例對象被稱爲這個類的伴生對象,而這個類被稱爲這個單例對象的伴生類。伴生類和伴生對象要在同一個源文件中定義,伴生對象和伴生類能夠互相訪問其私有成員。不與伴生類同名的單例對象稱爲孤立對象。編程
看看例子:緩存
import scala.collection.mutable.Map class ChecksumAccumulator { private var sum = 0 def add(b: Byte) { sum += b } def checksum(): Int = ~(sum & 0xFF) + 1 } 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) println("s:"+s+" cs:"+cs) cs } def main(args: Array[String]) { println("Java 1:"+calculate("Java")) println("Java 2:"+calculate("Java")) println("Scala :"+calculate("Scala")) } }
ChecksumAccumulator單例對象有一個方法,calculate,用來計算所帶的String參數中字符的校驗和。它還有一個私有字段,cache,一個緩存以前計算過的校驗和的可變映射。2方法的第一行,「if (cache.contains(s))」,檢查緩存,看看是否傳遞進來的字串已經做爲鍵存在於映射當中。若是是,就僅僅返回映射的值,「cache(s)」。不然,執行else子句,計算校驗和。else子句的第一行定義了一個叫acc的val並用新建的ChecksumAccumulator實例初始化它。下一行是個for表達式,對傳入字串的每一個字符循環一次,並在其上調用toByte把字符轉換成Byte,而後傳遞給acc所指的ChecksumAccumulator實例的add方法。完成了for表達式後,下一行的方法在acc上調用checksum,得到傳入字串的校驗和,並存入叫作cs的val。下一行,「cache += (s -> cs)」,傳入的字串鍵映射到整數的校驗和值,並把這個鍵-值對加入cache映射。方法的最後一個表達式,「cs」,保證了校驗和爲此方法的結果。測試
這裏打印的結果是:scala
s:Java cs:-130 Java 1:-130 Java 2:-130 s:Scala cs:-228 Scala :-228
問題來了,ChecksumAccumulator單例對象是不能new的,可是在代碼中出現了val acc = new ChecksumAccumulator,這不是矛盾嗎?其實否則,這裏new的實際上是ChecksumAccumulator單例對象的伴生類,即ChecksumAccumulator類,而伴生類和伴生對象能夠互相訪問對方的私有成員,因此acc能夠訪問ChecksumAccumulator單例對象的cache變量。同理,那麼第一次測試「Java」字符串的時候,acc實例執行了checksum()方法,接下來並把這個數據存到cache這個val中。在第二次測試「Java」字符串的時候,程序是直接從cache變量中獲取到了數據,並返回。對象
這裏也能夠看出類和單例對象的一個差異是,單例對象是在第一次訪問的時候初始化,不能夠new,不能帶參數,而類能夠new,能夠帶參數。每一個單例對象都被做爲由一個靜態變量指向的虛構類:synthetic class的一個實例來實現,所以它們與Java靜態類有着相同的初始化語法。blog
參考:《Scala編程》字符串