Cris的Scala筆記整理(八):面向對象中級繼承和多態

NO IMAGE

繼承和多態

1. Java 繼承回顧

class 子類名 extends 父類名 { 類體 }

子類會繼承父類所有的屬性和方法

2. 繼承簡述

繼承可以解決代碼複用,讓我們的編程更加靠近人類思維。當多個類存在相同的屬性(變量)和方法時,可以從這些類中抽象出父類,在父類中定義這些相同的屬性和方法,所有的子類不需要重新定義這些屬性和方法,只需要通過 extends 語句來聲明繼承父類即可

Java 一樣,Scala 也支持類的單繼承

3. Scala 繼承案例
object ExtendDemo extends App {
val stu = new Stu
stu.name = "cris"
stu.age = 23
stu.study() // cris is studying!!!
stu.info() // Student(cris,23)
}
class Person {
var name: String = ""
var age: Int = 18
def info(): Unit = {
println(toString)
}
override def toString = s"Person($name, $age)"
}
class Stu extends Person {
def study(): Unit = {
println(this.name + " is studying!!!")
}
override def toString = s"Student($name,$age)"
}

子類繼承了所有的屬性,只是私有的屬性不能直接訪問,需要通過公共的方法去訪問

驗證代碼如下

class Father {
var a = 100
protected var b = 200
private var c = 300
def func1() {}
protected def func2() {}
private def func3() {}
}
class Son extends Father {
def func(): Unit = {
println(this.a + this.b)
func1()
func2()
}
}

觀察反編譯後的字節碼文件

Cris的Scala筆記整理(八):面向對象中級繼承和多態

public class Father
{
public int a()
{
return this.a;
}
public void a_$eq(int x$1)
{
this.a = x$1;
}
private int a = 100;
public int b()
{
return this.b;
}
public void b_$eq(int x$1)
{
this.b = x$1;
}
private int b = 200;
private int c()
{
return this.c;
}
private void c_$eq(int x$1)
{
this.c = x$1;
}
private int c = 300;
public void func1() {}
public void func2() {}
private void func3() {}
}

實際上,Scala 中只有兩種訪問修飾符,一種 public,一種 private,protected 編譯後就是 public

4. 方法的重寫

Scala 明確規定,重寫一個非抽象方法需要用 override 修飾符,調用超類的方法需要使用 super 關鍵字

Cris的Scala筆記整理(八):面向對象中級繼承和多態

5. Scala 中的類型檢查和轉換(多態)
  1. 要測試某個對象是否屬於某個給定的類,可以用 isInstanceOf 方法;用 asInstanceOf 方法將引用轉換為子類的引用;classOf 獲取對象的類名

  2. classOf[String] 就如同 JavaString.class

  3. obj.isInstanceOf[T] 就如同 Javaobj instanceof T 判斷 obj 是不是 T 類型

  4. obj.asInstanceOf[T] 就如同 Java(T)objobj 強轉成 T 類型

示例代碼

  def main(args: Array[String]): Unit = {
println(classOf[String]) // class java.lang.String
// 上行代碼使用反射實現
val string = "cris"
println(string.getClass.getName) // java.lang.String
// 類型判斷
println(string.isInstanceOf[String]) // true
// 類型轉換(向上轉型)
val any: AnyRef = string
// 類型轉換(向下轉型)
println(any.asInstanceOf[String].charAt(0)) // c
}
}

向上轉型的目的:為了實現方法參數的統一;向下轉型的目的:為了使用特定類的特定方法

類型轉換最佳示例

object TypeConverse {
def main(args: Array[String]): Unit = {
val dog = new Dog02
val fish = new Fish02
func(dog) // dog is eating bone
func(fish) // fish is swimming
}
def func(p: Pet02): Unit = {
if (p.isInstanceOf[Dog02]) p.asInstanceOf[Dog02].eatBone()
else if (p.isInstanceOf[Fish02]) p.asInstanceOf[Fish02].swimming()
else println("類型錯誤!")
}
}
class Pet02 {
}
class Dog02 extends Pet02 {
var name = "dog"
def eatBone(): Unit = {
println(s"$name is eating bone")
}
}
class Fish02 extends Pet02 {
var name = "fish"
def swimming(): Unit = {
println(s"$name is swimming")
}
}

向下轉型的前提是:該對象本身就是要轉型的子類數據類型

6. 超類構造

回顧 Java 的超類構造

Java 中,創建子類對象時,子類的構造器總是去調用一個父類的構造器(顯式或者隱式調用)

看看 Scala 的超類構造

示例代碼

object SuperDemo {
def main(args: Array[String]): Unit = {
var b = new B("cris")
}
}
class A {
var name = "A"
println(s"A's name is $name")
}
class B extends A {
println(s"B's name is $name")
def this(name: String) {
this()
this.name = name
println(s"finally, B's name is $name")
}
}

執行結果如下:

Cris的Scala筆記整理(八):面向對象中級繼承和多態

總結一下執行順序:

  1. 調用 B 的輔助構造函數時,先要調用 B 的主構造(this()
  2. 調用 B 的主構造之前,調用父類 A 的主構造
  3. 最後才是調用 B 的輔助構造

注意點:

Cris的Scala筆記整理(八):面向對象中級繼承和多態

Scala 的構造器中,你不能使用 super 來調用父類的構造器

練習:寫一個能體現 Scala 構造器繼承特點的案例

object SuperDemo2 {
def main(args: Array[String]): Unit = {
val worker = new Worker("cris")
//    name = cris
//    age = 20
worker.info()
}
}
class People(pName: String) {
var name: String = this.pName
def info(): Unit = println(s"name = $name")
}
class Worker(name: String) extends People(name) {
var age = 20
override def info(): Unit = {
super.info()
println(s"age = $age")
}
}

結合輸出,想想上面代碼的執行順序

總結

  1. 子類構造一定會調用父類的構造(可以是主構造,也可以是輔助構造)
  2. 父類的所有輔助構造,最終都會調用父類的主構造
7. 屬性覆寫

回想:Java 中父類的屬性可以被覆寫嗎?

示例代碼

public class Demo {
public static void main(String[] args) {
Sub s = new Sub();
// james
System.out.println(s.name);
Super s2 = new Sub();
// cris
System.out.println(s2.name);
}
}
class Super {
String name = "cris";
}
class Sub extends Super {
String name = "james";
}

答案是:不會!

Java 給出的解釋是:隱藏字段代替了重寫

官網解釋如下:

​ Within a class, a field that has the same name as a field in the superclass hides the superclass’s field, even if their types are different. Within the subclass, the field in the superclass cannot be referenced by its simple name. Instead, the field must be accessed through super. Generally speaking, we don’t recommend hiding fields as it makes code difficult to read.

從上面這段解釋中,我們可以看出成員變量不能像方法一樣被重寫。當一個子類定義了一個跟父類相同名字的字段,子類就是定義了一個新的字段。這個字段在父類中被隱藏的,是不可重寫的

如果想要訪問父類的隱藏字段

  • 採用父類的引用類型,這樣隱藏的字段就能被訪問了,像上面所給出的例子一樣
  • 將子類強制類型轉化為父類類型,也能訪問到隱藏的字段

小結

​ 父類和子類定義了一個同名的字段,不會報錯。但對於同一個對象,用父類的引用去取值(字段),會取到父類的字段的值,用子類的引用去取值(字段),則取到子類字段的值。在實際的開發中,要儘量避免子類和父類使用相同的字段名,否則很容易引入一些不容易發現的bug

回顧 Java 的動態綁定

示例代碼

public class Demo {
public static void main(String[] args) {
Super s = new Sub();
System.out.println("s.getI() = " + s.getI());
System.out.println("s.sum() = " + s.sum());
System.out.println("s.sum1() = " + s.sum1());
}
}
class Super {
public int i = 10;
public int sum() {
return getI() + 10;
}
public int sum1() {
return i + 10;
}
public int getI() {
return i;
}
}
class Sub extends Super {
public int i = 20;
@Override
public int sum() {
return i + 20;
}
@Override
public int getI() {
return i;
}
@Override
public int sum1() {
return i + 10;
}
}

結果如下

Cris的Scala筆記整理(八):面向對象中級繼承和多態

如果我們將子類的 getI()sum1() 方法註釋掉,再執行,結果如下:

Cris的Scala筆記整理(八):面向對象中級繼承和多態

總結 Java 的動態綁定機制

  1. 當調用對象方法的時候,該方法會和該對象的內存地址綁定
  2. 當調用對象屬性時,沒有動態綁定機制,哪裡聲明,那裡使用

Scala 的屬性覆寫

示例代碼

object OverrideDemo {
def main(args: Array[String]): Unit = {
val a: AA = new BB
val b: BB = new BB
println(a.i)	// 實質調用的是 BB 的 i()方法
println(b.i)	// 實質調用的是 BB 的 i()方法
}
}
class AA {
// AA 編譯後的文件會生成一個 i() 方法用於讀取該屬性
val i = 10
}
class BB extends AA {
// BB 編譯後的文件會覆寫 AA 中的 i() 方法
override val i = 20
}

輸出

Cris的Scala筆記整理(八):面向對象中級繼承和多態

看看編譯後的源代碼

Cris的Scala筆記整理(八):面向對象中級繼承和多態

Cris的Scala筆記整理(八):面向對象中級繼承和多態

覆寫字段的注意事項和細節

  1. val 屬性只能重寫另一個 val 屬性或重寫不帶參數的同名方法

    示例代碼

    object OverrideDemo {
    def main(args: Array[String]): Unit = {
    val a: AA = new BB
    val b: BB = new BB
    println(a.func())	// 實質都是調用的 BB 中的 func()
    println(b.func)	// 實質都是調用的 BB 中的 func()
    }
    }
    class AA {
    // AA 編譯後的文件會生成一個 i() 方法用於讀取該屬性
    val i = 10
    def func(): Int = i
    }
    class BB extends AA {
    // BB 編譯後的文件會覆寫 AA 中的 i() 方法
    override val i = 20
    override val func: Int = i
    }
    

    輸出

    Cris的Scala筆記整理(八):面向對象中級繼承和多態

    查看編譯後的字節碼

    Cris的Scala筆記整理(八):面向對象中級繼承和多態

    Cris的Scala筆記整理(八):面向對象中級繼承和多態

  2. var 只能重寫另一個抽象的 var 屬性

    示例代碼

    Cris的Scala筆記整理(八):面向對象中級繼承和多態

    先看看什麼是抽象屬性:未初始化的變量就是抽象的屬性,抽象屬性需要在抽象類中

    然後再看看編譯後的字節碼

    Cris的Scala筆記整理(八):面向對象中級繼承和多態

    Cris的Scala筆記整理(八):面向對象中級繼承和多態

    值得一提的是,Scala 語法中,BBB 中的 override 關鍵字可以省略

重寫抽象的 var 屬性小結

  • 一個 var 屬性沒有初始化,那麼這個 var 屬性就是抽象屬性
  • 抽象的 var 屬性在編譯成字節碼文件時,屬性並不會聲明,但是會自動生成抽象方法,所以類必須聲明為抽象類
  • 如果是覆寫一個父類的抽象 var 屬性,那麼 override 關鍵字可省略
  • 如果是 var 屬性覆寫非抽象的 var 屬性,運行時會報錯,參考 StackOverflow
8. 抽象類

Scala 中,通過abstract關鍵字標記不能被實例化的類方法不用標記abstract,只要省掉方法體即可。抽象類可以擁有抽象字段,抽象字段就是沒有初始值的字段

示例代碼

Cris的Scala筆記整理(八):面向對象中級繼承和多態

如果在抽象方法前面加了 abstract ,運行時將會報以下錯誤,非常奇葩~

Cris的Scala筆記整理(八):面向對象中級繼承和多態

抽象類的價值

抽象類的價值更多在於設計,是設計者設計好之後,讓子類去繼承並實現(表示一種規範)

Scala 抽象類的細節

  1. 抽象類不能被實例化

  2. 抽象類不一定要包含 abstract 方法

  3. 一旦類包含了抽象方法或者抽象屬性,則這個類必須聲明為 abstract

  4. 抽象方法不能有主體,不允許使用 abstract 修飾

  5. 如果一個類繼承了抽象類,則它必須實現抽象類的所有抽象方法和抽象屬性,除非它自己也聲明為 abstract

  6. 抽象方法和抽象屬性不能使用 private、final 來修飾,因為這些關鍵字都是和重寫/實現相違背的

  7. 子類重寫抽象方法不需要 override ,寫上也不會錯

9. 匿名子類

回顧 Java 的匿名子類

示例代碼

public class Demo {
public static void main(String[] args) {
Man man = new Man() {
@Override
void work() {
System.out.println("廚師炒菜掙錢");
}
};
//        廚師炒菜掙錢
man.work();
}
}
abstract class Man {
/**
* 掙錢的方法
*/
abstract void work();
}

Scala 的匿名子類

object SubDemo2 {
def main(args: Array[String]): Unit = {
val monkey = new Monkey {
override var name: String = "金絲猴"
override def eat(): Unit = {
println("吃桃子")
}
}
monkey.eat()
println(monkey.name)
}
}
abstract class Monkey {
var name: String
def eat()
}
10. 繼承層級

請參考《第三章:變量》

11. 練習

定義員工類,包含姓名和月工資,以及計算年薪的方法。普通員工和經理繼承了員工,經理類多了獎金屬性和管理方法,普通員工多了工作方法,並且普通員工和經理均要重寫計算年薪的方法

測試類中添加一個方法,實現獲取任何員工年薪的需求

測試類中添加一個方法,實現如果是普通員工,調用工作方法;如果是經理,調用管理方法的需求

object Practice {
def main(args: Array[String]): Unit = {
val worker = new Worker2
val manager = new Manager
showEmployeeAnnual(worker)
showEmployeeAnnual(manager)
testEmployee(worker)
testEmployee(manager)
}
def showEmployeeAnnual(e: Employee): Unit = {
println(e.getAnnual)
}
def testEmployee(e: Employee): Unit = {
if (e.isInstanceOf[Worker2]) e.asInstanceOf[Worker2].work()
else if (e.isInstanceOf[Manager]) e.asInstanceOf[Manager].manage()
}
}
abstract class Employee {
// 定義抽象屬性
var name: String
var salary: Double
// 定義抽象方法
def getAnnual: Double
}
class Worker2 extends Employee {
override var name: String = "工人"
override var salary: Double = 2000
override def getAnnual: Double = {
this.salary * 12
}
def work(): Unit = {
println("工人工作~")
}
}
class Manager extends Employee {
override var name: String = "經理"
override var salary: Double = 20000.0
var bonus = 60000
override def getAnnual: Double = {
this.salary * 12 + this.bonus
}
def manage(): Unit = {
println("經理在管理~")
}
}

相關文章

Cris的SparkStreaming筆記

Cris的SparkSQL筆記

Cris的Scala筆記整理(十):隱式轉換

Cris的Scala筆記整理(九):面向對象高級