1. 程式人生 > >Scala入門到精通——第十一節 Trait進階

Scala入門到精通——第十一節 Trait進階

本節主要內容

  1. trait構造順序
  2. trait與類的比較
  3. 提前定義與懶載入
  4. trait擴充套件類
  5. self type

1 trait構造順序

在前一講當中我們提到,對於不存在具體實現及欄位的trait,它最終生成的位元組碼檔案反編譯後是等同於java中的介面,而對於存在具體實現及欄位的trait,其位元組碼檔案反編譯後得到的java中的抽象類,它有著scala語言自己的實現方式。因此,對於trait它也有自己的構造器,trait的構造器由欄位的初始化和其它trait體中的語句構成,下面是其程式碼演示:

package cn.scala.xtwy

import java.io.PrintWriter

trait Logger{
  println("Logger"
) def log(msg:String):Unit } trait FileLogger extends Logger{ println("FilgeLogger") val fileOutput=new PrintWriter("file.log") fileOutput.println("#") def log(msg:String):Unit={ fileOutput.print(msg) fileOutput.flush() } } object TraitDemo{ def main(args: Array[String]): Unit = { //匿名類 new FileLogger{ }.log("trat demo"
) } } //列印輸出內容為: Logger FilgeLogger //建立檔案file.log,內容為 # trat demo

通過上述不難發現,在建立匿名類物件時,先呼叫的是Logger類的構造器,然後呼叫的是FileLogger的構造器。實際上構造器是按以下順序執行的:
1. 如果有超類,則先呼叫超類的構造器
2. 如果有父trait,它會按照繼承層次先呼叫父trait的構造器
2. 如果有多個父trait,則按順序從左到右執行
3. 所有父類構造器和父trait被構造完之後,才會構造本類

class Person
class Student extends Person
with FileLogger with Cloneable
上述構造器的執行順序為: 1 首先呼叫父類Person的構造器 2 呼叫父trait Logger的構造器 3 再呼叫trait FileLogger構造器,再然後呼叫Cloneable的構造器 4 最後才呼叫Student的構造器

2 trait與類的比較

通過前一小節,可以看到,trait有自己的構造器,它是無參構造器,不能定義trait帶引數的構造器,即:

//不能定義trait帶引數的構造器
trait FileLogger(msg:String) 

除此之外 ,trait與普通的scala類並沒有其它區別,在前一講中我們提到,trait中可以有具體的、抽象的欄位,也可以有具體的、抽象的方法,即使trait中沒有抽象的方法也是合理的,如:

//FileLogger裡面沒有抽象的方法
trait FileLogger extends Logger{
  println("FilgeLogger")
  val fileOutput=new PrintWriter("file.log")
  fileOutput.println("#")

  def log(msg:String):Unit={
    fileOutput.print(msg)
    fileOutput.flush()
    }
}

3. 提前定義與懶載入

前面的FileLogger中的檔名被寫死為”file.log”,程式不具有通用性,這邊對前面的FileLogger進行改造,把檔名寫成引數形式,程式碼如下:

import java.io.PrintWriter

trait Logger{
  def log(msg:String):Unit
}

trait FileLogger extends Logger{
  //增加了抽象成員變數
  val fileName:String
  //將抽象成員變數作為PrintWriter引數
  val fileOutput=new PrintWriter(fileName:String)
  fileOutput.println("#")

  def log(msg:String):Unit={
    fileOutput.print(msg)
    fileOutput.flush()
    }
}

這樣的設計會存在一個問題,雖然子類可以對fileName抽象成員變數進行重寫,編譯也能通過,但實際執行時會出空指標異常,完全程式碼如下:

package cn.scala.xtwy

import java.io.PrintWriter

trait Logger{
  def log(msg:String):Unit
}

trait FileLogger extends Logger{

   //增加了抽象成員變數
  val fileName:String
  //將抽象成員變數作為PrintWriter引數
  val fileOutput=new PrintWriter(fileName:String)
  fileOutput.println("#")

  def log(msg:String):Unit={
    fileOutput.print(msg)
    fileOutput.flush()
    }
}

class Person
class Student extends Person with FileLogger{
  //Student類對FileLogger中的抽象欄位進行重寫
  val fileName="file.log"
}

object TraitDemo{
  def main(args: Array[String]): Unit = {
    new Student().log("trait demo")
  }
}

上述程式碼在編譯時不會有問題,但實際執行時會拋異常,異常如下:

Exception in thread "main" java.lang.NullPointerException
    at java.io.FileOutputStream.<init>(Unknown Source)
    at java.io.FileOutputStream.<init>(Unknown Source)
    at java.io.PrintWriter.<init>(Unknown Source)
    at cn.scala.xtwy.FileLogger$class.$init$(TraitDemo.scala:12)
    at cn.scala.xtwy.Student.<init>(TraitDemo.scala:22)
    at cn.scala.xtwy.TraitDemo$.main(TraitDemo.scala:28)
    at cn.scala.xtwy.TraitDemo.main(TraitDemo.scala)

具體原因就是構造器的執行順序問題,

