9. 面向對象高級
9.1 靜態屬性和靜態方法
① 回顧 Java 的靜態概念
public static 返回值類型 方法名(參數列表) {方法體}
Java
中靜態方法並不是通過對象調用的,而是通過類對象調用的,所以靜態操作並不是面向對象的
② Scala 中靜態的概念-伴生對象
Scala 語言是完全面向對象(萬物皆對象)的語言,所以並沒有靜態的操作(即在 Scala
中沒有靜態的概念)。但是為了能夠和 Java
語言交互(因為 Java
中有靜態概念),就產生了一種特殊的對象來模擬類對象,我們稱之為類的伴生對象。這個類的所有靜態內容都可以放置在它的伴生對象中聲明和調用

③ Scala 伴生類和伴生對象示例代碼
object Demo4 {
def main(args: Array[String]): Unit = {
println(Woman.sex) // 底層調用的是 Woman$.Module$.sex()
Woman.shopping() // 底層調用的是 Woman$.Module$.shopping()
}
}
/**
* 當我們吧同名的 class 和 object 放在同一個 .scala 文件中,class xxx 就是伴生類,object xxx 就是伴生對象
* 通常開發中會將成員屬性/方法寫在伴生類中,將靜態屬性/方法寫在伴生對象中
* class xxx 編譯後的文件 xxx.class 會有對 object xxx 的屬性或方法操作
* 而 object xxx 編譯後會生成 xxx$.class ;也就是說 class xxx 編譯後的文件會組合 object xxx 編譯後的 xxx.class
*/
class Woman {
var name = "woman"
}
object Woman {
val sex = 'F'
def shopping(): Unit = {
println("女人天生愛購物")
}
}
輸出

我們將字節碼文件反編譯後再看看原理



說明
Woman$
字節碼文件先生成Woman$
類型的靜態對象Woman
類中會自動生成object Woman
中定義的靜態方法和靜態屬性對應的getter/setter
方法,這些方法的實質都是通過對Woman$
靜態對象的相應方法調用
圖示

小結
Scala
中伴生對象採用object
關鍵字修飾,伴生對象中聲明的都是靜態內容,可以通過伴生對象名稱直接調用伴生對象對應的伴生類,他們的名字必須一致
從語法的角度,伴生對象其實就是類的靜態方法和靜態屬性的集合
從編譯的角度,伴生對象會生成一個靜態的實例,由伴生類中的靜態方法去調用這個靜態實例的方法
伴生對象和伴生類的聲明需要放在同一個
Scala
源文件中如果
class xxx
獨立存在一個Scala
源文件中,那麼xxx
就只是一個沒有靜態內容的類;如果object xxx
獨立存在一個Scala
源文件中,那麼xxx
會編譯生成一個xxx.class
和xxx$.class
,object xxx
中聲明的方法和屬性可以直接通過xxx.屬性
和xxx.方法
直接調用示例代碼
object Demo4 {
def main(args: Array[String]): Unit = {
add()
}
def add(): Unit = {
println("abc")
}
}
- 如果
Scala
源文件有一個伴生類及其對應的伴生對象,IDEA
中會如下顯示
object Girl {
}
class Girl{
}

④ 練習:小孩子玩遊戲
object Demo5 {
def main(args: Array[String]): Unit = {
val c1 = new Child("cris")
val c2 = new Child("james")
val c3 = new Child("申惠善")
Child.add(c1)
Child.add(c2)
Child.add(c3)
Child.showNum()
}
}
class Child(var name: String) {
}
object Child {
var num = 0
def add(c: Child): Unit = {
println(s"${c.name} 加入了遊戲")
num += 1
}
def showNum(): Unit = {
println(s"當前共有 $num 個小孩子再玩遊戲")
}
}
輸出

總結:可以像類一樣使用 object
,只需要記住 object
中定義的內容都是靜態類型的即可;object
還可以當做工具類使用,只需要定義工具方法即可
⑤ apply 方法
通過 object
的 apply
方法可以直接使用 類名(實參)
的方式來生成新的對象
object Practice {
def main(args: Array[String]): Unit = {
// val bag1 = new Bag("LV")
// apply 方法被調用...
// Bag 的主構造被調用
val bag = Bag("Channel")
}
}
class Bag(var name: String) {
println("Bag 的主構造被調用")
}
object Bag {
def apply(name: String): Bag = {
println("apply 方法被調用...")
new Bag(name)
}
}
⑥ 練習
- 將程序的運行參數進行倒敘打印,並且參數之間使用
-
隔開
println(args.toBuffer)
private val str: String = args.reverse.mkString("-")
println(str)
}

