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)
}
}
複製程式碼