Cris 的 Scala 筆記整理(八):物件導向中級-繼承和多型

Cris就是我發表於2018-12-20

繼承和多型

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("經理在管理~")
  }
}
複製程式碼

相關文章