class Student extends Person with FileLogger{
  //Student類對FileLogger中的抽象欄位進行重寫
  val fileName="file.log"
}
//在對Student類進行new操作的時候,它首先會
//呼叫Person構造器,這沒有問題,然後再呼叫
//Logger構造器,這也沒問題,但它最後呼叫FileLogger
//構造器的時候,它會執行下面兩條語句
//增加了抽象成員變數
  val fileName:String
  //將抽象成員變數作為PrintWriter引數
  val fileOutput=new PrintWriter(fileName:String)
此時fileName沒有被賦值,被初始化為null,在執行new PrintWriter(fileName:String)操作的時候便丟擲空指標異常

有幾種辦法可以解決前面的問題:
1 提前定義
提前定義是指在常規構造之前將變數初始化,完整程式碼如下:

package cn.scala.xtwy

import java.io.PrintWriter

trait Logger{
  def log(msg:String):Unit
}

trait FileLogger extends Logger{

  val fileName:String
  val fileOutput=new PrintWriter(fileName:String)
  fileOutput.println("#")

  def log(msg:String):Unit={
    fileOutput.print(msg)
    fileOutput.flush()
    }
}

class Person
class Student extends Person with FileLogger{
  val fileName="file.log"
}

object TraitDemo{
  def main(args: Array[String]): Unit = {
    val s=new {
      //提前定義
      override val fileName="file.log"
      } with Student
    s.log("predifined variable ")
  }
}

顯然,這種方式編寫的程式碼很不優雅,也比較難理解。此時可以通過在第一講中提到的lazy即懶載入的方式

2 lazy懶載入的方式

package cn.scala.xtwy

import java.io.PrintWriter

trait Logger{
  def log(msg:String):Unit
}

trait FileLogger extends Logger{

  val fileName:String
  //將方法定義為lazy方式
  lazy val fileOutput=new PrintWriter(fileName:String)
  //下面這條語句不能出現,否則同樣會報錯
  //因此,它是FileLogger構造器裡面的方法
  //在構造FileLogger的時候便會執行
  //fileOutput.println("#")

  def log(msg:String):Unit={
    fileOutput.print(msg)
    fileOutput.flush()
    }
}

class Person
class Student extends Person with FileLogger{
  val fileName="file.log"
}

object TraitDemo{
  def main(args: Array[String]): Unit = {
    val s=new Student
    s.log("predifined variable ")
  }
}

lazy方式定義fileOutput只有當真正被使用時才被初始化,例子中,當呼叫 s.log(“predifined variable “)時,fileOutput才被初始化,此時fileName已經被賦值了。

4 trait擴充套件類

在本節的第2小節部分,我們給出了trait與類之間的區別,我們現在明白,trait除了不具有帶引數的建構函式之外,與普通類沒有任何區別,這意味著trait也可以擴充套件其它類

trait Logger{
  def log(msg:String):Unit
}
//trait擴充套件類Exception
trait ExceptionLogger extends Exception with Logger{
  def log(msg:String):Unit={
    println(getMessage())
  }
}

如果此時定義了一個類混入了ExceptionLogger ,則Exception自動地成為這個類的超類,程式碼如下:

trait Logger{
  def log(msg:String):Unit
}

trait ExceptionLogger extends Exception with Logger{
  def log(msg:String):Unit={
    println(getMessage())
  }
}

//類UnprintedException擴充套件自ExceptionLogger
//注意用的是extends
//此時ExceptionLogger父類Exception自動成為
//UnprintedException的父類
class UnprintedException extends ExceptionLogger{
  override  def log(msg:String):Unit={
    println("")
  }
} 

當UnprintedException擴充套件的類或混入的特質具有相同的父類時,scala會自動地消除衝突,例如:

//IOException具有父類Exception
//ExceptionLogger也具有父類Exception
//scala會使UnprintedException只有一個父類Exception
class UnprintedException extends IOException with ExceptionLogger{
  override  def log(msg:String):Unit={
    println("")
  }
} 

5 self type

下面的程式碼演示了什麼是self type即自身型別

class A{
    //下面 self =>  定義了this的別名,它是self type的一種特殊形式
    //這裡的self並不是關鍵字,可以是任何名稱
    self =>  
    val x=2 
    //可以用self.x作為this.x使用
    def foo = self.x + this.x 
}

下面給出了內部類中使用場景

class OuterClass { 
    outer => //定義了一個外部類別名
    val v1 = "here"
    class InnerClass {
        // 用outer表示外部類,相當於OuterClass.this
        println(outer.v1) 
    }
}

而下面的程式碼則定義了自身型別self type,它不是前面別名的用途,

trait X{

}
class B{
  //self:X => 要求B在例項化時或定義B的子類時
  //必須混入指定的X型別,這個X型別也可以指定為當前型別
  self:X=>
}

自身型別的存在相當於讓當前類變得“抽象”了,它假設當前物件(this)也符合指定的型別,因為自身型別 this:X =>的存在,當前類構造例項時需要同時滿足X型別,下面給出自身型別的使用程式碼:

trait X{
  def foo()
}
class B{
  self:X=>
}
//類C擴充套件B的時候必須混入trait X
//否則的話會報錯
class C extends B with X{
  def foo()=println("self type demo")
}

object SelfTypeDemo extends App{
  println(new C().foo)
}

新增公眾微訊號,可以瞭解更多最新Spark、Scala相關技術資訊
這裡寫圖片描述