執行如下

編寫一個撲克牌 4 種花色的枚舉,讓其
toString
方法分別返回♣
,♦
,♥
,♠
,並實現一個函數,檢查某張牌的花色是否為紅色先試試
type
關鍵字
object Exer extends App {
// type 相當於是給數據類型起別名
type MyString = String
val name: MyString = "cris"
println(name) // cris
println(name.getClass.getName) // java.lang.String
}
然後再來完成練習
object Exer extends App {
println(Suits) // ♠,♣,♥,♦
println(Suits.isDiamond(Suits.Diamond)) // true
println(Suits.isDiamond(Suits.Heart)) //false
}
object Suits extends Enumeration {
type Suits = Value
val Spade = Value("♠")
val Club = Value("♣")
val Heart = Value("♥")
val Diamond = Value("♦")
override def toString(): String = Suits.values.mkString(",")
def isDiamond(s: Suits): Boolean = s == Diamond
}
9.2 單例
單例對象是指:使用單例設計模式保證在整個的軟件系統中,某個類只能存在一個對象實例
① 回顧 Java 單例對象
在 Java
中,創建單例對象分為餓漢式(加載類信息就創建單例對象,缺點是可能造成資源浪費,優點是線程安全)和懶漢式(使用時再創建單例對象)
通過靜態內部類實現懶漢式單例:
構造器私有化
類的內部創建對象
向外暴露一個靜態的公共方法
代碼實現
public class Main {
public static void main(String[] args) {
Single instance = Single.getInstance();
Single instance1 = Single.getInstance();
// true
System.out.println("(instance1==instance) = " + (instance1 == instance));
}
}
class Single {
private Single() {
}
/**
* 靜態內部類:1. 使用時才加載;2. 加載時,不會中斷(保證線程安全)
*/
private static class SingleInstance {
private static final Single INSTANCE = new Single();
}
public static Single getInstance() {
return SingleInstance.INSTANCE;
}
}
② Scala 中的單例模式
Scala
中實現單例異常簡單~
只需要寫一個 object
,就相當於創建了一個對應的單例對象
object SingletonDemo {
def main(args: Array[String]): Unit = {
}
}
object Singleton {
val name = "singleton"
def init(): Unit = {
println("init...")
}
}
看看反編譯後的代碼


圖示:

9.3 特質(重點)
① 回顧 Java 的接口
在Java中, 一個類可以實現多個接口。
在Java中,接口之間支持多繼承
接口中屬性都是常量
接口中的方法都是抽象的(Java 1.8 之前)
② Scala 的特質(trait)
簡介
從面向對象來看,接口並不屬於面向對象的範疇,Scala
是純面向對象的語言,在 Scala
中,沒有接口
Scala
語言中,採用特質 trait
(特徵)來代替接口的概念,也就是說,多個類具有相同的特徵(特徵)時,就可以將這個特質(特徵)獨立出來,採用關鍵字 trait
聲明
特質的聲明
trait 特質名{
trait 體
}
特質的基礎語法

說明
類和特質關係,使用繼承的關係;因為
Scala
的特質,有傳統interface
特點,同時又有抽象類特點當一個類去繼承特質時,第一個連接詞是
extends
,後面是with
如果一個類在繼承特質和父類時,應當把父類寫在
extends
後
③ trait 傳統使用案例
可以將 trait
當做傳統的接口使用
請根據以下圖示,使用 trait
完成需求

代碼如下:
object Demo {
def main(args: Array[String]): Unit = {
val c = new C
val f = new F
c.getConnection()
f.getConnection()
}
}
trait Connection {
def getConnection()
}
class A {}
class B extends A {}
class C extends A with Connection {
override def getConnection(): Unit = println("連接 MySQL 數據庫")
}
class D {}
class E extends D {}
class F extends D with Connection {
override def getConnection(): Unit = println("連接 HBase 數據庫")
}
我們看看反編譯後的代碼



代碼說明
如果我們創建了一個
trait
, 該trait
只有抽象的方法,那麼在底層就只會生成一個interface
繼承了
trait
的類,必須實現trait
的抽象方法(這點和Java
一樣)
特質的進一步說明
Scala
提供了特質(trait
),特質可以同時擁有抽象方法和具體方法,一個類可以實現/繼承多個特質代碼演示
object Demo2 {
def main(args: Array[String]): Unit = {
val account: Account = new BankAccount
account.check
account.info
}
}
trait Account {
def check
def info: Unit = {
println("account info")
}
}
class BankAccount extends Account {
override def check: Unit = {
println("需要提供銀行賬號和密碼進行驗證")
}
}
看看編譯後的代碼,Scala
是如何實現的?


