1. 概述

在 Scala 中,当我们需要从集合中筛选出满足条件的元素时,通常会使用 filterwithFilter 方法。虽然这两个方法看起来作用类似,但它们的行为和性能表现却有显著差异。

本文将带你深入理解两者的区别,并分析它们在实际使用中的性能影响。

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 而没有后续调用 mapflatMapforeach,那么它根本不会执行任何逻辑!

6. 小结

通过对比可以发现:

  • filter 是“立即求值”,适合一次性过滤操作;
  • withFilter 是“延迟求值”,更适合链式处理,避免中间集合创建,提升性能。

根据业务需求合理选择两者,能让你的 Scala 代码更优雅也更高效。

完整源码见 GitHub 项目地址


原始标题:Filter vs WithFilter