Scala入門到精通——第十一節 Trait進階
本節主要內容
- trait構造順序
- trait與類的比較
- 提前定義與懶載入
- trait擴充套件類
- 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相關技術資訊