再看看 trait
的編譯圖示

- 特質中沒有實現的方法就是抽象方法;類通過
extends
繼承特質,通過with
可以繼承多個特質;也可以針對特質生成匿名類
val account2 = new Account {
override def check(): Unit = {
println("需要提供指紋進行驗證")
}
}
// 需要提供指紋進行驗證
account2.check()
- 所有的
Java
接口都可以當做Scala
特質使用
class MyClass extends Serializable{
}
實質上這個 Serializable
特質繼承了 Java
的Serializable

④ 特質的動態混入(MixIn)機制
首先,Scala
允許匿名子類動態的增加方法(Java
同樣也支持),示例代碼如下
object MixInDemo {
def main(args: Array[String]): Unit = {
val car = new Car {
def run(): Unit = {
println("什麼路都能開")
}
}
// 什麼路都能開
car.run()
}
}
class Car {}
然後看看 Scala
的特質混入機制如何實現的
object MixInDemo {
def main(args: Array[String]): Unit = {
val car = new Car with Transform {
override def speed(): Unit = println("加速300km/h")
}
car.speed()
}
}
trait Transform {
def speed()
}
class Car {}
實質還是通過匿名子類的方式來實現的,看看編譯後的字節碼



總結
- 除了可以在類聲明時繼承特質以外,還可以在構建對象時混入特質,擴展目標類的功能
- 此種方式也可以應用於對抽象類功能進行擴展、
object MixInDemo {
def main(args: Array[String]): Unit = {
val p = new Plain with Transform
p.speed()
}
}
abstract class Plain
trait Transform {
def speed(): Unit = println("加速")
}
動態混入是
Scala
特有的方式(Java
沒有動態混入),可在不修改類聲明/定義的情況下,擴展類的功能,非常的靈活,耦合性低動態混入可以在不影響原有的繼承關係的基礎上,給指定的類擴展功能
思考:如果抽象類中有沒有實現的方法,如何動態混入特質?
動態混入特質的同時實現抽象方法即可
問題:Scala 中創建對象一共 有幾種方式?
- new
- apply
- 動態混入
- 匿名子類
⑤ 疊加特質
構建對象的同時如果混入多個特質,稱之為疊加特質
特質聲明順序從左到右,方法執行順序從右到左
示例如下
請根據以下圖示寫出一個關於疊加特質的案例

object MixInDemo {
def main(args: Array[String]): Unit = {
// 混入對象的構建順序和特質聲明順序一致
val e = new EE with CC with DD
}
}
trait AA {
println("AAAA")
def func()
}
trait BB extends AA {
println("BBB")
override def func(): Unit = println("BBB's func")
}
trait CC extends BB {
println("CCC")
override def func(): Unit = {
println("CC's func")
super.func()
}
}
trait DD extends BB {
println("DD")
override def func(): Unit = {
println("CC's func")
super.func()
}
}
class EE
輸出

如果我們調用 e
的 func
方法
輸出

總結
當構建一個混入對象時,
構建順序和 聲明的順序一致(從左到右)
,機制和類的繼承一致執行方法時,
是從右到左執行(按特質)
Scala
中特質的方法中如果調用super
,並不是表示調用父特質的方法,而是向前面(左邊)繼續查找特質,如果找不到,才會去父特質查找
疊加特質細節
特質聲明順序從左到右。
Scala
在執行疊加對象的方法時,會首先從後面的特質(從右向左)開始執行Scala
中特質中如果調用super
,並不是表示調用父特質的方法,而是向前面(左邊)繼續查找特質,如果找不到,才會去父特質查找如果想要調用具體特質的方法,可以指定:
super[特質].xxx(…)
;其中的泛型必須是該特質的直接超類類型
示例代碼
trait DD extends BB {
println("DD")
override def func(): Unit = {
println("DD's func")
super[BB].func()
}
}
def main(args: Array[String]): Unit = {
val e = new EE with CC with DD
e.func()
}
此時輸出如下

⑥ 特質中重寫抽象方法
如果執行以下代碼

就會報錯

