1. 概述

Scala 是 JVM 生态系统的一部分,因此在大多数非全新项目中,我们都会与 Java 库打交道。在这些交互中,我们可能需要将 Java 集合作为参数传入,或者作为返回值接收,甚至两者兼而有之。

虽然我们可以在 Scala 中直接使用 Java 集合,但这意味着我们将无法使用 Scala 的诸多特性,比如函数式接口(如 mapforeach),更严重的是,我们将失去 Scala 中强大的 monadic for 语法糖。因此,我们更希望像操作 Scala 集合那样去操作 Java 集合

在本教程中,我们将学习如何在 Java 和 Scala 集合之间进行相互转换,并了解如何使用 Scala 的惯用法来完成这些转换。

我们会借助 Scala 标准库提供的转换方法,并探讨如何将这些转换引入作用域。

2. 集合转换的历史简述

Scala 从一开始就在努力与 Java 保持兼容,因此它提供了用于 Java 到 ScalaScala 到 Java 的包装器。

但我们不推荐使用这些包装器及其伴生对象,因为它们已经被标记为废弃(deprecated)。

2.1. 当前实现概述

目前的集合转换机制依赖于 隐式转换(implicit conversions)

在 Scala 2.12 中,我们可以通过混入 DecorateAsScalaDecorateAsJava 特质(traits) 来限制隐式转换的数量和作用域:

class JavaToScalaConversionsTest extends FlatSpec with Matchers with DecorateAsJava {
  // ...
}

或者,通过导入 JavaConverters 对象(object),我们可以使用局部导入来控制转换的可用性,但这种方式会导入所有的隐式转换方法:

class CollectionConversionsTest extends FlatSpec with Matchers { 

  import scala.collection.JavaConverters._

  // ...
}

从 Scala 2.13 开始,上述方法已被移除。现在推荐使用 scala.jdk 包中的隐式转换,只需导入:

import scala.jdk.CollectionConverters._

我们可以通过多态扩展方法 asJavaasScala 来完成最常见的集合转换。具体来说,可以实现如下转换:

Scala Java
scala.collection.Iterable java.lang.Iterable
scala.collection.Iterator java.util.Iterator
scala.collection.mutable.Buffer java.util.List
scala.collection.mutable.Set java.util.Set
scala.collection.mutable.Map java.util.Map
scala.collection.concurrent.Map java.util.concurrent.ConcurrentMap

还有一些其他转换,其中部分是单向的。完整的转换列表可以参考 Scala 官方文档

这套实现非常高效,使用了装饰器模式避免集合的拷贝。更进一步地,双重装饰器会相互抵消。因此,如果你在调用 asScala 后再调用 asJava,或者反之,你会得到原始对象的实例:

class CollectionConversionsTest extends FlatSpec with Matchers {
  "Round trip conversions from Java to Scala" should "not have any overhead" in {
    val javaList = new ArrayList[Int]
    javaList.add(1)
    javaList.add(2)
    javaList.add(3)
    assert(javaList eq javaList.asScala.asJava)
  }

  "Round trip conversions from Scala to Java" should "not have any overhead" in {
    val scalaSeq = Seq(1, 2, 3).toIterator
    assert(scalaSeq eq scalaSeq.asJava.asScala)
  }
}

⚠️ 在可变集合中,两边的副作用是可见的

但是,如果将不可变的 Scala 集合转换为可变接口,则会抛出 UnsupportedOperationException

  "Conversions to mutable collections" should "throw an unsupported operation exception" in {
    val scalaSeq = Seq(1, 2, 3)
    val javaList = scalaSeq.asJava
    assertThrows[UnsupportedOperationException](javaList.add(4))
  }

3. 实战转换示例

3.1. 从 Java 到 Scala 的函数式处理

最常见的情况是从 Java 到 Scala 的转换。当我们与 Java 库交互时,可能会收到一个 Java 集合作为结果,经过处理后再传递给另一个 Java 方法。这是标准双向转换大显身手的场景。

来看一个例子:

我们收到一个 Java Iterator<Integer>,希望对其元素加一后再传回:

class JavaToScalaConversionsTest extends FlatSpec with Matchers {
  import scala.jdk.CollectionConverters._
  "Standard conversions" should "convert from Java Iterators and back" in {
    val api = new JavaApi
    val javaList = api.getOneToFive
    val incremented = javaList.asScala.map(_ + 1).map(Integer.valueOf(_))

    assert(api.iteratorToString(incremented.asJava) == "[2, 3, 4, 5, 6]")
  }
}

⚠️ 上面的例子展示了在处理 Java 的原始类型包装类时,我们必须格外小心。例如,加一后 Scala 集合的类型会变为 Iterator[Int],如果我们再调用 asJava,得到的将是 java.util.Iterator[Int]

结论:Scala 集合转换只改变集合结构,不改变元素内容

3.2. 从 Java 到 Scala 的命令式处理

我们收到一个 Java 可变 List,对其进行处理后再传回:

  "Standard conversions" should "support Java's lists" in {
    val api = new JavaApi
    val javaList = api.getNames

    val scalaList = for (name <- javaList.asScala) yield s"Hello ${name}"
    val withExclamation = api.addExclamation(scalaList.asJava)

    assert(withExclamation.toString == "[Hello Oscar!, Hello Helga!, Hello Faust!]")
    assert(!(withExclamation eq javaList))
  }

上面的代码展示了如何在 Scala 中以惯用方式处理 Java 返回的集合。但最后一行断言说明我们创建了一个新集合,这是正常的,因为函数式编程更倾向于不可变性,编译器也会优化掉大部分因创建对象带来的性能损耗

但如果你遇到必须避免创建新实例的场景,也可以使用命令式风格:

  "Standard conversions" should "support Java's mutable lists" in {
    val api = new JavaApi
    val javaList = api.getNames

    val scalaList = javaList.asScala
    for (ix <- 0 until scalaList.size) {
      scalaList(ix) = s"Hi ${scalaList(ix)}"
    }
    val withExclamation = api.addExclamation(scalaList.asJava)

    assert(withExclamation.toString == "[Hi Oscar!, Hi Helga!, Hi Faust!]")
    assert(withExclamation eq javaList)
  }

✅ 上面的代码展示了如何操作可变集合,且没有创建新实例。但通常来说,代码清晰度和正确性比纯粹的性能更重要。只有在性能敏感的部分才考虑使用命令式风格。

其他 Java 到 Scala 的转换类似,只有一个特殊情况需要注意。

3.3. Java Properties 的特殊处理

有时我们需要在 Scala 中处理 Java 的 Properties 对象。**我们可以将其转换为 Scala 的 Map**,但注意这是一个单向转换,因为没有标准方法可以将 Scala 的 Map 转换为 Java 的 Properties

  "Java properties" should "be converted to a Scala Map" in {
    val api = new JavaApi
    val javaProps = api.getConfig

    assert(javaProps.asScala == Map("name" -> "Oscar", "level" -> "hard"))
  }

3.4. 从 Scala 到 Java

由于大多数转换是双向的,我们在前面已经用到了 asScala 方法。标准库也提供了 asJavaCollection 方法用于转换:

class ScalaToJavaConversionsTest extends FlatSpec with Matchers
  with DecorateAsJava {

  "A Scala Iterable" should "be passable as a parameter expecting a Java Collection" in {
    val api = new JavaApi

    val scalaIterable = Seq(1, 2, 3)
    assert(api.collectionSize(scalaIterable.asJavaCollection) == "Collection of size: 3")
  }
}

此外,asJava 方法支持从 Scala 的 Seq、可变 Seq 转换为 Java 的 List,从 Scala 的 Set 转换为 Java 的 Set,以及从 Scala 不可变 Map 转换为 Java 的 Map

4. 避免使用隐式转换

有些组织对隐式转换持谨慎态度,因此会禁止使用。

✅ 幸运的是,我们可以通过显式调用转换方法来实现相同的效果。这些方法都以 as 为前缀,在 IDE 中非常容易发现:

scala java conversion

5. 总结

在本教程中,我们探讨了 Scala 和 Java 集合之间的标准转换是如何提升与 Java 库互操作性的

我们还了解了在哪些情况下转换可以最小化性能开销,并学会了在必要时使用命令式风格避免创建新集合实例。此外,我们也注意到 Scala 2.12 和 2.13 在集合转换方面的一些变化


原始标题:Converting Java Collections to Scala Collections

» 下一篇: Scaladoc指南