一、核心概念与本质
1. 单例对象(Standalone Object)
- 定义:用
object关键字定义的独立对象,是 Scala 实现「单例模式」的原生方式,JVM 加载时会自动创建唯一实例,全局共享。 - 本质:底层编译为
类名$的类 + 一个MODULE$静态常量(指向唯一实例),相当于 Scala 对「单例」的语法糖封装。 - 核心作用:替代 Java 的
static方法 / 属性、定义工具类、程序入口(main方法)。
2. 伴生类 & 伴生对象
- 伴生类:普通的
class定义,存储对象的实例成员(每个实例独有的属性 / 方法)。 - 伴生对象:与伴生类同名、同文件的
object,存储对象的静态 / 共享成员(所有实例共享)。 - 核心特性:伴生类和伴生对象可以互相访问对方的
private成员(包括私有构造器、私有方法 / 属性),这是 Scala 特有的访问权限设计。
关键区别
| 特性 | 单例对象(独立) | 伴生类 | 伴生对象 |
|---|---|---|---|
| 定义关键字 | object | class | object |
| 与类的关系 | 独立,无同名类 | 普通类 | 与伴生类同名、同文件 |
| 实例化方式 | 无需实例化(JVM 自动创建) | new 或 apply | 无需实例化 |
| 存储成员类型 | 工具方法、常量、main 方法 | 实例成员(每个对象独有) | 共享成员(所有实例共享) |
| 访问权限 | 仅自身私有成员 | 可访问伴生对象私有成员 | 可访问伴生类私有成员 |
二、实战示例:独立单例对象
独立单例对象无需关联任何类,常用于定义工具方法、常量或程序入口:
// 独立单例对象(工具类)
object StringUtils {
// 常量(等价于 Java 的 static final)
val EMPTY_STRING: String = ""
// 工具方法(等价于 Java 的 static 方法)
def isEmpty(str: String): Boolean = str == null || str.trim.isEmpty
def reverse(str: String): String = str.reverse
// 程序入口(main 方法必须定义在 object 中)
def main(args: Array[String]): Unit = {
println(isEmpty("")) // 输出:true
println(reverse("Scala")) // 输出:alacS
}
}
// 调用单例对象的成员(无需实例化)
println(StringUtils.EMPTY_STRING) // 输出:""
println(StringUtils.isEmpty("test")) // 输出:false
三、实战示例:伴生类 + 伴生对象
这是最常用的组合方式,核心是「实例成员放类,共享成员放对象」,且互相访问私有成员。
步骤 1:定义伴生类和伴生对象
// 伴生类(存储实例成员)
class Person(private val id: String, var name: String) { // id 是私有属性
// 实例方法(每个 Person 实例独有)
def getInfo: String = s"ID: $id, Name: $name"
// 调用伴生对象的私有方法
def validateId: Boolean = Person.checkIdFormat(id)
}
// 伴生对象(存储共享成员,与伴生类同名、同文件)
object Person {
// 共享常量(所有 Person 实例共享)
val DEFAULT_AGE: Int = 18
// 私有工具方法(仅伴生类/伴生对象可访问)
private def checkIdFormat(id: String): Boolean = {
id != null && id.length == 18 // 模拟身份证格式校验
}
// 工厂方法(apply):简化实例化,隐藏 new 关键字
def apply(name: String): Person = new Person(s"ID-${scala.util.Random.nextLong()}", name)
// 重载 apply 方法:支持多参数实例化
def apply(id: String, name: String): Person = new Person(id, name)
}
步骤 2:使用伴生类和伴生对象
// 1. 用伴生对象的 apply 方法实例化伴生类(无需 new)
val p1 = Person("Alice") // 等价于 Person.apply("Alice")
val p2 = Person("123456789012345678", "Bob")
// 2. 访问伴生类的实例成员
println(p1.name) // 输出:Alice
println(p1.getInfo) // 输出:ID: ID-xxxx, Name: Alice
// 3. 调用伴生类中访问伴生对象私有方法的逻辑
println(p1.validateId) // 输出:false(随机ID长度≠18)
println(p2.validateId) // 输出:true(ID长度=18)
// 4. 访问伴生对象的共享常量
println(Person.DEFAULT_AGE) // 输出:18
// 5. 伴生对象可直接访问伴生类的私有属性(测试)
object Test {
def main(args: Array[String]): Unit = {
val p = new Person("18位身份证号", "Charlie")
// 注意:只有伴生对象能访问伴生类的私有属性,其他类不行
// println(p.id) // 编译报错(Test 不是 Person 的伴生对象)
}
}
核心亮点:私有化构造器
通过伴生对象可以私有化伴生类的构造器,强制用户通过工厂方法(apply)实例化,控制实例创建逻辑:
// 伴生类:主构造器私有化(private 修饰)
class User private (val username: String, val email: String)
// 伴生对象:唯一能创建 User 实例的地方
object User {
// 工厂方法:校验参数合法性后才创建实例
def apply(username: String, email: String): Option[User] = {
if (username.nonEmpty && email.contains("@")) {
Some(new User(username, email))
} else {
None // 参数不合法则返回 None
}
}
}
// 使用:必须通过 User.apply 创建实例
val validUser = User("alice123", "alice@test.com") // Some(User@xxx)
val invalidUser = User("", "invalid-email") // None
println(validUser.get.username) // 输出:alice123
// val user = new User("bob", "bob@test.com") // 编译报错(构造器私有)
四、扩展:case class 的自动伴生对象
Scala 的 case class(样例类)会自动生成伴生对象,并内置 apply、unapply 等方法,简化数据封装:
// 定义 case class(自动生成伴生对象)
case class Book(title: String, author: String, price: Double)
// 1. 无需 new 实例化(自动调用伴生对象的 apply 方法)
val book = Book("Scala编程", "Martin Odersky", 89.0)
// 2. 自动生成 toString、equals 等方法(伴生对象提供)
println(book) // 输出:Book(Scala编程,Martin Odersky,89.0)
// 3. 伴生对象的 unapply 方法(模式匹配用)
book match {
case Book(title, _, price) => println(s"《$title》价格:$price")
}
// 输出:《Scala编程》价格:89.0
总结
-
独立单例对象:用
object定义,全局唯一实例,替代 Java 的static,用于工具类、常量、程序入口。 -
伴生类 & 伴生对象:
- 同名、同文件,
class存实例成员,object存共享成员。 - 核心优势:互相访问私有成员,可私有化构造器,通过
apply方法简化实例化。
- 同名、同文件,
-
关键语法糖:
- 调用
类名(参数)等价于调用伴生对象的apply(参数),无需new。 case class自动生成伴生对象,内置apply/unapply等方法,适合数据封装。
- 调用
-
核心场景:控制实例创建逻辑、分离实例 / 共享成员、实现工具类、程序入口。