修改方式如下:
- 去掉
super.xxx
- 因為調用父特質的抽象方法,實際使用時,卻沒有具體的實現,就無法執行成功,為了避免這種情況的發生,可以抽象重寫方法,這樣在使用時,可能其他特質實現了這個抽象方法
trait A2 {
def func()
}
trait B2 extends A2 {
abstract override def func(): Unit = {
println("B2")
super.func()
}
}
如此重寫就不會再報錯了(相當詭異的語法~)
改造之前的代碼
object Main3 {
def main(args: Array[String]): Unit = {
val e = new E2 with C2 with B2
e.func()
}
}
trait A2 {
println("A2")
def func()
}
trait B2 extends A2 {
println("B2")
abstract override def func(): Unit = {
println("B2's func")
super.func()
}
}
trait C2 extends A2 {
println("C2")
override def func(): Unit = {
println("C2's func")
}
}
class E2
解釋:
對象 e
執行 func()
將會從 B2
開始執行對應的 func()
,B2
的 func()
將會調用 super.func()
,指向的就是 C2
的 func()
;而 C2
的 func()
是對父特質 A2
的抽象方法 func()
的完整實現
示意圖

⑦ 富接口
既有抽象方法,又有非抽象方法的特質
trait A2 {
def func()
def func2(): Unit = println("A2")
}
⑧ 特質的字段
解釋:特質中可以定義字段,如果初始化該字段就成為具體字段;如果不初始化就是抽象字段。混入該特質的類具有該字段,字段不是繼承,而是直接加入類,成為自己的字段
object Main3 {
def main(args: Array[String]): Unit = {
val e2 = new E2 with D2
println(e2.name) // cris
}
}
trait C2 {
var name: String
}
trait D2 extends C2 {
var name = "cris"
}
class E2
反編譯後的代碼

⑨ 多個特質的初始化順序
我們除了在創建對象的時候使用 with
來繼承特質,還可以在聲明類的時候使用 with
來繼承特質
一個類又繼承超類又繼承多個特質的時候,請問初始化該類的順序?
object Main3 {
def main(args: Array[String]): Unit = {
val e2 = new E2
}
}
trait A2{
println("A2")
}
trait B2 extends A2{
println("B2")
}
trait C2 extends A2{
println("C2")
}
class D2{
println("D2")
}
class E2 extends D2 with C2 with B2{
println("E2")
}
輸出

總結
先初始化超類
超類初始化完畢後按照順序初始化特質
初始化特質前先初始化該特質的父特質
多個特質具有相同的父特質只初始化一次
最後執行子類的初始化代碼
如果是動態混入,那麼類的初始化順序又是怎麼樣的?
class F extends D2 {
println("F")
}
def main(args: Array[String]): Unit = {
var f = new F with C2 with B2
}
輸出

總結
- 先初始化超類
- 然後初始化子類
- 最後初始化特質,初始化順序和上面一致
兩種初始化流程的理解
- 聲明類並繼承特質的方式可以理解為在初始化對象之前需要初始化必須的所有超類和特質
- 創建類再繼承特質的方式可以理解為混入特質之前就已經創建好了匿名類
⑩ 擴展類的特質
- 特質可以繼承類,以用來拓展該類的一些功能
object Main4 {
def main(args: Array[String]): Unit = {
val e = new MyException {}
e.func()
}
}
trait MyException extends Exception {
def func(): Unit = {
println(getMessage) // getMessage 方法來自 Exception 類(java.lang.Exception)
}
}
- 所有混入擴展特質的類,會自動成為那個特質所繼承的超類的子類
object Main4 {
def main(args: Array[String]): Unit = {
val e = new MyException2
println(e.getMessage) // this is my exception!
}
}
trait MyException extends Exception {
def func(): Unit = {
println(getMessage) // getMessage 方法來自 Exception 類(java.lang.Exception)
}
}
// MyException2 就是 Exception 的子類,所以可以重寫 Exception 的 getMessage 方法
class MyException2 extends MyException {
override def getMessage: String = "this is my exception!"
}
如果混入該特質的類,已經繼承了另一個類(
A
類),則要求A
類是特質超類的子類,否則就會出現了多繼承現象
,發生錯誤為了便於理解,修改上面的代碼
class A {}
class MyException2 extends A with MyException {
override def getMessage: String = "this is my exception!"
}
執行出錯

如果將 A
繼承 Exception
class A extends Exception{}
那麼執行成功~
⑩① 訪問特質自身類型
主要是為了解決特質的循環依賴問題,同時可以確保特質在不擴展某個類的情況下,依然可以做到限制混入該特質的類的類型
示例代碼

