22. Scala与Java集成
本书完成于2021年,因此本章主要介绍Scala 3和Java 11的集成,后者是Oracle目前的长期支持版本。Java版本的要点在于,目前Oracle每年都有两个主要的Java版本发布计划。
一般来说,Scala和Java代码是可以无缝混合的。在大多数情况下,可以创建一个sbt项目,把Scala代码放在 src/main/scala 中,把Java代码放在 src/main/java 中,就可以正常工作了。
本章中的小节涵盖了转换器、特质和接口、异常、数字类型的转换等问题。
在作者与Scala/Java的集成的经验中,遇到的最大问题是它们的集合库之间的差异。然而,总是能够通过Scala的 CollectionConverters 对象来解决这些问题。从Scala 2.13开始,现在有两个 CollectionConverters对象:
Scala代码中使用的扩展方法在 scala.jdk.Collection Converters 中。
Java代码的转换方法在 scala.jdk.javaapi.Collection Converters 中。
同样地,这些转换对象也会处理Scala的 Option 和Java的 Optional 之间的转换:
Scala的扩展方法在 scala.jdk.OptionConverters 中。
Java的转换方法在 scala.jdk.javaapi.OptionConverters 中。
这些转换方法在本章的初始小节中展示。
在转换相关的小节之后,小节 22.5和22.6深入探讨了Scala 特质和Java接口之间的关系。由于Java 8或更高版本中接口的特性,traits和接口比 Scala Cookbook 第一版中更加紧密地结合在一起。
Scala和Java之间的其他方面集成需要使用注解,这些在关于异常( @throws )、varargs参数( @varargs )和序列化( @SerialVersionUID )的小节中都有涉及。
最后,如果对Java很熟悉,但对Scala很陌生,那么有必要提一下,Scala中的所有东西都是一个对象。具体来说就是意味着Scala没有原始的数字数据类型。如图22-1所示,在Scala REPL中输入数字1,然后输入小数点,再按Tab键,REPL就会显示 Int 实例上的所有方法。

