1. 概述
在 Scala 中,当我们需要从集合中筛选出满足条件的元素时,通常会使用 filter
或 withFilter
方法。虽然这两个方法看起来作用类似,但它们的行为和性能表现却有显著差异。
本文将带你深入理解两者的区别,并分析它们在实际使用中的性能影响。
2. 示例准备
我们以一个程序员列表为例,每个程序员包含姓名、级别(Junior、Mid、Senior)以及掌握的语言列表:
case class Programmer(name: String,
level: Level,
knownLanguages: List[String])
sealed trait Level
object Level {
case object Junior extends Level
case object Mid extends Level
case object Senior extends Level
}
构造一些测试数据:
val programmers: List[Programmer] = List(
Programmer(name = "Kelly",
level = Level.Mid,
knownLanguages = List("JavaScript")),
Programmer(name = "John",
level = Level.Senior,
knownLanguages = List("Java", "Scala", "Kotlin")),
Programmer(name = "Dave",
level = Level.Junior,
knownLanguages = List("C", "C++"))
)
接着定义两个谓词函数用于过滤,同时增加计数器便于观察执行次数:
def isMidOrSenior(implicit counter: AtomicInteger): Programmer => Boolean =
programmer => {
counter.incrementAndGet()
println("verify level " + programmer)
List(Level.Mid, Level.Senior).contains(programmer.level)
}
def knowsMoreThan1Language(implicit counter: AtomicInteger): Programmer => Boolean =
programmer => {
counter.incrementAndGet()
println("verify number of known languages " + programmer)
programmer.knownLanguages.size > 1
}
val getName: Programmer => String =
programmer => {
println("get name " + programmer)
programmer.name
}
我们的目标是找出那些不是初级(Junior)并且掌握超过一门语言的程序员的名字。
3. filter 方法详解
3.1. 签名说明
filter
的方法签名为:
def filter(p: A => Boolean): Repr
其中:
A
是集合中的元素类型,在这里就是Programmer
Repr
是当前集合的实际类型,这里是List[Programmer]
3.2. 执行过程分析
来看具体使用示例:
implicit val counter: AtomicInteger = new AtomicInteger(0)
val desiredProgrammers: List[Programmer] = programmers
.filter(isMidOrSenior)
.filter(knowsMoreThan1Language)
counter.get() shouldBe 5
desiredProgrammers.map(getName) shouldBe List("John")
counter.get() shouldBe 5
控制台输出如下:
verify level Programmer(Kelly,Mid,List(JavaScript))
verify level Programmer(John,Senior,List(Java, Scala, Kotlin))
verify level Programmer(Dave,Junior,List(C, C++))
verify number of known languages Programmer(Kelly,Mid,List(JavaScript))
verify number of known languages Programmer(John,Senior,List(Java, Scala, Kotlin))
get name Programmer(John,Senior,List(Java, Scala, Kotlin))
✅ 执行顺序清晰:先对所有元素应用第一个条件,再对结果应用第二个条件。
❌ 性能隐患:总共进行了 5 次谓词判断,其中 Dave 被提前过滤掉,但仍参与了第一次判断。
4. withFilter 方法详解
4.1. 签名说明
withFilter
的方法签名与 filter
类似,但返回的是 WithFilter
类型:
def withFilter(p: A => Boolean): WithFilter[A, Repr]
⚠️ 它并不会立即执行,而是构建一个延迟计算的包装器。
4.2. 执行过程分析
使用 withFilter
进行链式操作:
implicit val counter: AtomicInteger = new AtomicInteger(0)
val desiredProgrammers: WithFilter[Programmer, List[Programmer]] =
programmers
.withFilter(isMidOrSenior)
.withFilter(knowsMoreThan1Language)
counter.get() shouldBe 0
desiredProgrammers.map(getName) shouldBe List("John")
counter.get() shouldBe 5
控制台输出为:
verify level Programmer(Kelly,Mid,List(JavaScript))
verify number of known languages Programmer(Kelly,Mid,List(JavaScript))
verify level Programmer(John,Senior,List(Java, Scala, Kotlin))
verify number of known languages Programmer(John,Senior,List(Java, Scala, Kotlin))
get name Programmer(John,Senior,List(Java, Scala, Kotlin))
verify level Programmer(Dave,Junior,List(C, C++))
✅ 单次遍历完成所有操作:每条记录只遍历一次,两个谓词和映射函数都在同一次迭代中处理。
5. 对比总结
特性 | filter |
withFilter |
---|---|---|
返回类型 | 实际集合 | WithFilter 包装器 |
是否立即执行 | ✅ 是 | ❌ 否 |
多次过滤性能 | ❌ 差(多次遍历) | ✅ 好(一次遍历) |
适用场景 | 单一过滤后直接使用 | 多条件链式处理 |
📌 简单粗暴结论:
- 如果只是简单过滤一次并拿到结果,用
filter
- 如果要进行链式操作(比如多个
filter
+map
),用withFilter
更高效
💡 踩坑提醒:如果你只用了 withFilter
而没有后续调用 map
、flatMap
或 foreach
,那么它根本不会执行任何逻辑!
6. 小结
通过对比可以发现:
filter
是“立即求值”,适合一次性过滤操作;withFilter
是“延迟求值”,更适合链式处理,避免中间集合创建,提升性能。
根据业务需求合理选择两者,能让你的 Scala 代码更优雅也更高效。
完整源码见 GitHub 项目地址