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相关技术资讯

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-08-26 12:20:01

Scala入门到精通——第十一节 Trait进阶的相关文章

Scala入门到精通——第二十一节 类型参数(三)-协变与逆变

作者:摇摆少年梦 视频地址:http://www.xuetuwuyou.com/course/12 本节主要内容 协变 逆变 类型通匹符 1. 协变 协变定义形式如:trait List[+T] {} .当类型S是类型A的子类型时,则List[S]也可以认为是List[A}的子类型,即List[S]可以泛化为List[A].也就是被参数化类型的泛化方向与参数类型的方向是一致的,所以称为协变(covariance). 图1 协变示意图 为方便大家理解,我们先分析java语言中为什么不存在协变及下一

Scala入门到精通——第十七节 类型参数(一)

本节主要内容 类型变量界定(Type Variable Bound) 视图界定(View Bound) 上界(Upper Bound)与下界(Lower Bound) 1. 类型变量界定(Type Variable Bound) 类型变量界定是指在泛型的基础上,对泛型的范围进行进一步的界定,从而缩下泛型的具体范围,例如: //下面的类编译通不过 //因为泛型T在编译的时候不能确定其具体类型 //即并不是所有的类中都存在compareTo方法 class TypeVariableBound { d

Scala入门到精通——第十节 Scala类层次结构、Traits初步

本节主要内容 Scala类层次结构总览 Scala中原生类型的实现方式解析 Nothing.Null类型解析 Traits简介 Traits几种不同使用方式 1 Scala类层次结构 Scala中的类层次结构图如下: 来源:Programming in Scala 从上面的类层次结构图中可以看到,处于继承层次最顶层的是Any类,它是scala继承的根类,scala中所有的类都是它的子类 Any类中定义了下面几个方法: //==与!=被声明为final,它们不能被子类重写 final def ==

Scala入门到精通——第二十七节 Scala操纵XML

本节主要内容 XML 字面量 XML内容提取 XML对象序列化及反序列化 XML文件读取与保存 XML模式匹配 1. XML 字面量 XML是一种非常重要的半结构化数据表示方式,目前大量的应用依赖于XML,这些应用或利用XML作为数据交换格式,或利用XML进行文件配置等.像JAVA.C++及其它流行的程序开发语言都是依赖于第三方库来实现XML的操作,例如JAVA经常通过JDOM,DOM4J等XML处理工具进行XML的操纵,但Scala提供了对XML的原生支持,通过scala.xml._包下的类或

Scala入门到精通——第二十节 类型參数(二)

本节主要内容 Ordering与Ordered特质 上下文界定(Context Bound) 多重界定 类型约束 1. Ordering与Ordered特质 在介绍上下文界定之前,我们对scala中的Ordering与Ordered之间的关联与差别进行解说,先看Ordering.Ordered的类继承层次体系: 通过上面两个图能够看到,Ordering混入了java中的Comparator接口.而Ordered混入了java的Comparable接口.我们知道java中的Comparator是一

Scala入门到精通——第二十节 类型参数(二)

本节主要内容 Ordering与Ordered特质 上下文界定(Context Bound) 多重界定 类型约束 1. Ordering与Ordered特质 在介绍上下文界定之前,我们对scala中的Ordering与Ordered之间的关联与区别进行讲解,先看Ordering.Ordered的类继承层次体系: 通过上面两个图可以看到,Ordering混入了java中的Comparator接口,而Ordered混入了java的Comparable接口,我们知道java中的Comparator是一

Scala入门到精通——第三十节 Scala脚本编程与结束语

本节主要内容 REPL命令行高级使用 使用Scala进行Linux脚本编程 结束语 1. REPL命令行高级使用 在使用REPL命令行时,有时候我们需要粘贴的代码比较大,而普通的粘贴可能会些一些问题,比如中文粘贴会出现乱码.多行代码粘贴时会出错,此时需要用到REPL的高级功能.在日常开发过程中,我们粘贴多行代码的时候会遇到下列问题: //本意是要粘贴下面两行代码 class Person(val name:String,val age:Int) val p=new Person("摇摆少年梦&q

Scala入门到精通——第二十九节 Scala数据库编程

本节主要内容 Scala Mavenproject的创建 Scala JDBC方式訪问MySQL Slick简单介绍 Slick数据库编程实战 SQL与Slick相互转换 本课程在多数内容是在官方教程上改动而来的,官方给的样例是H2数据库上的.经过本人改造,用在MySQL数据库上,官方教程地址:http://slick.typesafe.com/doc/2.1.0/sql-to-slick.html 1. Scala Mavenproject的创建 本节的project项目採用的是Maven P

Scala入门到精通——第十二节 I/O与正则表达式

本节主要内容 Scala I/O操作简介 Scala 写文件 Scala 读文件 Scala 网络I/O 正则表达式简介 Scala正则表达式实战 1. Scala I/O操作简介 I/O操作是一门编程语言中的重要内容,在Scala中,它更多的是调用java中的I/O类或者通过对java中的I/O类进行相应的封装来实现I/O操作.在上一节内容中我们已经用到了I/O操作: trait FileLogger extends Logger{ val fileName:String //PrintWri