1. 概述
Scala 是 JVM 生态系统的一部分,因此在大多数非全新项目中,我们都会与 Java 库打交道。在这些交互中,我们可能需要将 Java 集合作为参数传入,或者作为返回值接收,甚至两者兼而有之。
虽然我们可以在 Scala 中直接使用 Java 集合,但这意味着我们将无法使用 Scala 的诸多特性,比如函数式接口(如 map
和 foreach
),更严重的是,我们将失去 Scala 中强大的 monadic for
语法糖。因此,我们更希望像操作 Scala 集合那样去操作 Java 集合。
在本教程中,我们将学习如何在 Java 和 Scala 集合之间进行相互转换,并了解如何使用 Scala 的惯用法来完成这些转换。
我们会借助 Scala 标准库提供的转换方法,并探讨如何将这些转换引入作用域。
2. 集合转换的历史简述
Scala 从一开始就在努力与 Java 保持兼容,因此它提供了用于 Java 到 Scala 和 Scala 到 Java 的包装器。
但我们不推荐使用这些包装器及其伴生对象,因为它们已经被标记为废弃(deprecated)。
2.1. 当前实现概述
目前的集合转换机制依赖于 隐式转换(implicit conversions)。
在 Scala 2.12 中,我们可以通过混入 DecorateAsScala
和 DecorateAsJava
特质(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._
我们可以通过多态扩展方法 asJava
或 asScala
来完成最常见的集合转换。具体来说,可以实现如下转换:
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 中非常容易发现:
5. 总结
在本教程中,我们探讨了 Scala 和 Java 集合之间的标准转换是如何提升与 Java 库互操作性的。
我们还了解了在哪些情况下转换可以最小化性能开销,并学会了在必要时使用命令式风格避免创建新集合实例。此外,我们也注意到 Scala 2.12 和 2.13 在集合转换方面的一些变化。