四, Scala 伴生对象, 特质
文章目录
四, Scala 伴生对象和伴生类
4.1 单例对象和伴生对象
-
Scala语言是完全面向对象的语言, 所以并没有静态的操作(即在Scala中没有静态的概念, 没有静态变量, 静态方法等等);
-
但是为了能够和Java语言交互(因为Java中有静态概念), Scala使用单例对象去模拟实现Java中的静态概念; 如何模拟呢?
让单例对象的名称与类的名称保持一致
(即object A, class A, 类名为A, 保持一致), 这个单例对象呢, 也有了个新的名称, 我们称之为某个类的伴生对象; -
(即, object A 本身是个单例对象, 但是由于和 class A类名一致了, 就把 object A 看作是 class A的伴生对象, class A叫伴生类);
-
这里注意: 单例对象不等同于伴生对象噢!
4.1.1 什么是单例对象?
[基本语法]
object Person{
val name: String = _
}
单例对象的特点
特点 |
---|
1. 单例对象使用 object 关键字声明, 就像是定义一个类那样, objct Box |
2. 单例对象对应的同名类叫伴生类, 特点就是伴生类和伴生对象同名 |
3.单例对象中的属性和方法都可通过伴生对象名(类名)直接调用访问 |
4. 当一个单例对象和某个类共享一个名称时,这个单例对象称为 伴生对象. 同理,这个类被称为是这个单例对象的伴生类。类和它的伴生对象可以互相访问其私有成员。 |
补充:
- 使用伴生对象来定义那些在伴生类中不依赖于实例化对象而存在的成员变量或者方法。
- 在 Java 中 static 成员对应于 Scala 中的伴生对象的普通成员。
[案例实操]
//(1)伴生对象采用 object 关键字声明
object Person {
var country: String = "China"
}
//(2)伴生对象对应的类称之为伴生类,伴生对象的名称应该和伴生类名一致。
class Person {
var name: String = "bobo"
}
object Test {
def main(args: Array[String]): Unit = {
//(3)伴生对象中的属性和方法都可以通过伴生对象名(类名)直接调用访
问。
println(Person.country)
}
}
- 参考文章: Scala官方文档: 单例对象, 伴生对象
4.1.2 如何使用Scala的伴生对象和伴生类来实现单例模式?
我们先来简单的回顾一下Java或其他编程语言中学到的单例模式:
- 具体文章: 单例模式的六种写法
[案例实操]
- 前面我们知道了 object xx(单例对象)是Scala语言中专门处理静态成员的角色, 所以我们把静态私有单例类对象, 静态公有获取单例类对象的方法均放置在单例对象中;
- 把私有的构造方法直接在类声明中体现出来 class xx private(){}, 即如果想让主构造器变成私有的, 可以在()之前加上private;
package SingletonPattern
object Singleton {
//私有的静态的单例类实例对象
var singleton: Singleton = _
//公有的静态的获取单例类对象的方法
def getSingleton(): Singleton = {
if(singleton == null)
singleton = new Singleton()
singleton
}
}
class Singleton private(){ //单例类的构造方法私有化
println("伴生类的方法执行了")
}
object Test{
def main(args: Array[String]): Unit = {
//获取单例对象
var singleton1 = Singleton.getSingleton()
var singleton2 = Singleton.getSingleton()
println(singleton1)
println(singleton2)
}
}
4.1.2 apply方法
4.1.2.1 什么是apply方法及其作用是什么?
待补充
参考文章: 什么是apply?
apply()的定义和作用 |
---|
1. apply目的是缩小Scala面向对象和函数式范式之间的差距 |
2. 借助apply()方法, Scala中的函数可以被看做是对象 |
3. apply方法类似于Python中的_call_, 它允许您将给定类的实例用作函数 |
没看懂? 请仔细看下面的一段话, 并认真体会下面的代码!
- apply缩短了面向对象和函数式范式之间的差距
- 当我们
将对象以函数的方式进行调用时,Scala会隐式地将函数调用转换为在该伴生对象中调用apply()方法
, 什么是函数调用?f(n)
!
如 Person(“xiaoming”) 等同于 Person.apply(“xiaoming”) , 请仔细看下面的代码- 所以我们经常在伴生对象中使用 apply方法作为创建类实例的工厂方法
object Person{
def main(args: Array[String]): Unit = {
Person("xiaomign") // 等同于 Person p = new Person("xiaoming"), 实例化Person类并传入name参数
}
def apply(name: String): Unit = {
//在这里实例化对象了, 所以此时apply可以作为提供了Person类的实例对象的工厂方法
Person p = new Person(name)
}
class Person(){
//辅构造器
//带参构造器
//辅构造器一定要直接或间接的调用主构造器
var name: String = _
this(_name: String){
this()
this.name = _name
}
}
}
[案例一,]
package traitdemo
class MyAdder(x: Int) {
def apply(y: Int) = x + y
}
object MyAdder{
def main(args: Array[String]): Unit = {
val adder = new MyAdder(2)
val res = adder(4) // equivalent to
println(s"res = ${res}") //6
println(adder.apply(4)) //也是6
}
}
[案例二, apply 在创建对象时的应用]
- 通过伴生对象的apply() 方法,
实现不使用new方法创建对象, 可以对外隐藏构造方法的调用
; - apply() 可以重载;
- Scala中 obj(arg)的语句实际是在调用该对象的apply方法, 即 obj.apply(arg), 用以统一面向对象编程和函数式编程的风格;
- 当
使用new 关键字构建对象时, 调用的其实是类的构造方法
, 当使用类名构建对象时, 调用的其实是伴生对象的apply()方法
; - 类名构建对象: new Person() 等价于 Person()
object Test {
def main(args: Array[String]): Unit = {
//(1)通过伴生对象的 apply 方法,实现不使用 new 关键字创建对象。
val p1 = Person()
println("p1.name=" + p1.name)
val p2 = Person("bobo")
println("p2.name=" + p2.name)
}
}
//(2)如果想让主构造器变成私有的,可以在()之前加上 private
class Person private(cName: String) {
var name: String = cName
}
object Person {
def apply(): Person = {
println("apply 空参被调用")
new Person("xx")
}
def apply(name: String): Person = {
println("apply 有参被调用")
new Person(name)
}
}
4.2 特质(Trait)
- Scala中, 利用trait(特质)类代替接口的概念, 也就是说, 当**多个类具有相同的特质(特征)**时, 就可以将这个特质(特征独立出来), 采用关键字trait声明;
- Scla中的trait中既可以有抽象属性和方法, 也可以有具体的属性和方法, 一个类可以混入(mix in)多个特质;
- Scala引入trait特征值,
- 第一可以替代java的接口,
- 第二也是对单继承机制的一种补充;
4.2.1 特质的声明
[基本语法]
trait 特质名{
trait 主体
}
[案例实操]
trait PersonTrait{
//声明属性
var name: String = _
//声明方法:
def eat(): Unit = {
方法体
}
//抽象属性
var age: Int
//抽象方法(省略方法体)
def say(): Unit
}
4.2.2 特质基本语法
- 一个类具有某种特质(特征), 就意味着这个类满足了这个特质(特征)的所有要素,
- 所以在使用时, 也采用了
extends
关键字 - 如果有多个特质或存在父类, 那么需要采用 with关键字连接
[基本语法]
//1. 没有父类
class 类名 extends 特质1 with 特质2 whit 特质3 ...
//2. 有父类
class 类名 extends 父类 with 特质1 with 特质2 with 特质3 ...
trait PersonTrait {
//(1)特质可以同时拥有抽象方法和具体方法
// 声明属性
var name: String = _
// 抽象属性
ar age: Int
// 声明方法
def eat(): Unit = {
println("eat")
}
// 抽象方法
def say(): Unit
}
trait SexTrait {
var sex: String
}
//(2)一个类可以实现/继承多个特质
//(3)所有的 Java 接口都可以当做 Scala 特质使用
class Teacher extends PersonTrait with java.io.Serializable {
override def say(): Unit = {
println("say")
}
override var age: Int = _
}
object TestTrait {
def main(args: Array[String]): Unit = {
val teacher = new Teacher
teacher.say()
teacher.eat()
//(4)动态混入:可灵活的扩展类的功能
val t2 = new Teacher with SexTrait {
override var sex: String = "男"
}
//调用混入 trait 的属性
println(t2.sex)
}
}
4.2.3 特质叠加
- 由于一个类可以混入(mixin)多个trait, 且trait中可以有具体的属性和方法,若
混入的特质中具有下同的方法(方法名,参数列表,返回值均相同), 必然会发生继承冲突问题
- 冲突分为两种
[特质冲突-普通冲突]
- 一个类中
混入了多个特质, 如果这多个特质中有相同的方法
(同名同参通返回),但是这多个特质之间没有任何关系
, 那么直接在类中override 相同的方法即可
[特制叠加-钻石叠加问题]
- 一个类(sub)混入了两个特质(TraitA, TraitB), 特质A和特质B具有相同的具体方法m1, 并且这两个特质均继承自相同的特质(TraitC),
- 在类(Sub)中如何调用m1方法, 就是所谓的"钻石问题",
- 解决钻石叠加问题, 只需要记住: 特质声明的顺序是从左到右, 而方法执行的顺序是从右向左
package traitconflictdemo
object DemonTest {
def main(args: Array[String]): Unit = {
val ball = new MyBall
ball.descri()
println("======================================")
//动态混入
val ball2 = new MyNewBall with Category with Color {
override def descri(): String = {
" my ball is a new " + super.descri()
}
}
ball2.descri()
}
case class MyNewBall()
}
//钻石叠加问题: sub 混入特质A,B, 特质A.B具有相同的方法, 特质AB均继承了特质C, 特质方法调用从左向右 (a->b->c)
// c(ball)
// b(color) a(category)
// sub(myBall)
trait Ball{
var ball: String = "ball"
def descri(): String = ball
}
trait Color extends Ball{
var color: String = "red"
override def descri(): String = color + super.descri()
}
trait Category extends Ball{
var category: String = "basket"
override def descri(): String = category + super.descri()
}
class MyBall extends Category with Color{
override def descri(): String = {
"my ball is a " + super.descri()
}
}
super并不是指继承关系,而是指的加载顺序,从右到左,不一定调用父类的,而是调用左边的混入特质,左边没有了,才调用父类的
抽象类混入特质,从右往左调用,super调用的是当前特质左边的特质,如果没有特质了,才调用父类的,而不是直接调用父类的方法,如果想直接调用父类的,可以加泛型限制,
4.2.4 特质叠加执行顺序
4.2.5 特质和抽象类的区别
- 优先使用特质; 一个类扩展多个特质是很方便的, 但是只能扩展一个抽象类;
- 如果你需要构造函数参数, 使用抽象类, 因为抽象类可以定义带参数的构造函数, 而特质不行(无论是有参还是无参的构造)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)