執行代碼如下
def main(args: Array[String]): Unit = {
val e = new MyException2
println(e.getMessage) // exception
}
9.4 嵌套類
在 Scala
中,你幾乎可以在任何語法結構中內嵌任何語法結構。如在類中可以再定義一個類,這樣的類是嵌套類,其他語法結構也是一樣
嵌套類類似於 Java
中的內部類
① 回顧 Java 的內部類
Java 中,一個類的內部又完整的嵌套了另一個類,這樣的結構稱為嵌套類;其中被嵌套的類稱為內部類,嵌套的類稱為外部類。
內部類最大的特點就是可以直接訪問私有屬性,並且可以體現類與類之間的包含關係
Java 內部類的分類
從定義在外部類的成員位置上區分:
- 成員內部類(無 static 修飾)
- 靜態內部類(有 static 修飾)
從定義在外部類的局部位置上區分:
- 局部內部類(有類名)
- 匿名內部類(無類名)
② Scala 的內部類
示例代碼
object InnerClassDemo {
def main(args: Array[String]): Unit = {
// 創建成員內部類實例
val outer1 = new Outer
val inner1 = new outer1.Inner
// 創建靜態內部類實例
val staticInner = new Outer.StaticInnerClass
}
}
class Outer {
// 成員內部類
class Inner {}
}
object Outer {
// 靜態內部類
class StaticInnerClass {}
}
內部類訪問外部類的屬性
- 內部類如果想要訪問外部類的屬性,可以通過外部類對象訪問
即:外部類名.this.屬性名
示例代碼
def main(args: Array[String]): Unit = {
val outer1 = new Outer
val inner1 = new outer1.Inner
inner1.func() // name is cris, age is 0
}
}
class Outer {
private var name = "cris"
val age = 0
class Inner {
def func(): Unit = {
println(s"name is ${Outer.this.name}, age is ${Outer.this.age}")
}
}
}
- 內部類如果想要訪問外部類的屬性,也可以通過外部類別名訪問
即:外部類名別名.屬性名
;關鍵是外部類屬性必須放在別名之後再定義
示例代碼
object InnerClassDemo {
def main(args: Array[String]): Unit = {
val outer1 = new Outer
val inner1 = new outer1.Inner
inner1.func() // name is 大帥, age is 12
}
}
class Outer {
MyOuter =>
class Inner {
def func(): Unit = {
println(s"name is ${MyOuter.name}, age is ${MyOuter.age}")
}
}
private var name = "大帥"
val age = 12
}
③ 類型投影
修改上面的代碼如下:
object InnerClassDemo {
def main(args: Array[String]): Unit = {
val outer1 = new Outer
val inner1 = new outer1.Inner
val outer2 = new Outer
val inner2 = new outer2.Inner
inner1.func(inner1) // name is 大帥, age is 12
inner1.func(inner2) // 報錯!原因就是因為 Scala 中成員內部類的類型默認是和外部類對象關聯
}
}
class Outer {
MyOuter =>
class Inner {
// 這裡參數類型雖然是 Inner,實質上是 Outer.Inner,其中 Outer 指定生成當前內部類的外部類對象
def func(i: Inner): Unit = {
println(s"name is ${MyOuter.name}, age is ${MyOuter.age}")
}
}
private var name = "大帥"
val age = 12
}
解決方式需要使用 Scala
的類型投影
類型投影是指:在方法聲明上,如果使用 外部類#內部類 的方式,表示忽略內部類的對象關係,等同於 Java
中內部類的語法操作,我們將這種方式稱之為類型投影(即:忽略對象的創建方式,只考慮類型)

④ 練習
java.awt.Rectangle類有兩個很有用的方法translate和grow,但可惜的是像java.awt.geom.Ellipse2D這樣的類沒有。在Scala中,你可以解決掉這個問題。定義一個RenctangleLike特質,加入具體的translate和grow方法。提供任何你需要用來實現的抽象方法,以便你可以像如下代碼這樣混入該特質:
val egg = new java.awt.geom.Ellipse2D.Double(5,10,20,30) with RectangleLike
egg.translate(10,-10)
egg.grow(10,20)
實現代碼如下:
object Practice2 {
def main(args: Array[String]): Unit = {
val egg = new Ellipse2D.Double(5, 10, 20, 30) with RectangleLike
egg.translate(1, 2) // 3.0
egg.grow(2, 4) // (2.0,4.0)
}
}
trait RectangleLike {
this: java.awt.geom.Ellipse2D.Double =>
def translate(a: Double, b: Double): Unit = {
println(a + b)
}
def grow(a: Double, b: Double): Unit = {
println(a, b)
}
}