apply的基本使用

19 阅读7分钟

在 Scala 中,apply 是一个特殊方法,核心作用是简化对象 / 类的调用语法—— 无需显式写 .apply(),直接像「函数调用」一样使用对象 / 类名加括号即可触发。它广泛用于:集合初始化(如 Map("a"->1))、对象创建、索引访问等场景,是 Scala 语法糖的核心之一。

本文从「基础概念 → 核心使用场景 → 自定义实现」逐步讲解,覆盖日常开发 99% 的 apply 用法。

一、先明确:apply 的核心本质

  1. 语法糖obj(arg1, arg2) 等价于 obj.apply(arg1, arg2),省略 .apply 让代码更简洁。
  2. 无返回值限制apply 可以返回任意类型(值、对象、Unit 等),取决于业务需求。
  3. 支持重载:一个类 / 对象可以定义多个不同参数列表的 apply 方法。
  4. 最常用场景:集合初始化(List(1,2,3)Map("a"->1))、对象工厂(替代 new)、索引访问(array(0)map("key"))。

二、Scala 内置的 apply 用法(日常最常用)

Scala 标准库(集合、数组等)大量使用 apply 简化 API,以下是你每天都会用到的场景:

1. 集合初始化(最高频)

Scala 中所有集合(ListMapSetArray 等)的「字面量初始化」,本质都是调用伴生对象的 apply 方法:

scala

// 1. List 初始化:等价于 List.apply(1, 2, 3)
val list = List(1, 2, 3)

// 2. Map 初始化:等价于 Map.apply(("a",1), ("b",2)) 或 Map.apply("a"->1, "b"->2)
val map = Map("a" -> 1, "b" -> 2)

// 3. Set 初始化:等价于 Set.apply(1, 2, 3)
val set = Set(1, 2, 3)

// 4. Array 初始化:等价于 Array.apply(10, 20, 30)
val array = Array(10, 20, 30)

为什么不用 new List(1,2,3)?因为集合的伴生对象 apply 封装了复杂的初始化逻辑(如不可变集合的节点创建),且返回的是具体实现类(如 List 实际返回 Nil 或 :: 实例),比直接 new 更简洁、灵活。

2. 索引访问(集合 / 数组取值)

数组、MapSeq 等支持「括号索引取值」,本质是调用实例的 apply 方法:

scala

val array = Array(10, 20, 30)
val map = Map("a" -> 1, "b" -> 2)
val list = List(1, 2, 3)

// 1. 数组取值:array(0) 等价于 array.apply(0)
val firstElem = array(0) // 10

// 2. Map 取值:map("a") 等价于 map.apply("a")(注意:Key 不存在抛异常)
val aValue = map("a") // 1

// 3. List 取值:list(1) 等价于 list.apply(1)
val secondElem = list(1) // 2

注意:Map 的 apply 方法在 Key 不存在时会抛 NoSuchElementException,安全取值推荐用 map.get("key")(返回 Option)。