图 22-1 在Scala中,所有东西都是一个对象,即使整数也是。
22.1 在Scala中使用Java集合对象
问题
你正在Scala应用程序中使用Java类,而这些类要么返回Java集合,要么在其方法调用中需要Java集合,因此你需要将这些集合与Scala集合结合起来使用。
解决方案
在Scala代码中使用 scala.jdk.CollectionConverters 对象的扩展方法来实现转换。例如,如果在一个名为 JavaCollections 的公有Java类中有一个这样的 getNumbers 的方法:
可以在Scala代码中把这个Java列表转换成Scala Seq,如下:
同样地,如果有一个Java方法,返回一个 Map :
可以像这样将其转换为Scala Map :
同样地,这个例子展示了如何将Java Properties 对象转换为Scala Map 。
println语句会打印出这样的输出:
讨论
本小节展示了如何在Scala中进行集合转换。要在Java代码中进行转换,请看下一个小节。
类型转换
解决方案中的第一个例子展示了如何从 Java.util.List<Integer> 创建一个Scala Seq[Integer] :
假设确认想要一个 Seq[Int] ——而不是 Seq[Integer] ——那么代码中需要添加一个 Integer 到 Int 的转换过程。
这段代码的作用如下:
创建一个 jlist2 其类型为 java.util.List[Integer] 。
使用 asScala 将该代码转换为Scala的 Buffer[Integer] 。
用 map 方法和 integer2Int 函数将 Buffer[Integer] 转换为 Buffer[Int] 。
通过调用 toSeq 创建最终的 Seq[Int] 。
转换方法
像这样的代码之所以能够工作,是因为 CollectionConverters 对象中的转换方法。表22-1列出了使用 asScala 和 asJava 方法所能进行的双向转换:
表22-1,scala.jdk.CollectionConverters 对象所提供的双向转换功能
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
例如,可以使用 asJava 将Scala Buffer 转换为Java List ,也可以使用 asScala 进行相反的转换。
表22-2显示了额外的双向转换。 asScala 支持向Scala的转换,括号中提供的特别命名的扩展方法则可把Scala集合转换为Java集合。
表22-2 额外的双向转换,包括特别命名的方法
scala.collection.Iterable
java.util.Collection (通过 asJavaCollection)
scala.collection.Iterator
java.util.Enumeration (通过 asJavaEnumeration)
scala.collection.mutable.Map
java.util.Dictionary (通过 asJavaDictionary)
表22-3 列出了用 asJava 可以实现的单向转换。
表22-3 CollectionConverters 类提供的从Scala 到 Java的单向转换
scala.collection.Seq
java.util.List
scala.collection.mutable.Seq
java.util.List
scala.collection.Set
java.util.Set
scala.collection.Map
java.util.Map
表22-4 列出了用 asScala 可以实现的单向转换。
表22-4 asScala 提供的单向转换
java.util.Properties
scala.collection.mutable.Map[String,String]
另见
scala.jdk.CollectionConverters ( https://oreil.ly/kzx4f )提供从Java到Scala的转换
scala.javaapi.CollectionConverters( https://oreil.ly/YloeY ) 提供从Scala到Java的转换
scala.jdk.javaapi.StreamConverters ( https://oreil.ly/k4FNr )用于创建可以与Scala一起工作的Java流
22.2 在Java中使用Scala集合
问题
你需要在Java应用程序中访问Scala集合类,并将这些Scala类转换为Java类。
解决方案
在Java代码中,使用Scala的 scala.javaapi.CollectionConverters 对象的方法来实现转换的工作。例如,如果在Scala类中有一个像这样的 List[String] :
可以像这样在Java代码中访问Scala的 List :
关于这段代码,有几点需要注意:
在Java代码中,创建一个 ScalaClass 的实例,就像创建一个Java类的实例一样。
ScalaClass 有一个名为 strings 的字段,但在Java中,必须以方法的形式访问这个字段的,比如说, sc.strings() 。
这里把这段代码写得很长,是为了帮助强调这些要点,也可以像这样一步到位地进行转换:
讨论
本小节展示了如何在Java中进行集合转换。要在Scala代码中进行转换,请参见前面的小节。
在某些情况下,会遇到类型擦除的问题。例如,鉴于Scala类中的这个ints字段:
必须将Scala的Seq 作为Java中的List<Object> 来访问:
当用 scalac 编译Scala类,然后用 javap 反汇编时,就可以看到类型擦除的问题,会看到这样的代码:
如图所示,该类文件只知道有一个 ints() 方法返回 Seq<java.lang.Object> 。
如果读者接触过Scala的源代码,就知道可以先将 Seq[Int] 转换为 Seq[Integer] ,从而使其更容易从Java中访问:
现在可以像这样在一个Java类中以 List<Integer> 的形式访问它:
如果不能修改Scala代码,可能要进行其他与强制类型转换有关的工作来处理 java.util.List<Object>,这取决于实际需求。
另见
scala.jdk.javaapi.CollectionConverters 对象所支持的双向转换与前面的小节中所示的 scala.jdk.CollectionConverters 对象相同。关于这些转换,请参见前面的小节,更多细节请参见这些链接:
scala.jdk.CollectionConverters ( https://oreil.ly/kzx4f )提供从Java到Scala的转换
scala.javaapi.CollectionConverters ( https://oreil.ly/YloeY ) 提供从Scala到Java的转换
scala.jdk.javaapi.StreamConverters ( https://oreil.ly/k4FNr )用于创建可以与Scala一起工作的Java流
22.3 在Scala中使用Java的Optional值
问题
你想在Scala代码中使用Java的Optional值。
解决方案
在写Scala代码时,导入 scala.jdk.OptionConverters 对象,然后使用扩展方法 toScala 将Java Optional 值转换为Scala Option 。
为了证明这一点,在这里会创建一个有两个 Optional<String> 值的Java类,一个包含一个字符串,另一个则为空:
然后在下面的Scala代码中,可以访问这些字段。如果直接访问它们,它们都将是 Optional 值:
但是通过使用 scala.jdk.OptionConverters 方法,可以将其转换成Scala的 Option 值:
这种语法之所以有效,是因为 toScala 被定义为一个扩展方法,所以可以在Optional实例上调用。
数字值
数字值从Java到Scala的转换也很好实现。假设有以下Java代码:
可以在Scala代码中使用如前所述的这些类型为 Optional<Integer> 或 OptionalInt 的字段。
讨论
在整合时如果能修改Java侧源码,也可以使用 scala.jdk.javaapi.OptionConverters 中的转换方法,在Java代码中而不是在Scala代码中把 Optional 值转换成 Option 值。注意这个对象也被命名为 OptionConverters ,但这两个对象是在不同的地方用来转换 Optional 值的。
在Scala代码中使用 scala.jdk.OptionConverters
在Java代码中使用 scala.jdk.javaapi.OptionConverters
在Java中将Optional转换为Option
下面这个例子展示了如何使用 scala.jdk.javaapi.OptionConverters 对象的方法,在Java代码中把 Optional 字段转换为 Option 值。
注意第一个例子使用了 Optional ,第二个例子使用了 OptionalInt ,两者都转换为了Scala Option 。当它们在Java代码中被这样转换后,在Scala代码中就会显示为 Option 值:
另见
用于转换 Optional 和 Option 值的两个Scala对象是:
在Scala代码中使用scala.jdk.OptionConverters ( https://oreil.ly/6Vl9y )
在Java代码中使用scala.jdk.javaapi.OptionConverters ( https://oreil.ly/POlST )
22.4 在Java中使用Scala的Option值
问题
你想在Java代码中使用Scala的 Option 值。
解决方案
可以在Scala代码中或在Java代码中把一个Scala Option 值转换成一个Java Optional 值。这里展示的是在Scala中的解决方案,而在Java中的解决方案则在讨论中展示。
在Scala代码中,在导入 scala.jdk.OptionConverters 后,使用 toJava 扩展方法将Scala Option 转换为 Java Optional 值。
为了证明这一点,这里创建有两个 Option[String] 值的Scala类,一个包含一个字符串,另一个是空的,使用 toJava 将这些 Option[String] 值转换成 java.util.Optional[String] :
然后在Java代码中,直接以 Optional 为类型访问这些字段:
这两个字段可以作为 Optional 值使用,在Java代码中看到的唯一区别是Scala字段(如 scalaStringSome )在Java代码中作为方法出现,而不是作为字段来使用。
含有数字值的 Option
转换被Scala 数字值的 Option 可能要多花点功夫,但它们也还是可以被转换为Java Optional 值。scala.jdk.Option.Converters 对象为这些情况提供了几种方法。例如,给定这样Scala代码,它创建了 Optional 和 OptionalInt 值:
这些字段可以像这样在Java代码中被访问:
这个解决方案的关键是,Java代码中的 Optional 字段需要以下面这些方式之一来声明:
由于类型擦除,试图像这样声明字段类型将导致编译时错误:
讨论
如果在Java这一侧来做转换,而Scala代码只提供给一个 Option 值,可以在Java代码中将其转换成 Optional 值。只要使用 scala.jdk.javaapi.OptionConverters ( https://oreil.ly/POlST )对象中的 toJava* 方法即可。
同样的名字,不同的包 -- todo (鸟图)
请注意,虽然这个对象与解决方案中显示的对象( OptionConverters )有相同的名字,但它们在不同的包中,而且这个对象是要在Java代码中使用。
为了演示在Java中把一个 Option 转换为一个 Optional 值,首先在Scala代码中创建一个 Option[String] :
现在可以在Java代码中使用 OptionConverters 的 toJava 方法将其转换为 Optional<String> :
除了 toJava 之外,可以在Java代码中使用的其他转换方法是:
toJavaOptionalDouble
toJavaOptionalInt
toJavaOptionalLong
另见
用于转换 Optional 和 Option 值的两个Scala对象是:
在Scala代码中使用scala.jdk.OptionConverters ( https://oreil.ly/6Vl9y )
在Java代码中使用scala.jdk.javaapi.OptionConverters ( https://oreil.ly/POlST )
22.5 在Java中使用Scala的特质
问题
你想在Java侧使用Scala中已经实现了的特质。
解决方案
本书是用Java 11测试的,在Java 11中,可以像使用Java接口一样使用Scala 特质,即使该特质已经实现。例如,给定这两个Scala 特质,一个已经实现,一个只有接口:
一个Java类可以同时实现这两个接口,并实现 multiply 方法:
讨论
这个解决方案过去需要将Scala 特质包装在一个类中,以便Java应用程序可以使用它,但现在的解决方案是使用Scala trait,就像它是一个Java接口一样。
22.6 在Scala中使用Java的接口
问题
你想在Scala中实现Java的接口。
解决方案
在Scala应用程序中,使用 extends 关键字和逗号来实现Java的接口,就像实现Scala 特质一样。
例如,给定以下三个Java接口:
可以用平时使用的 extends 关键字来实现特质一样在Scala中创建一个 Dog 类并实现 speak 和 wag 方法:
讨论
请注意,Java Running 接口声明了一个名为 run 的默认方法。如下所示,在Scala中可以很容易使用Java接口中的默认方法。
同样,Java接口中的静态方法在Scala中也可以轻松使用:
22.7 为Scala方法添加异常注解
问题
你想让Java代码知道一个Scala方法可以抛出一个或多个异常,便于用 try/catch 块来处理这些异常。
解决方案
Scala方法中添加 @throws 注解,这样Java侧的使用者就会知道这些方法会抛出哪些异常。
例如,这个Scala的 exceptionThrower 方法被注解为声明它抛出了一个 Exception :
结果是,这里的Java代码无法编译,因为没有处理这个异常:
编译器给出了这样的错误:
这就是这段代码想要的实现的:这个注解告诉Java编译器 exceptionThrower 可以抛出一个异常。现在当写Java代码时,必须用一个 try 代码块来处理这个异常,或者声明Java方法抛出了一个异常:
相反,如果不对Scala的 exceptionThrower 方法进行注解,Java代码就会被成功编译。这可能不是想要的,因为Java代码可能没有考虑到Scala方法抛出的异常。
讨论
要声明一个Scala方法可以抛出多个异常,可以在声明方法之前使用多个 throws 注解:
然后在Java代码中,像往常一样捕获这些异常:
另见
虽然这个小节展示了如何注释抛出异常的方法,以便与Java一起工作,但 "Scala方式 "是方法永远不应该抛出异常。参见10.8小节,"实现函数式错误处理",以了解处理可能的错误的首选方法的细节。
22.8 给方法添加varargs注解让其能够被Java代码使用
问题
你想从Java代码中调用一个带有varargs字段的Scala方法。
解决方案
用 @varargs 注解来标记Scala方法。例如,这个Scala类中的 printAll 方法声明了一个varargs字段—— String*,并且用 @varargs 将其标记:
因为 printAll 是用 @varargs 注解声明的,所以它可以在Java程序中调用并接受可变参数,如本例中所示:
当这段代码运行时,它的输出结果如下:
讨论
如果在 printAll 方法上没有使用 @varargs 注解,所示的Java代码甚至不会通过编译,会出现以下编译器错误:
从Java代码这一侧来看,如果没有 @varargs 注解, printAll 方法是以 scala.collection.immutable.Seq<java.lang.String> 作为其参数。
调用Java的varargs方法
从Scala中调用一个Java的varargs方法,一般都能正常使用。例如,这个Java方法:
可以在下面的Scala代码中调用,并且如预期的那样工作:
22.9 使用@SerialVersionUID和其他注解
问题
你想将一个Scala类设置为可序列化的,并设置serialVersionUID。
解决方案
使用Scala @SerialVersionUID 注解,同时也让类扩展 Serializable 类型:
对于上面这个类写一个 deepClone 这样的函数:
可以这样复制一个 Sheep :
复制 Sheep 如期成功,但是由于 greet 字段有 @transient 注解,在被复制出来的对象中它是 null 。讨论中展示了这个问题的解决方案。
@transient -- TODO (小松许图)
@transient 注解意味着这个字段不应该被序列化。
刚刚的代码可以执行成功,但是如果尝试对普通的Cat类执行相同的操作,在调用deepClone方法时,将抛出一个异常:
这段代码失败是因为它没有使用 @SerialVersionUID 和 Serializable 。
讨论
解决方案中展示的 Sheep 类如期工作,但如上所示,在序列化/反序列化过程后, greet 字段最终为空。解决这个问题的方法是让 greet 字段成为一个 transient 的 lazy val 。
通过同时使用 @transient 和 lazy val :
像前面一样,greet 字段不会被序列化
在 deepClone 序列化/反序列化过程之后,当访问 d2.greet 时,greet 字段会被重新计算。
此时,greet 不会为空。
其他注解
表22-5展示了其他Scala注解及其在Java中的对应关系。
表22-5。Scala注解以及其在Java中的对应关系
scala.deprecated
用于将一个成员标记为废弃(如下例所示)。
scala.serializable
java.io.Serializable (如本小节所示)。
scala.SerialVersionUID
serialVersionUID 字段(如本小节所示)。
scala.throws
throws 关键字(如22.7小节所示)。
scala.transient
transient 关键字(如本小节所示)。
scala.annotation.varargs
在方法、函数或构造函数中的一个字段上使用,它使编译器生成一个Java varargs风格的参数(如22.8小节所示)。
@threadUnsafe
当在一个 lazy val 字段上使用时,该字段将被更快地初始化,但其方式不是线程安全的。
@targetName
为一个成员定义一个替代名称。定义的方法名在Scala中使用,而* targetName* 则从其他语言如Java中访问。
下面的例子列举了这些注解中的几个:
下面这个例子展示了使用 @targetName 注解所需的Scala和Java代码:
这是一个不错的新方法,可以在Java代码中提供容易使用的名字。
另见
22.7小节展示了 @throws 注解。
22.8小节展示了 @varargs 注解。
Scala 3 @targetName文档( https://oreil.ly/5jRfa )提供了更多关于其使用的细节。
最后更新于
这有帮助吗?