scala(十二) 特质

2022-04-18 15:09:24 浏览数 (1)

特质的定义

Scala语言中,采用特质(trait)来代替接口的概念,也就是说,多个类具有相同的特质(trait)时,就可以将这个特质(trait)独立出来,采用关键字trait声明。

Scala中的trait中即可以有抽象属性和方法,也可以有具体的属性和方法,一个类可以混入(mixin)多个特质

Scala引入trait特征,第一可以替代Java的接口,第二个也是对单继承机制的一种补充。

特质的语法

trait 特质名 { trait体 }

案例一:人类有很多不同的特性,有的人会唱歌,有的人会跳舞,也有即会唱歌又会跳舞。

代码语言:javascript复制
  // 跳舞
  trait Dancing{
    //具体的舞蹈有子类去实现
    def dance():Unit
  }

  // 唱歌
  trait Sing{
    //具体的歌曲有子类去实现
    def song():Unit
  }

若是把你我他分类的话,那我们都属于人类

代码语言:javascript复制
  // 人类
  abstract class Person{

    val name:String

  }

创建学生类;每个学生都会一些基本舞蹈和歌曲,就需要使用 with 去实现 DancingSing两个 特质(trait)

代码语言:javascript复制
  //学生
  class Student(val na:String) extends Person with Dancing with Sing {
    //重写 姓名属性
    override val name:String=this.na

    override def dance(): Unit = {
      println(s"$name 在跳,广播体操")
    }

    override def song(): Unit = {
      println(s"$name 在唱,保卫黄河")
    }
  
}
代码语言:javascript复制
  def main(args: Array[String]): Unit = {
    val student=new Student("马小跳")
    student.dance() // 马小跳 在跳,广播体操
    student.song() // 马小跳 在唱,保卫黄河
  }

子类需要继承父类的时候,此时extends 关键字用于继承class,特质的实现通过with关键字实现。

案例二:程序员工作离不开电脑,一台电脑组成又分为很多模块。 主板:

代码语言:javascript复制
  trait Motherboard{
    def boardInfo():Unit 
  }

CPU:

代码语言:javascript复制
  trait CPU{
    def cpuInfo():Unit
  }

内存:

代码语言:javascript复制
  trait RAM{
    def ramInfo():Unit
  }

额太多了,就不写了,

定义好电脑的模板

代码语言:javascript复制
  class Computer(val board:String,val cpu:String,val ram:String) extends Motherboard with CPU with RAM{
    override def boardInfo(): Unit = {
      println(s"主板:$board")
    }

    override def cpuInfo(): Unit = println(s"CPU:$cpu")

    override def ramInfo(): Unit = println(s"内存:$ram")
  }

根据我们的配置,组装电脑

代码语言:javascript复制
  def main(args: Array[String]): Unit = {

    val   computer=new Computer("超级好的主板","超级好的CPU","超级大的内存")
    
    computer.boardInfo()
    computer.cpuInfo()
    computer.ramInfo()
   
  }
代码语言:javascript复制
主板:超级好的主板
CPU:超级好的CPU
内存:超级大的内存

子类不需要继承父class 的时候 ,此时 第一个特质的实现通过 extends 关键字来实现,其他特质依旧使用 with关键字。

通过上面两种案例讲解说明,特质(trait) 的两种实现方式

  1. 子类需要继承父类的时候,此时extends 关键字用于继承class,特质的实现通过with关键字实现。
  2. 子类不需要继承父class 的时候 ,此时 第一个特质的实现通过 extends 关键字来实现,其他特质依旧使用 with关键字。

在特质(trait)中 既可以定义抽象方法,也可以定义具体方法。 如:拿案例二演示(不管合不合理,意思明白就行)。

代码语言:javascript复制
  trait Motherboard{

    def boardInfo():Unit

    //内存插口
    def ramInterface(): Unit ={
      println("只有两个内存插口")
    }

  }

具体方法,子类可不用重写,直接调用即可。

