Scala 系列(九)—— 繼承和特質

  • 2019 年 10 月 3 日
  • 筆記

一、繼承

1.1 Scala中的繼承結構

Scala 中繼承關係如下圖:

  • Any 是整個繼承關係的根節點;
  • AnyRef 包含 Scala Classes 和 Java Classes,等價於 Java 中的 java.lang.Object;
  • AnyVal 是所有值類型的一個標記;
  • Null 是所有引用類型的子類型,唯一實例是 null,可以將 null 賦值給除了值類型外的所有類型的變數;
  • Nothing 是所有類型的子類型。

1.2 extends & override

Scala 的集成機制和 Java 有很多相似之處,比如都使用 extends 關鍵字表示繼承,都使用 override 關鍵字表示重寫父類的方法或成員變數。示例如下:

//父類  class Person {      var name = ""    // 1.不加任何修飾詞,默認為 public,能被子類和外部訪問    var age = 0    // 2.使用 protected 修飾的變數能子類訪問,但是不能被外部訪問    protected var birthday = ""    // 3.使用 private 修飾的變數不能被子類和外部訪問    private var sex = ""      def setSex(sex: String): Unit = {      this.sex = sex    }    // 4.重寫父類的方法建議使用 override 關鍵字修飾    override def toString: String = name + ":" + age + ":" + birthday + ":" + sex    }

使用 extends 關鍵字實現繼承:

// 1.使用 extends 關鍵字實現繼承  class Employee extends Person {      override def toString: String = "Employee~" + super.toString      // 2.使用 public 或 protected 關鍵字修飾的變數能被子類訪問    def setBirthday(date: String): Unit = {      birthday = date    }    }

測試繼承:

  object ScalaApp extends App {      val employee = new Employee      employee.name = "heibaiying"    employee.age = 20    employee.setBirthday("2019-03-05")    employee.setSex("男")      println(employee)  }    // 輸出: Employee~heibaiying:20:2019-03-05:男

1.3 調用超類構造器

在 Scala 的類中,每個輔助構造器都必須首先調用其他構造器或主構造器,這樣就導致了子類的輔助構造器永遠無法直接調用超類的構造器,只有主構造器才能調用超類的構造器。所以想要調用超類的構造器,程式碼示例如下:

class Employee(name:String,age:Int,salary:Double) extends Person(name:String,age:Int) {      .....  }

1.4 類型檢查和轉換

想要實現類檢查可以使用 isInstanceOf,判斷一個實例是否來源於某個類或者其子類,如果是,則可以使用 asInstanceOf 進行強制類型轉換。

object ScalaApp extends App {      val employee = new Employee    val person = new Person      // 1. 判斷一個實例是否來源於某個類或者其子類 輸出 true    println(employee.isInstanceOf[Person])    println(person.isInstanceOf[Person])      // 2. 強制類型轉換    var p: Person = employee.asInstanceOf[Person]      // 3. 判斷一個實例是否來源於某個類 (而不是其子類)    println(employee.getClass == classOf[Employee])    }

1.5 構造順序和提前定義

1. 構造順序

在 Scala 中還有一個需要注意的問題,如果你在子類中重寫父類的 val 變數,並且超類的構造器中使用了該變數,那麼可能會產生不可預期的錯誤。下面給出一個示例:

// 父類  class Person {    println("父類的默認構造器")    val range: Int = 10    val array: Array[Int] = new Array[Int](range)  }    //子類  class Employee extends Person {    println("子類的默認構造器")    override val range = 2  }    //測試  object ScalaApp extends App {    val employee = new Employee    println(employee.array.mkString("(", ",", ")"))    }

這裡初始化 array 用到了變數 range,這裡你會發現實際上 array 既不會被初始化 Array(10),也不會被初始化為 Array(2),實際的輸出應該如下:

父類的默認構造器  子類的默認構造器  ()

可以看到 array 被初始化為 Array(0),主要原因在於父類構造器的執行順序先於子類構造器,這裡給出實際的執行步驟:

  1. 父類的構造器被調用,執行 new Array[Int](range) 語句;
  2. 這裡想要得到 range 的值,會去調用子類 range() 方法,因為 override val 重寫變數的同時也重寫了其 get 方法;
  3. 調用子類的 range() 方法,自然也是返回子類的 range 值,但是由於子類的構造器還沒有執行,這也就意味著對 range 賦值的 range = 2 語句還沒有被執行,所以自然返回 range 的默認值,也就是 0。

這裡可能比較疑惑的是為什麼 val range = 2 沒有被執行,卻能使用 range 變數,這裡因為在虛擬機層面,是先對成員變數先分配存儲空間並賦給默認值,之後才賦予給定的值。想要證明這一點其實也比較簡單,程式碼如下:

class Person {    // val range: Int = 10 正常程式碼 array 為 Array(10)    val array: Array[Int] = new Array[Int](range)    val range: Int = 10  //如果把變數的聲明放在使用之後,此時數據 array 為 array(0)  }    object Person {    def main(args: Array[String]): Unit = {      val person = new Person      println(person.array.mkString("(", ",", ")"))    }  }

2. 提前定義

想要解決上面的問題,有以下幾種方法:

(1) . 將變數用 final 修飾,代表不允許被子類重寫,即 final val range: Int = 10;

(2) . 將變數使用 lazy 修飾,代表懶載入,即只有當你實際使用到 array 時候,才去進行初始化;

lazy val array: Array[Int] = new Array[Int](range)

(3) . 採用提前定義,程式碼如下,代表 range 的定義優先於超類構造器。

class Employee extends {    //這裡不能定義其他方法    override val range = 2  } with Person {    // 定義其他變數或者方法    def pr(): Unit = {println("Employee")}  }

但是這種語法也有其限制:你只能在上面程式碼塊中重寫已有的變數,而不能定義新的變數和方法,定義新的變數和方法只能寫在下面程式碼塊中。

注意事項:類的繼承和下文特質 (trait) 的繼承都存在這個問題,也同樣可以通過提前定義來解決。雖然如此,但還是建議合理設計以規避該類問題。


二、抽象類

Scala 中允許使用 abstract 定義抽象類,並且通過 extends 關鍵字繼承它。

定義抽象類:

abstract class Person {    // 1.定義欄位    var name: String    val age: Int      // 2.定義抽象方法    def geDetail: String      // 3. scala 的抽象類允許定義具體方法    def print(): Unit = {      println("抽象類中的默認方法")    }  }

繼承抽象類:

class Employee extends Person {    // 覆蓋抽象類中變數    override var name: String = "employee"    override val age: Int = 12      // 覆蓋抽象方法    def geDetail: String = name + ":" + age  }  


三、特質

3.1 trait & with

Scala 中沒有 interface 這個關鍵字,想要實現類似的功能,可以使用特質 (trait)。trait 等價於 Java 8 中的介面,因為 trait 中既能定義抽象方法,也能定義具體方法,這和 Java 8 中的介面是類似的。

// 1.特質使用 trait 關鍵字修飾  trait Logger {      // 2.定義抽象方法    def log(msg: String)      // 3.定義具體方法    def logInfo(msg: String): Unit = {      println("INFO:" + msg)    }  }

想要使用特質,需要使用 extends 關鍵字,而不是 implements 關鍵字,如果想要添加多個特質,可以使用 with 關鍵字。

// 1.使用 extends 關鍵字,而不是 implements,如果想要添加多個特質,可以使用 with 關鍵字  class ConsoleLogger extends Logger with Serializable with Cloneable {      // 2. 實現特質中的抽象方法    def log(msg: String): Unit = {      println("CONSOLE:" + msg)    }  }

3.2 特質中的欄位

和方法一樣,特質中的欄位可以是抽象的,也可以是具體的:

  • 如果是抽象欄位,則混入特質的類需要重寫覆蓋該欄位;
  • 如果是具體欄位,則混入特質的類獲得該欄位,但是並非是通過繼承關係得到,而是在編譯時候,簡單將該欄位加入到子類。
trait Logger {    // 抽象欄位    var LogLevel:String    // 具體欄位    var LogType = "FILE"  }

覆蓋抽象欄位:

class InfoLogger extends Logger {    // 覆蓋抽象欄位    override var LogLevel: String = "INFO"  }

3.3 帶有特質的對象

Scala 支援在類定義的時混入 父類 trait,而在類實例化為具體對象的時候指明其實際使用的 子類 trait。示例如下:

trait Logger:

// 父類  trait Logger {    // 定義空方法 日誌列印    def log(msg: String) {}  }

trait ErrorLogger:

// 錯誤日誌列印,繼承自 Logger  trait ErrorLogger extends Logger {    // 覆蓋空方法    override def log(msg: String): Unit = {      println("Error:" + msg)    }  }

trait InfoLogger:

// 通知日誌列印,繼承自 Logger  trait InfoLogger extends Logger {      // 覆蓋空方法    override def log(msg: String): Unit = {      println("INFO:" + msg)    }  }

具體的使用類:

// 混入 trait Logger  class Person extends Logger {    // 調用定義的抽象方法    def printDetail(detail: String): Unit = {      log(detail)    }  }

這裡通過 main 方法來測試:

object ScalaApp extends App {      // 使用 with 指明需要具體使用的 trait    val person01 = new Person with InfoLogger    val person02 = new Person with ErrorLogger    val person03 = new  Person with InfoLogger with ErrorLogger    person01.log("scala")  //輸出 INFO:scala    person02.log("scala")  //輸出 Error:scala    person03.log("scala")  //輸出 Error:scala    }

這裡前面兩個輸出比較明顯,因為只指明了一個具體的 trait,這裡需要說明的是第三個輸出,因為 trait 的調用是由右到左開始生效的,所以這裡列印出 Error:scala。

3.4 特質構造順序

trait 有默認的無參構造器,但是不支援有參構造器。一個類混入多個特質後初始化順序應該如下:

// 示例  class Employee extends Person with InfoLogger with ErrorLogger {...}
  1. 超類首先被構造,即 Person 的構造器首先被執行;
  2. 特質的構造器在超類構造器之前,在類構造器之後;特質由左到右被構造;每個特質中,父特質首先被構造;
    • Logger 構造器執行(Logger 是 InfoLogger 的父類);
    • InfoLogger 構造器執行;
    • ErrorLogger 構造器執行;
  3. 所有超類和特質構造完畢,子類才會被構造。


參考資料

  1. Martin Odersky . Scala 編程 (第 3 版)[M] . 電子工業出版社 . 2018-1-1
  2. 凱.S.霍斯特曼 . 快學 Scala(第 2 版)[M] . 電子工業出版社 . 2017-7

更多大數據系列文章可以參見 GitHub 開源項目: 大數據入門指南

Tags: