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

1、繼承

1.1 Scala中的繼承結構

Scala 中繼承關係以下圖:java

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

1.2 extends & override

Scala 的集成機制和 Java 有不少類似之處,好比都使用 extends 關鍵字表示繼承,都使用 override 關鍵字表示重寫父類的方法或成員變量。示例以下:git

//父類
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 關鍵字實現繼承:github

// 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 的類中,每一個輔助構造器都必須首先調用其餘構造器或主構造器,這樣就致使了子類的輔助構造器永遠沒法直接調用超類的構造器,只有主構造器才能調用超類的構造器。因此想要調用超類的構造器,代碼示例以下:ide

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),實際的輸出應該以下:this

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

能夠看到 array 被初始化爲 Array(0),主要緣由在於父類構造器的執行順序先於子類構造器,這裏給出實際的執行步驟:scala

  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) 的繼承都存在這個問題,也一樣能夠經過提早定義來解決。雖然如此,但仍是建議合理設計以規避該類問題。


2、抽象類

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、特質

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 開源項目大數據入門指南

相關文章
相關標籤/搜索