代码语言:javascript复制
  def main(args: Array[String]): Unit = {
    val computer=new Computer("超级好的主板","超级好的CPU","超级大的内存")
    computer.ramInterface()
  }

在特质(trait)中 既可以定义抽象属性,也可以定义具体属性。

代码语言:javascript复制
  // 主板
  trait Motherboard{
    // 抽象属性
    val info:String
    // 具体属性
    val area=18
    

    def boardInfo():Unit

    //内存插口
    def ramInterface(): Unit ={
      println("只有两个内存插口")
    }

  }

抽象属性需要被子类重写

代码语言:javascript复制
  class Computer(val board:String,val cpu:String,val ram:String) extends Motherboard with CPU with RAM{
    override def boardInfo(): Unit = println(s"主板:$board")
    override def cpuInfo(): Unit = println(s"CPU:$cpu")
    override def ramInfo(): Unit = println(s"内存:$ram")

    // 重写 Motherboard 中的 info 属性
    override val info: String = "主板"
  }

基本语法:

  1. 没有父类:class 类名 extends 特质1 with 特质2 with 特质3 …
  2. 有父类:class 类名 extends 父类 with 特质1 with 特质2 with 特质3…

说明

  1. 类和特质的关系:使用继承的关系。
  2. 当一个类去继承特质时,第一个连接词是extends,后面是with。
  3. 如果一个类在继承特质和父类时,应当把父类写在extends后。

对象的混入

这里用上面的案例一 演示说明; 现在的学生,若只会 唱歌跳舞 肯定是不行的,有点家庭比较好的学生肯定还有其他的,。比如:弹钢琴,弹吉他

乐器

代码语言:javascript复制
  trait MusicalInstruments{
    def play:Unit
  }

由于该特质(trait)属于个别同学所独有的,所以就无需定义到 Student 中。而是让 使用with关键字,让个别对象去实现。

代码语言:javascript复制
  def main(args: Array[String]): Unit = {
    val weh=new Student("王二虎") with  MusicalInstruments {
      override def play(): Unit = {println(s"$name 弹棉花")}
    }
    weh.dance()
    weh.song()
    weh.play()

    val lff=new Student("李菲菲") with  MusicalInstruments {
      override def play(): Unit = {println(s"$name 弹棉花")}
    }

    lff.dance()
    lff.song()
    lff.play()

    val mxt=new Student("马小跳")

    mxt.dance()
    mxt.song()

  }

输出结果

代码语言:javascript复制
王二虎 在跳,广播体操
王二虎 在唱,保卫黄河
王二虎 弹棉花
李菲菲 在跳,广播体操
李菲菲 在唱,保卫黄河
李菲菲 弹棉花
马小跳 在跳,广播体操
马小跳 在唱,保卫黄河

这种行为就叫对象混入 (2)一个类可以混入(mixin)多个特质 (3)所有的Java接口都可以当做Scala特质使用 (4)动态混入:可灵活的扩展类的功能

方法叠加

由于一个类可以混入(mixin)多个trait,且trait中可以有具体的属性和方法,若混入的特质中具有相同的方法(方法名,参数列表,返回值均相同),必然会出现继承冲突问题。冲突分为以下两种:

第一种,一个类(Sub)混入的两个trait(TraitA,TraitB)中具有相同的具体方法,且两个trait之间没有任何关系,解决这类冲突问题,直接在类(Sub)中重写冲突方法。

第二种,一个类(Sub)混入的两个trait(TraitA,TraitB)中具有相同的具体方法,且两个trait继承自相同的trait(TraitC),及所谓的“钻石问题”,解决这类冲突问题,Scala采用了特质叠加的策略。

所谓的特质叠加,就是将混入的多个trait中的冲突方法叠加起来;

定义五个特质(trait)

代码语言:javascript复制
  trait A{
    def sayHello()={
      println("hello","A")
    }
  }

  trait B{

    def sayHello()={
      println("hello","B")
    }
  }

  trait C{

    def sayHello()={
      println("hello","C")
    }
  }

  trait D{

    def sayHello()={
      println("hello","D")
    }
 }

定义 一个 H 类 去实现这些特质(trait)

代码语言:javascript复制
class H extends A with B with C with D{}

通过 H 调用 sayHello()

代码语言:javascript复制
  def main(args: Array[String]): Unit = {
    val h =new H
    h.sayHello()

  }

结果报错了;意思是说,这些函数有歧义,不指定该调用谁的 sayHello 函数。

代码语言:javascript复制
Error:(34, 9) class H inherits conflicting members:
  method sayHello in trait C of type ()Unit  and
  method sayHello in trait D of type ()Unit
(Note: this can be resolved by declaring an override in class H.)
  class H extends A with B with C with D{}

也提示你this can be resolved by declaring an override in class H. ;让你重写 sayHello()

代码语言:javascript复制
  class H extends A with B with C with D{

    override def sayHello(): Unit =  println("hello","H")
  }

这样运行就没有问题了

代码语言:javascript复制
(hello,H)

还有一种情况,子类中可以通过super 访问父类的方法

代码语言:javascript复制
  class H extends A with B with C with D{
    super.sayHello()
    override def sayHello(): Unit =  println("hello","H")
  }

结果:super.sayHello()打印的是 D 的 sayHello()。

代码语言:javascript复制
(hello,D)
(hello,H)

这种结果产生的原因很简单,因为的继承顺序是从 A-D,所以从左到右的顺序去找最后一个重名的函数,所以运行的是特质D的sayHello()。

若要调用指定的特质的sayHello(),可以使用[]指定。 如调用 特质B 的sayHello()。 语法:

super[特质名].方法。

代码语言:javascript复制
 class H extends A with B with C with D{
    super[B].sayHello()
    override def sayHello(): Unit =  println("hello","H")
  }
代码语言:javascript复制
(hello,B)
(hello,H)

以上就是第一种冲突方式,及解决方式。

说完第一种,还有第二种,指多个特质(trait)是有联系的。 再定义一个特质(trait) F 并定义一个 sayHello()

代码语言:javascript复制
  trait F{
    def sayHello()={
      println("hello","F")
    }
  }

其他特质A-D 继承 F 重写 sayHello(),并各自调用父类(F) 的sayHello()。

代码语言:javascript复制
  trait A extends F {

    override def sayHello()={
      println("hello","A")
      super.sayHello()
    }
  }

  trait B extends F{

    override def sayHello()={
      println("hello","B")
      super.sayHello()
    }
  }

  trait C extends F{

    override def sayHello()={
      println("hello","C")
      super.sayHello()
    }
  }

  trait D extends F{

    override def sayHello()={
      println("hello","D")
      super.sayHello()
    }
  }

H 不变

代码语言:javascript复制
  class H extends A with B with C with D{
    super.sayHello()
    override def sayHello(): Unit =  println("hello","H")
  }

此时的运行结果如何?

代码语言:javascript复制
  def main(args: Array[String]): Unit = {
    val h =new H
    h.sayHello()
  }

结果:这种就是所谓的“钻石问题”

代码语言:javascript复制
(hello,D)
(hello,C)
(hello,B)
(hello,A)
(hello,F)
(hello,H)

原理也很简单。 首先运行的是H,H调用spuer 会运行特质D,然后由D调用 super ,此时D并不是调用 F,而是指向的是上一个特质C ,然后C又指向B,B又指向A,此时A才真正的指向F。由于A-D super 是在打印下面所有先这执行的打印后执行的super。H和其他相反,super在前,打印在后。运行最后运行的是H。

钻石的形式钻石的形式

红色:表示继承或实现 黑色:表示指向

自身类型

有这么一个需求,需要将对象持久化(保存到磁盘) 回顾 java 实现对象持久化步骤

  1. 实现 Serializable 接口
  2. 提供get/set 方法
  3. 序列化,使用ObjectOutputStream 对对象写入文件
  4. 反序列化,读取文件生成对象,使用ObjectInputStream

scala 中也是也是如此 创建一个Person类,提供get/set 方法,暂时不指定 Serializable 接口。

代码语言:javascript复制
  class Person {
    // id
    @BeanProperty var id:Int=_
    // 姓名
    @BeanProperty var name:String=_
    // 年龄
    @BeanProperty var age:Int=_
  }

序列化、反序列化是一个完整的功能,我们可以将其封装到单独的类中。

代码语言:javascript复制
  class ObjectWriteAndRead{
    /**
     * 序列化
     * @param path 文件地址
     */
    def write(path:String): Unit ={
      try {
        val fos=new FileOutputStream(path)
        val objectInput=new ObjectOutputStream(fos)
        // 写入磁盘
       objectStream.writeObject(this)

        // 刷新与关闭
        objectStream.flush()
        objectStream.close()
        fos.close()
        println("序列化成功")
      }catch {
        case e:IOException=>{
          println("序列化失败",e)
        }
      }
    }
  }

Person 若要进行序列化或反序列化可以继承该类

代码语言:javascript复制
 class Person  extends ObjectWriteAndRead{...}

若对象没有实现序列化接口,运行时肯定会报错

代码语言:javascript复制
 def main(args: Array[String]): Unit = {
    //创建对象
    val person=new Person()
    person.setId(1001)
    person.setName("王小二")
    person.setAge(19)

    person.write("D:\aaa.txt")
  }

报错

代码语言:javascript复制
(序列化失败,java.io.NotSerializableException: com.admin.xxx.traita.Demo03$Person)

别觉得我说的是废话,遇到这种情况,我们很多时候都会忘记(是否实现了Serializable接口),只有等到运行报错时,翻看异常信息才恍然大悟。有没有一种机制能让我们在一开始就提示我们,而不是等到出问题之后?

这就需要自身类型了,它主要用于提醒子类,子类继承父类,需要满足继承父类的某些条件。如:必须实现 Serializable接口

语法:

this:类型 =>

案例:指定 ObjectWriteAndRead 的自身类型

代码语言:javascript复制
  class ObjectWriteAndRead{
    this: Serializable =>
    ...
  }

此时 Person 类就弹错误,告知其需要实现 Serializable 接口。

自身类型自身类型

按照提示实现 Serializable 接口。

代码语言:javascript复制
class Person  extends ObjectWriteAndRead with Serializable {...}

然后再运行

代码语言:javascript复制
序列化成功

这就是自身类型的作用,说重要也不重要,但是是一个很好的辅助,大大提高我们开发效率,毕竟减少了解决报错的时间。

最后也把反序列化完成吧,在ObjectWriteAndRead新增一个read函数,用于进行反序列化

代码语言:javascript复制
 class ObjectWriteAndRead{
    this: Serializable =>
    /**
     * 序列化
     * @param path 文件地址
     */
    def write(path:String): Unit ={
      try {
        val fos=new FileOutputStream(path)
        val objectStream=new ObjectOutputStream(fos)
        // 写入磁盘
        objectStream.writeObject(this)

        // 刷新与关闭
        objectStream.flush()
        objectStream.close()
        fos.close()
        println("序列化成功")
      }catch {
        case e:IOException=>{
          println("序列化失败",e)
        }
      }
    }

    /**
     * 反序列化
     * @param path
     * @return
     */
    def read(path:String):AnyRef={

     try {
       val fis=new FileInputStream(path)

       val objectStream=new ObjectInputStream(fis)
       println("反序列化成功")
       objectStream.readObject()
     }catch {
       case e:IOException =>{
         println("反序列化失败",e)
         null
       }
     }
    }
  }

运行

代码语言:javascript复制
  def main(args: Array[String]): Unit = {
    //创建对象
    val person=new Person()
    person.setId(1001)
    person.setName("王小二")
    person.setAge(19)
    // 序列化
    person.write("D:\aaa.txt")

    //反序列化
    val newPerson = person.read("D:\aaa.txt").asInstanceOf[Person]
    println(person.getId)
    println(person.getName)
    println(person.getAge)
  }

person.read("D:aaa.txt") : 是一个反序列化的结果,类型是AnyRef .asInstanceOf[T] 用于类型强转

输出结果

代码语言:javascript复制
序列化成功
反序列化成功
1001
王小二
19

完整代码

代码语言:javascript复制
class Person  extends ObjectWriteAndRead with Serializable {
    // id
    @BeanProperty var id:Int=_
    // 姓名
    @BeanProperty var name:String=_
    // 年龄
    @BeanProperty var age:Int=_

  }

  def main(args: Array[String]): Unit = {
    //创建对象
    val person=new Person()
    person.setId(1001)
    person.setName("王小二")
    person.setAge(19)
    // 序列化
    person.write("D:\aaa.txt")



    //反序列化
    val newPerson = person.read("D:\aaa.txt").asInstanceOf[Person]
    println(person.getId)
    println(person.getName)
    println(person.getAge)

  }


  class ObjectWriteAndRead{
    this: Serializable =>
    /**
     * 序列化
     * @param path 文件地址
     */
    def write(path:String): Unit ={
      try {
        val fos=new FileOutputStream(path)
        val objectStream=new ObjectOutputStream(fos)
        // 写入磁盘
        objectStream.writeObject(this)

        // 刷新与关闭
        objectStream.flush()
        objectStream.close()
        fos.close()
        println("序列化成功")
      }catch {
        case e:IOException=>{
          println("序列化失败",e)
        }
      }
    }

    /**
     * 反序列化
     * @param path
     * @return
     */
    def read(path:String):AnyRef={

     try {
       val fis=new FileInputStream(path)

       val objectStream=new ObjectInputStream(fis)
       println("反序列化成功")
       objectStream.readObject()
     }catch {
       case e:IOException =>{
         println("反序列化失败",e)
         null
       }
     }
    }
  }

扩展

类型检查和转换

定义三个类

代码语言:javascript复制
  class Person{
    val name="Person"
  }

  class Student extends Person{
    override val name: String = "Student"
  }

  class Teacher extends Person{
    override val name: String = "Teacher"
  }

Student 与 Teacher 都继承 Person

  1. obj.isInstanceOf[T]:判断obj是不是T类型。
代码语言:javascript复制
  def main(args: Array[String]): Unit = {
    val stu:Person=new Student

    println(s"判断是否为 Student 类:${stu.isInstanceOf[Student]}")
    println(s"判断是否为 Teacher 类:${stu.isInstanceOf[Teacher]}")
  }
代码语言:javascript复制
判断是否为 Student 类:true
判断是否为 Teacher 类:false
  1. obj.asInstanceOf[T]:将obj强转成T类型。
代码语言:javascript复制
  def main(args: Array[String]): Unit = {
    val stu:Person=new Student

    val o1=stu.asInstanceOf[Person]
    println("o1:" o1.name)

    val o2=stu.asInstanceOf[Student]
    println("o2:" o2.name)

    val o3=stu.asInstanceOf[Teacher]
    println("o3:" o3.name)

  }

o3 无法转换 无法转换为 Teacher

代码语言:javascript复制
o1:Student
o2:Student
Exception in thread "main" java.lang.ClassCastException: com.admin.xxx.traita.Demo04$Student cannot be cast to com.admin.xxx.traita.Demo04$Teacher

至于为啥o1 输出的也是 Student 因为Scala 中属性也具有多态性。

  1. classOf获取对象的类名。
代码语言:javascript复制
  def main(args: Array[String]): Unit = {

    val clazz: Class[Student] = classOf[Student]
    println(clazz.getSimpleName) // 获取类名
  }
代码语言:javascript复制
Student

0 人点赞