特质的定义
Scala语言中,采用特质(trait)来代替接口的概念,也就是说,多个类具有相同的特质(trait)时,就可以将这个特质(trait)独立出来,采用关键字trait声明。
Scala中的trait中即可以有抽象属性和方法,也可以有具体的属性和方法,一个类可以混入(mixin)多个特质。
Scala引入trait特征,第一可以替代Java的接口,第二个也是对单继承机制的一种补充。
特质的语法
trait 特质名 { trait体 }
案例一:人类有很多不同的特性,有的人会唱歌,有的人会跳舞,也有即会唱歌又会跳舞。
代码语言:javascript复制 // 跳舞
trait Dancing{
//具体的舞蹈有子类去实现
def dance():Unit
}
// 唱歌
trait Sing{
//具体的歌曲有子类去实现
def song():Unit
}
若是把你我他分类的话,那我们都属于人类
。
// 人类
abstract class Person{
val name:String
}
创建学生类;每个学生都会一些基本舞蹈和歌曲,就需要使用 with
去实现 Dancing
与 Sing
两个 特质(trait)
//学生
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) 的两种实现方式。
- 子类需要继承父类的时候,此时
extends
关键字用于继承class
,特质的实现通过with
关键字实现。 - 子类不需要继承父
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 = "主板"
}
基本语法:
- 没有父类:class 类名 extends 特质1 with 特质2 with 特质3 …
- 有父类:class 类名 extends 父类 with 特质1 with 特质2 with 特质3…
说明
- 类和特质的关系:使用继承的关系。
- 当一个类去继承特质时,第一个连接词是extends,后面是with。
- 如果一个类在继承特质和父类时,应当把父类写在extends后。
对象的混入
这里用上面的案例一
演示说明;
现在的学生,若只会 唱歌
,跳舞
肯定是不行的,有点家庭比较好的学生肯定还有其他的,。比如:弹钢琴,弹吉他
乐器
代码语言:javascript复制 trait MusicalInstruments{
def play:Unit
}
由于该特质(trait)属于个别同学所独有的,所以就无需定义到 Student 中。而是让 使用with
关键字,让个别对象去实现。
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
函数。
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()
。
class H extends A with B with C with D{
override def sayHello(): Unit = println("hello","H")
}
这样运行就没有问题了
代码语言:javascript复制(hello,H)
还有一种情况,子类中可以通过super
访问父类的方法
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()。
语法:
代码语言:javascript复制super[特质名].方法。
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 实现对象持久化步骤
- 实现
Serializable
接口 - 提供
get/set
方法 - 序列化,使用
ObjectOutputStream
对对象写入文件 - 反序列化,读取文件生成对象,使用
ObjectInputStream
scala 中也是也是如此
创建一个Person
类,提供get/set
方法,暂时不指定 Serializable
接口。
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
的自身类型
class ObjectWriteAndRead{
this: Serializable =>
...
}
此时 Person
类就弹错误,告知其需要实现 Serializable
接口。
按照提示实现 Serializable
接口。
class Person extends ObjectWriteAndRead with Serializable {...}
然后再运行
代码语言:javascript复制序列化成功
这就是自身类型
的作用,说重要也不重要,但是是一个很好的辅助
,大大提高我们开发效率,毕竟减少了解决报错的时间。
最后也把反序列化完成吧,在ObjectWriteAndRead
新增一个read
函数,用于进行反序列化
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
- obj.isInstanceOf[T]:判断obj是不是T类型。
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
- obj.asInstanceOf[T]:将obj强转成T类型。
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 中属性也具有多态性。
- classOf获取对象的类名。
def main(args: Array[String]): Unit = {
val clazz: Class[Student] = classOf[Student]
println(clazz.getSimpleName) // 获取类名
}
代码语言:javascript复制Student