Scala 伴生对象:companion object
Scala 语言中没有 Java 那样的 static(静态)关键字。为了实现类似于 Java 中静态成员(类变量、类方法)的功能,Scala 引入了 object 关键字。当 object 与 class 在同一个源文件中且名称相同时,它就被称为“伴生对象”。伴生对象与其对应的类可以互相访问私有成员,是 Scala 中管理静态逻辑、工厂方法和单例模式的核心工具。
1. 定义伴生对象基础结构
创建一个名为 Person 的 Scala 源文件(Person.scala)。在文件中编写同名的类(class)和对象(object)。这两个定义必须在同一个文件中,才能构成伴生关系。
打开你的 IDE 或文本编辑器,输入以下代码:
class Person(val name: String, val age: Int) {
// 实例成员
def introduce(): String = s"Hello, I am $name."
}
// 伴生对象
object Person {
// 静态成员
val defaultAge = 18
def getSpecies(): String = "Homo Sapiens"
}
```
在上述代码中,`Person` 类负责定义实例的行为,而 `Person` 对象则负责定义类的行为(相当于 Java 中的静态方法)。
---
### 2. 访问伴生对象的静态成员
**调用**伴生对象中的方法或字段时,直接使用**对象名**进行引用,无需创建类的实例。这与 Java 中调用 `StaticMethod()` 的方式完全一致。
**执行**以下调用代码:
```scala
// 直接通过伴生对象名访问
val species = Person.getSpecies()
val age = Person.defaultAge
// 输出结果
println(species) // 输出: Homo Sapiens
println(age) // 输出: 18
```
---
### 3. 实现工厂模式:`apply` 方法
伴生对象最常用的场景之一是实现工厂模式。通过在伴生对象中**定义** `apply` 方法,你可以**省略** `new` 关键字来创建类的实例。这种语法糖让代码看起来更像是在调用一个函数。
#### 3.1 定义 `apply` 方法
**修改** `Person` 对象,**添加** `apply` 方法:
```scala
object Person {
val defaultAge = 18
// 定义 apply 方法,接收创建实例所需的参数
def apply(name: String): Person = {
// 使用默认年龄创建实例
new Person(name, defaultAge)
}
def apply(name: String, age: Int): Person = {
new Person(name, age)
}
}
```
#### 3.2 使用工厂方法创建实例
**使用**简化的语法**实例化**对象。当编译器看到 `Person(...)` 时,它会自动**调用** `Person.apply(...)`。
```scala
// 方式 1:使用 new (传统方式)
val p1 = new Person("Alice", 25)
// 方式 2:使用 apply (工厂方式,省略 new)
val p2 = Person("Bob") // 调用 apply(name: String)
val p3 = Person("Charlie", 30) // 调用 apply(name: String, age: Int)
println(p2.name) // 输出: Bob
println(p2.age) // 输出: 18 (使用了默认值)
```
为了更直观地理解调用流程,请参考以下逻辑流向:
```mermaid
graph LR
A[用户代码: Person] -->|隐式转换| B[伴生对象: apply]
B -->|执行逻辑| C{参数匹配}
C -->|name: String| D[返回: new Person, 18]
C -->|name: String, age: Int| E[返回: new Person, age]
D --> F[获得实例 p2]
E --> G[获得实例 p3]
```
---
### 4. 互相访问私有成员
伴生对象与其伴生类之间可以**访问**彼此的 `private` 成员。这一特性打破了 Java 中 private 只能在类内部访问的限制,非常适合用于封装内部逻辑或共享配置。
#### 4.1 在类中访问对象的私有成员
**重构**代码,将一些配置项**设为**私有,并在类中**使用**它们。
```scala
class Person(val name: String, private val realAge: Int) {
// 访问伴生对象的私有字段
def getInfo(): String = {
// Person.secretWord 是 private 的,但在类中可见
s"Name: $name, Secret: ${Person.secretWord}"
}
}
object Person {
// 定义私有字段
private val secretWord = "OpenSesame"
def apply(name: String): Person = new Person(name, 18)
}
```
#### 4.2 在对象中访问类的私有成员
**编写**一个辅助方法,在伴生对象中**提取**类的私有信息。例如,假设我们需要一个工具来比较两个人的真实年龄,而不通过公开的 getter 方法。
```scala
class Person(val name: String, private val realAge: Int) {
// ... 类定义保持不变
}
object Person {
private val secretWord = "SecretCode"
// 在伴生对象中定义方法,访问类的 private 字段 realAge
def compareAge(p1: Person, p2: Person): Int = {
// 直接访问 private realAge
p1.realAge - p2.realAge
}
def apply(name: String): Person = new Person(name, 18)
}
// 使用示例
val alice = new Person("Alice", 30)
val bob = Person("Bob", 25) // age 默认 18,这里假设 apply 逻辑已修改为传入 25
// 调用伴生对象的比较方法
val ageDiff = Person.compareAge(alice, bob)
println(ageDiff) // 输出: 5
```
---
### 5. 定义隐式值与隐式转换
伴生对象是存放隐式转换和隐式参数的最佳场所。当编译器在当前作用域找不到隐式值时,它会自动去伴生对象中**查找**。
#### 5.1 定义隐式转换
**假设**你需要给 `Person` 类增加一个“展示详细资料”的方法,但你不能修改原有的 `Person` 类代码(或者为了保持代码整洁)。**定义**一个隐式类来实现。
```scala
// 仍然在 Person.scala 文件中,或者同一个包对象下
object Person {
// ... 其他成员
// 定义隐式类
implicit class RichPerson(person: Person) {
def showDetails(): Unit = {
println(s"Details: ${person.name} is ${person.realAge} years old.")
}
}
}
5.2 使用隐式转换
导入或直接使用该隐式方法。由于隐式类定义在伴生对象中,只要 Person 类在作用域内,这个转换就会自动生效。
val charlie = Person("Charlie")
// 直接调用原本不存在的方法
// 编译器会自动找到 RichPerson 的包装
charlie.showDetails() // 输出: Details: Charlie is 18 years old.
6. 继承 App 特质作为程序入口
在 Scala 中,程序的入口点通常定义在伴生对象中,并继承 scala.App 特质。这样可以省略编写 def main(args: Array[String]): Unit = ... 的样板代码。
修改伴生对象定义如下:
object MyApp extends App {
// 这里的代码直接作为 main 方法的函数体执行
println("Application Started")
val p = Person("David")
println(p.introduce())
}
7. 核心特征对比表
下表总结了 Scala 中 class(类)与 companion object(伴生对象)的主要区别与联系。
| 特性 | Class (类) | Object (对象/伴生对象) |
|---|---|---|
| 实例化 | 需要 new 关键字(或有 apply 方法) |
不能实例化,本身就是一个单例 |
| 成员性质 | 实例成员(每个对象一份) | 静态成员(全局唯一一份) |
| 访问方式 | 通过实例引用(如 p.name) |
通过名称直接访问(如 Person.method) |
| 包含内容 | 构造器参数、实例方法、实例变量 | apply 方法、工厂方法、静态常量、隐式值 |
| 私有访问 | 只能访问类自己的私有成员 | 可以访问伴生类的私有成员(双向互通) |
| 继承 | 可以继承类和特质 | 可以继承特质(如 App),不能继承类 |

暂无评论,快来抢沙发吧!