scala的单例对象和伴生类

37 阅读3分钟

一、核心概念与本质

1. 单例对象(Standalone Object)

  • 定义:用 object 关键字定义的独立对象,是 Scala 实现「单例模式」的原生方式,JVM 加载时会自动创建唯一实例,全局共享。
  • 本质:底层编译为 类名$ 的类 + 一个 MODULE$ 静态常量(指向唯一实例),相当于 Scala 对「单例」的语法糖封装。
  • 核心作用:替代 Java 的 static 方法 / 属性、定义工具类、程序入口(main 方法)。

2. 伴生类 & 伴生对象

  • 伴生类:普通的 class 定义,存储对象的实例成员(每个实例独有的属性 / 方法)。
  • 伴生对象:与伴生类同名、同文件object,存储对象的静态 / 共享成员(所有实例共享)。
  • 核心特性:伴生类和伴生对象可以互相访问对方的 private 成员(包括私有构造器、私有方法 / 属性),这是 Scala 特有的访问权限设计。

关键区别

特性单例对象(独立)伴生类伴生对象
定义关键字objectclassobject
与类的关系独立,无同名类普通类与伴生类同名、同文件
实例化方式无需实例化(JVM 自动创建)newapply无需实例化
存储成员类型工具方法、常量、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(样例类)会自动生成伴生对象,并内置 applyunapply 等方法,简化数据封装:

// 定义 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

总结

  1. 独立单例对象:用 object 定义,全局唯一实例,替代 Java 的 static,用于工具类、常量、程序入口。

  2. 伴生类 & 伴生对象

    • 同名、同文件,class 存实例成员,object 存共享成员。
    • 核心优势:互相访问私有成员,可私有化构造器,通过 apply 方法简化实例化。
  3. 关键语法糖

    • 调用 类名(参数) 等价于调用伴生对象的 apply(参数),无需 new
    • case class 自动生成伴生对象,内置 apply/unapply 等方法,适合数据封装。
  4. 核心场景:控制实例创建逻辑、分离实例 / 共享成员、实现工具类、程序入口。