Scala核心编程(八)面向对象编程(高级特性)
一、静态属性和静态方法
1.1 提出问题
有一类经典问题:一群小孩在玩堆雪人,不时有新的小孩加入,如何使用面向对象的思想编写程序,知道现在共有多少人在玩?
核心需求:需要一个所有对象共享的计数变量。
1.2 回顾Java的静态概念
在Java中,我们使用static关键字:
publicstatic返回值类型 方法名(参数列表){方法体}// 静态属性...说明:Java中静态方法并不是通过对象调用的,而是通过类对象调用的,所以静态操作并不是面向对象的。
1.3 Scala中的伴生对象
Scala语言是完全面向对象(万物皆对象)的语言,所以并没有静态的操作(即在Scala中没有静态的概念)。但是为了和Java语言交互,产生了一种特殊的对象来模拟类对象,称之为类的伴生对象。
这个类的所有静态内容都可以放置在它的伴生对象中声明和调用。
1.4 伴生对象的快速入门
classScalaPerson{varname:String=_// 伴生类}objectScalaPerson{varsex:Boolean=true// 伴生对象}// 调用println(ScalaPerson.sex)1.5 伴生对象小结
- Scala中伴生对象采用
object关键字声明,伴生对象中声明的全是**"静态"内容,可以通过伴生对象名称**直接调用。 - 伴生对象对应的类称之为伴生类,伴生对象的名称应该和伴生类名一致。
- 伴生对象中的属性和方法都可以通过伴生对象名(类名)直接调用访问。
- 从语法角度来讲,伴生对象其实就是类的静态方法和成员的集合。
- 从技术角度来讲,Scala还是没有生成静态的内容,只不过是将伴生对象生成了一个新的类,实现属性和方法的调用。
- 从底层原理看,伴生对象实现静态特性是依赖于
public static final MODULE$实现的。 - 伴生对象的声明应该和伴生类的声明在同一个源码文件中(如果不在同一个文件中会运行错误!)。
- 如果
class A独立存在,那么A就是一个类;如果object A独立存在,那么A就是一个**"静态"性质的对象**(即类对象)。 - 当一个文件中存在伴生类和伴生对象时,文件的图标会发生变化。
1.6 伴生对象解决小孩游戏问题
classChild{varname:String=_}objectChild{vartotal:Int=0defjoin(child:Child):Unit={total+=1println(s"${child.name}加入了游戏")}}1.7 伴生对象-apply方法
在伴生对象中定义apply方法,可以实现**类名(参数)**方式来创建对象实例:
classCat(cName:String){varname:String=cName}objectCat{// apply方法是当我们创建对象时,可以通过Cat()调用defapply():Cat={println("apply被调用")newCat("xx")}defapply(name:String):Cat={println("apply被调用")newCat(name)}}// 使用valcat1=Cat()// 调用无参applyvalcat2=Cat("Tom")// 调用有参apply1.8 课堂练习
题目:将以下Java题用Scala完成。
- 在Frock类中声明私有的静态属性
currentNum,初始值为100000,作为衣服出厂的序列号起始值。 - 声明公有的静态方法
getNextNum,作为生成上衣唯一序列号的方法。每调用一次,将currentNum增加100,并作为返回值。 - 在TestFrock类的main方法中,分两次调用
getNextNum方法,获取序列号并打印输出。 - 在Frock类中声明
serialNumber(序列号)属性,并提供对应的get方法。 - 在Frock类的构造器中,通过调用
getNextNum方法为Frock对象获取唯一序列号,赋给serialNumber属性。 - 在TestFrock类的main方法中,分别创建三个Frock对象,并打印三个对象的序列号,验证是否为按100递增。
二、特质(trait)
2.1 回顾Java接口
// 声明接口interface接口名// 实现接口class类名implements接口名1,接口2Java接口特点:
- 一个类可以实现多个接口
- 接口之间支持多继承
- 接口中属性都是常量
- 接口中的方法都是抽象的
2.2 Scala接口的介绍
从面向对象来看,接口并不属于面向对象的范畴,Scala是纯面向对象的语言,在Scala中,没有接口。
Scala语言中,采用**特质trait(特征)**来代替接口的概念。也就是说,多个类具有相同的特征时,就可以将这个特质独立出来,采用关键字trait声明。
理解trait等价于 (interface + abstract class)
2.3 trait的声明
trait特质名{// trait体}trait命名一般首字母大写,例如:Cloneable,Serializable
objectT1extendsSerializable{// Serializable就是Scala的一个特质// 在Scala中,Java中的接口可以当做特质使用}2.4 Scala中trait的使用
一个类具有某种特质,就意味着这个类满足了这个特质的所有要素。使用时采用extends关键字,如果有多个特质或存在父类,需要采用with关键字连接。
没有父类:
class类名extends特质1with特质2with特质3..有父类:
class类名extends父类with特质1with特质2with特质32.5 特质的快速入门案例
可以把特质看作是对继承的一种补充。
Scala的继承是单继承,也就是一个类最多只能有一个父类。引入trait特征:
- 第一可以替代Java的接口
- 第二也是对单继承机制的一种补充
traittrait1{// 声明方法,抽象的defgetConnect(user:String,pwd:String):Unit}classA{}classBextendsA{}classCextendsAwithtrait1{overridedefgetConnect(user:String,pwd:String):Unit={println("c连接mysql")}}classD{}classEextendsDwithtrait1{defgetConnect(user:String,pwd:String):Unit={println("e连接oracle")}}classFextendsD{}2.6 特质trait的再说明
- Scala提供了特质(trait),特质可以同时拥有抽象方法和具体方法,一个类可以实现/继承多个特质。
- 特质中没有实现的方法就是抽象方法。类通过
extends继承特质,通过with可以继承多个特质。 - 所有的Java接口都可以当做Scala特质使用。
traitLogger{deflog(msg:String)// 抽象方法}classConsoleextendsLoggerwithCloneablewithSerializable{deflog(msg:String){println(msg)}}2.7 带有具体实现的特质
和Java中的接口不太一样的是,特质中的方法并不一定是抽象的,也可以有非抽象方法(即实现了的方法)。
traitOperate{definsert(id:Int):Unit={println("保存数据="+id)}}traitDBextendsOperate{overridedefinsert(id:Int):Unit={print("向数据库中")super.insert(id)}}classMySQLextendsDB{}2.8 带有特质的对象,动态混入
- 除了可以在类声明时继承特质以外,还可以在构建对象时混入特质,扩展目标类的功能。
- 此种方式也可以应用于对抽象类功能进行扩展。
- 动态混入是Scala特有的方式(Java没有动态混入),可在不修改类声明/定义的情况下,扩展类的功能,非常的灵活,耦合性低。
- 动态混入可以在不影响原有的继承关系的基础上,给指定的类扩展功能。
traitOperate3{definsert(id:Int):Unit={println("插入数据 = "+id)}}classOracleDB{}abstractclassMySQL3{}// 动态混入varoracle=newOracleDBwithOperate3 oracle.insert(999)valmysql=newMySQL3withOperate3 mysql.insert(4)思考:如果抽象类中有抽象的方法,如何动态混入特质?
2.9 创建对象的几种方式
在Scala中创建对象共有几种方式?
new对象apply创建- 匿名子类方式
- 动态混入
2.10 叠加特质
基本介绍:构建对象的同时如果混入多个特质,称之为叠加特质。
特质声明顺序从左到右,方法执行顺序从右到左。
traitOperate4{println("Operate4...")definsert(id:Int)}traitData4extendsOperate4{println("Data4")overridedefinsert(id:Int):Unit={println("插入数据 = "+id)}}traitDB4extendsData4{println("DB4")overridedefinsert(id:Int):Unit={print("向数据库")super.insert(id)}}traitFile4extendsData4{println("File4")overridedefinsert(id:Int):Unit={print("向文件")super.insert(id)}}classMySQL4{}valmysql=newMySQL4withDB4withFile4 mysql.insert(888)叠加特质注意事项和细节:
- 特质声明顺序从左到右。
- Scala在执行叠加对象的方法时,会首先从**后面的特质(从右向左)**开始执行。
- Scala中特质中如果调用
super,并不是表示调用父特质的方法,而是向前面(左边)继续查找特质,如果找不到,才会去父特质查找。 - 如果想要调用具体特质的方法,可以指定:
super[特质].xxx(...),其中的泛型必须是该特质的直接超类类型。
traitFile4extendsData4{println("File4")overridedefinsert(id:Int):Unit={print("向文件")super[Data4].insert(id)// 指定调用Data4的insert}}2.11 在特质中重写抽象方法
提出问题:
traitOperate5{definsert(id:Int)}traitFile5extendsOperate5{definsert(id:Int):Unit={println("将数据保存到文件中..")super.insert(id)// 报错!}}运行代码会出错,原因就是没有完全的实现insert,同时还没有声明abstract override。
解决问题:
方式1:去掉super()…
方式2:调用父特质的抽象方法,那么在实际使用时,没有方法的具体实现,无法编译通过。为了避免这种情况的发生,可重写抽象方法,这样在使用时,就必须考虑动态混入的顺序问题。
traitOperate5{definsert(id:Int)}traitFile5extendsOperate5{abstractoverridedefinsert(id:Int):Unit={println("将数据保存到文件中..")super.insert(id)}}traitDB5extendsOperate5{definsert(id:Int):Unit={println("将数据保存到数据库中..")}}classMySQL5{}valmysql5=newMySQL5withDB5withFile5理解 abstract override 的小技巧:
当我们给某个方法增加了abstract override后,就是明确地告诉编译器:该方法确实是重写了父特质的抽象方法,但是重写后,该方法仍然是一个抽象方法(因为没有完全的实现,需要其它特质继续实现[通过混入顺序])。
重写抽象方法时需要考虑混入特质的顺序问题和完整性问题:
varmysql2=newMySQL5withDB5// okmysql2.insert(100)varmysql3=newMySQL5withFile5// errorvarmysql4=newMySQL5withFile5withDB5// errorvarmysql4=newMySQL5withDB5withFile5// ok2.12 当作富接口使用的特质
富接口:即该特质中既有抽象方法,又有非抽象方法。
traitOperate{definsert(id:Int)// 抽象defpageQuery(pageno:Int,pagesize:Int):Unit={// 实现println("分页查询")}}2.13 特质中的具体字段
特质中可以定义具体字段,如果初始化了就是具体字段,如果不初始化就是抽象字段。混入该特质的类就具有了该字段,字段不是继承,而是直接加入类,成为自己的字段。
traitOperate6{varopertype:String// 抽象字段}traitDB6extendsOperate6{varopertype:String="insert"// 具体字段definsert():Unit={}}classMySQL6{}varmysql=newMySQL6withDB6 println(mysql.opertype)// insert2.14 特质中的抽象字段
特质中未被初始化的字段在具体的子类中必须被重写。
2.15 特质构造顺序
特质也是有构造器的,构造器中的内容由**“字段的初始化”**和一些其他语句构成。
第一种特质构造顺序(声明类的同时混入特质):
- 调用当前类的超类构造器
- 第一个特质的父特质构造器
- 第一个特质构造器
- 第二个特质构造器的父特质构造器(如果已经执行过,就不再执行)
- 第二个特质构造器
- …重复4,5的步骤(如果有第3个,第4个特质)
- 当前类构造器
traitAA{println("A...")}traitBBextendsAA{println("B....")}traitCCextendsBB{println("C....")}traitDDextendsBB{println("D....")}classEE{println("E...")}classFFextendsEEwithCCwithDD{println("F....")}第二种特质构造顺序(在构建对象时,动态混入特质):
- 调用当前类的超类构造器
- 当前类构造器
- 第一个特质构造器的父特质构造器
- 第一个特质构造器
- 第二个特质构造器的父特质构造器(如果已经执行过,就不再执行)
- 第二个特质构造器
- …重复5,6的步骤
- 当前类构造器
分析两种方式对构造顺序的影响:
- 第1种方式实际是构建类对象,在混入特质时,该对象还没有创建。
- 第2种方式实际是构造匿名子类,可以理解成在混入特质时,对象已经创建了。
2.16 扩展类的特质
特质可以继承类,以用来拓展该类的一些功能。
traitLoggedExceptionextendsException{deflog():Unit={println(getMessage())// 方法来自于Exception类}}所有混入该特质的类,会自动成为那个特质所继承的超类的子类。
// UnhappyException 就是Exception的子类classUnhappyExceptionextendsLoggedException{// 已经是Exception的子类了,所以可以重写方法overridedefgetMessage="错误消息!"}注意:如果混入该特质的类,已经继承了另一个类(A类),则要求A类是特质超类的子类,否则就会出现多继承现象,发生错误。
// 正确:因为 IndexOutOfBoundsException 是 LoggedException特质的超类Exception的子类classUnhappyException2extendsIndexOutOfBoundsExceptionwithLoggedException{overridedefgetMessage="错误信息!"}classCCC{}// 错误:因为 CCC 不是 LoggedException特质的超类Exception的子类classUnhappyException3extendsCCCwithLoggedException{// 编译错误overridedefgetMessage="错误信息!"}2.17 自身类型
说明:自身类型主要是为了解决特质的循环依赖问题,同时可以确保特质在不扩展某个类的情况下,依然可以做到限制混入该特质的类的类型。
// Logger就是自身类型特质traitLogger{// 明确告诉编译器,我就是Exception// 如果没有这句话,下面的getMessage不能调用this:Exception=>deflog():Unit={// 既然我就是Exception,那么就可以调用其中的方法println(getMessage)}}// 对吗?classConsoleextendsLogger{}// 错误!// 对吗?classConsoleextendsExceptionwithLogger{}// 正确!三、嵌套类
3.1 基本介绍
在Scala中,你几乎可以在任何语法结构中内嵌任何语法结构。如在类中可以再定义一个类,这样的类是嵌套类,其他语法结构也是一样。嵌套类类似于Java中的内部类。
面试题:Java中,类共有五大成员:
- 属性
- 方法
- 内部类
- 构造器
- 代码块
3.2 Java内部类的简单回顾
在Java中,一个类的内部又完整的嵌套了另一个完整的类结构。被嵌套的类称为内部类(inner class),嵌套其他类的类称为外部类。内部类最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系。
classOuter{// 外部类classInner{// 内部类}}classOther{// 外部其他类}3.3 Java内部类的分类
从定义在外部类的成员位置上来看:
- 成员内部类(没用static修饰)
- 静态内部类(使用static修饰)
定义在外部类局部位置上(比如方法内)来看:
- 局部内部类(有类名)
- 匿名内部类(没有类名)
3.4 Scala嵌套类的使用
定义成员内部类和静态内部类:
classScalaOuterClass{classScalaInnerClass{// 成员内部类}}objectScalaOuterClass{// 伴生对象classScalaStaticInnerClass{// 静态内部类}}创建对象:
valouter1:ScalaOuterClass=newScalaOuterClass()valouter2:ScalaOuterClass=newScalaOuterClass()// Scala创建内部类的方式和Java不一样,将new关键字放置在前// 使用 对象.内部类 的方式创建valinner1=newouter1.ScalaInnerClass()valinner2=newouter2.ScalaInnerClass()// 创建静态内部类对象valstaticInner=newScalaOuterClass.ScalaStaticInnerClass()3.5 在内部类中访问外部类的属性
方式1:通过外部类对象访问
访问方式:外部类名.this.属性名
classScalaOuterClass{varname:String="scott"privatevarsal:Double=1.2classScalaInnerClass{// 成员内部类definfo()={// 访问方式:外部类名.this.属性名println("name = "+ScalaOuterClass.this.name+" age ="+ScalaOuterClass.this.sal)}}}
ScalaOuterClass.this相当于是ScalaOuterClass这个外部类的一个实例。
方式2:通过外部类别名访问(推荐)
访问方式:外部类名别名.属性名
classScalaOuterClass{myOuter=>// 这样写,myOuter就是代表外部类的一个对象classScalaInnerClass{// 成员内部类definfo()={println("name = "+ScalaOuterClass.this.name+" age ="+ScalaOuterClass.this.sal)println("name = "+myOuter.name+" age ="+myOuter.sal)}}// 当给外部指定别名时,需要将外部类的属性放到别名后varname:String="scott"privatevarsal:Double=1.2}
外部类名.this等价于外部类名别名
3.6 类型投影
问题引出:
classScalaOuterClass3{myOuter=>classScalaInnerClass3{// 成员内部类deftest(ic:ScalaInnerClass3):Unit={System.out.println(ic)}}}valouter1:ScalaOuterClass3=newScalaOuterClass3()valouter2:ScalaOuterClass3=newScalaOuterClass3()valinner1=newouter1.ScalaInnerClass3()valinner2=newouter2.ScalaInnerClass3()inner1.test(inner1)// ok,因为需要outer1.ScalaInnerinner1.test(inner2)// error,需要outer1.ScalaInner,而不是outer2.ScalaInner分析:
- Java中的内部类从属于外部类,因此在Java中
inner.test(inner2)就可以,因为是按类型来匹配的。 - Scala中内部类从属于外部类的对象,所以外部类的对象不一样,创建出来的内部类也不一样,无法互换使用。
解决方式 - 使用类型投影:
类型投影是指:在方法声明上,如果使用外部类#内部类的方式,表示忽略内部类的对象关系,等同于Java中内部类的语法操作。
即:忽略对象的创建方式,只考虑类型。
classScalaOuterClass3{myOuter=>classScalaInnerClass3{// 使用类型投影deftest(ic:ScalaOuterClass3#ScalaInnerClass3):Unit={System.out.println(ic)}}}总结
本章深入讲解了Scala面向对象编程的三大高级特性:
| 特性 | 核心要点 |
|---|---|
| 伴生对象 | 用object模拟静态,实现apply工厂方法 |
| 特质(trait) | 替代接口,支持具体实现、动态混入、叠加特性 |
| 嵌套类 | 类型投影解决内部类类型绑定问题 |
掌握这些特性,你就能写出更加优雅、灵活的Scala代码!