3. 字符串索引(CharSequence 的 apply

Scala 中字符串(String)隐式转换为 CharSequence,支持通过 apply 按索引取字符:

scala

val str = "Scala"
// str(0) 等价于 str.apply(0),返回索引 0 的字符
val firstChar = str(0) // 'S'
val thirdChar = str(2) // 'a'

三、自定义 apply 方法(核心技能)

除了使用内置 apply,你还可以在伴生对象中自定义 apply,实现灵活的逻辑(如工厂模式、简化对象创建)。

场景 1:伴生对象的 apply(替代 new,工厂模式)

最常用的自定义场景:用伴生对象的 apply 封装对象创建逻辑,避免直接使用 new(隐藏构造细节、支持参数重载)。

示例:用户类的工厂方法

scala

// 定义一个 User 类(私有构造方法,只能通过伴生对象创建)
class User private(val name: String, val age: Int) {
  override def toString: String = s"User($name, $age)"
}

// 伴生对象:定义 apply 方法作为工厂
object User {
  // 重载 1:接收 name 和 age
  def apply(name: String, age: Int): User = new User(name, age)
  
  // 重载 2:只接收 name,age 默认为 18
  def apply(name: String): User = new User(name, 18)
  
  // 重载 3:接收 Map,从 Map 中提取参数
  def apply(params: Map[String, Any]): User = {
    val name = params.getOrElse("name", "未知").asInstanceOf[String]
    val age = params.getOrElse("age", 18).asInstanceOf[Int]
    new User(name, age)
  }
}

// 使用 apply 创建对象(无需 new,直接 User(...))
val user1 = User("张三", 25) // 调用 apply(name, age)
val user2 = User("李四")     // 调用 apply(name)
val user3 = User(Map("name" -> "王五", "age" -> 30)) // 调用 apply(params)

println(user1) // User(张三, 25)
println(user2) // User(李四, 18)
println(user3) // User(王五, 30)

优势:

  1. 隐藏 new 关键字,代码更简洁;
  2. 支持多种参数形式(重载),适配不同创建场景;
  3. 封装构造逻辑(如参数校验、默认值、类型转换),避免外部重复代码。

场景 2:类的 apply(实例的「函数式调用」)

在类中定义 apply,可以让该类的实例像「函数」一样被调用,适合需要「输入参数 → 输出结果」的场景(如计算器、转换器)。

示例:加法计算器类

scala

class Adder(base: Int) {
  // 类的 apply 方法:接收一个数,返回 base + 该数
  def apply(num: Int): Int = base + num
}

// 创建 Adder 实例
val adder5 = new Adder(5) // 基础值为 5

// 调用实例的 apply 方法(省略 .apply)
val result1 = adder5(3)  // 等价于 adder5.apply(3) → 5+3=8
val result2 = adder5(10) // 5+10=15

println(result1) // 8
println(result2) // 15

示例:字符串转换器

scala

class StringTransformer(prefix: String, suffix: String) {
  def apply(str: String): String = s"$prefix$str$suffix"
}

val wrapper = new StringTransformer("[", "]")
val wrapped = wrapper("Scala") // 等价于 wrapper.apply("Scala") → "[Scala]"
println(wrapped) // [Scala]

场景 3:样例类(Case Class)的默认 apply

Scala 的样例类(case class)会自动生成伴生对象和 apply 方法,无需手动定义,直接通过 CaseClass(...) 创建实例(无需 new):

scala

// 样例类:自动生成伴生对象和 apply 方法
case class Person(name: String, age: Int)

// 直接调用 apply 创建实例(无需 new)
val person1 = Person("赵六", 28) // 等价于 Person.apply("赵六", 28)
val person2 = Person("孙七", 32)

println(person1) // Person(赵六,28)

这是样例类最便捷的特性之一,日常开发中创建数据模型(如 DTO、POJO)时,优先用样例类,避免手动写 apply

四、apply 与 update 的区别(避免混淆)

Scala 中还有一个和 apply 类似的特殊方法 update,用于「修改值」,语法糖是 obj(arg1, arg2) = value,等价于 obj.update(arg1, arg2, value)

两者常搭配使用(如集合的「取值」和「赋值」):

scala

// 1. Array 的 apply(取值)和 update(赋值)
val array = Array(10, 20, 30)
array(0) // 取值:apply(0) → 10
array(0) = 100 // 赋值:update(0, 100),等价于 array.update(0, 100)
println(array(0)) // 100

// 2. 可变 Map 的 apply(取值)和 update(赋值)
import scala.collection.mutable.Map
val map = Map("a" -> 1)
map("a") // 取值:apply("a") → 1
map("a") = 2 // 赋值:update("a", 2),等价于 map.update("a", 2)
map("b") = 3 // 新增键值对:update("b", 3)
println(map) // Map(a -> 2, b -> 3)
方法语法糖形式核心用途
applyobj(arg1, arg2)取值、创建对象
updateobj(arg1, arg2) = v赋值、修改值

五、常见使用场景总结

  1. 集合初始化List(...)Map(...)Array(...)(最高频);
  2. 对象工厂:用伴生对象的 apply 替代 new,封装构造逻辑(如样例类);
  3. 索引访问array(0)map("key")str(2)(取值);
  4. 实例函数化:让类的实例像函数一样被调用(如 adder(3));
  5. 参数重载:支持多种参数形式,适配不同使用场景(如 User(name)User(name, age))。

六、注意事项

  1. Key 不存在风险Map 的 apply 方法在 Key 不存在时会抛 NoSuchElementException,安全取值推荐用 map.get(key)(返回 Option)或 map.getOrElse(key, 默认值)
  2. 不可变集合的 apply:不可变集合(如 ListMap)的 apply 只用于「取值」,不支持「赋值」(赋值需用可变集合的 update);
  3. 自定义 apply 的命名apply 是关键字,不能自定义方法名(必须叫 apply);
  4. 伴生对象与类的 apply:伴生对象的 apply 常用于「创建实例」,类的 apply 常用于「实例的函数式调用」,两者分工明确。

总结

apply 是 Scala 中最实用的语法糖之一,核心价值是「简化调用」:

  • 内置用法:集合初始化、索引取值(日常开发直接用,无需关心底层);
  • 自定义用法:伴生对象工厂方法、类的函数式调用(提升代码简洁性和封装性)。

记住一句话:看到 XXX(...) 且 XXX 是对象 / 类名时,本质就是调用 XXX.apply(...)