Scala 编译到同一套字节码,调用 Java 类通常零成本直接写;真正的摩擦在集合语义、函数类型与异常体系三处。本节给出双向调用的完整清单与缓冲层实践。
import java.util.{HashMap => JHashMap} import java.time.LocalDateTime val now = LocalDateTime.now() // Java 类直接调用 val rnd = new java.util.Random() rnd.nextInt(100) // Java 的函数式接口(SAM)可用 Scala 函数字面量 val executor = new java.util.concurrent.Executor: def execute(r: Runnable): Unit = r.run()
反方向也成立:Scala 方法编译后就是普通字节码方法,Java 能直接调用;Scala 的函数值编译为 Function1 接口实例,Java 侧调用其 apply。企业里大量 Scala 服务与 Java 框架(Spring 全家桶除外)就是这么共存的。
import scala.jdk.CollectionConverters.* val jList = new java.util.ArrayList[String]() jList.add("a") val sList: List[String] = jList.asScala.toList // 转 Scala,转回去 .asJava
asScala/asJava 是 O(1) 的视图包装(大多数情况),不是拷贝。边界上立刻转成所在语言的惯用集合,别让 Java 集合渗进业务逻辑深处——两种集合的 API 差异会在每一次调用时收税。
Java 引用可能为 null,Scala 类型系统不追踪它。被 Java 返回值击穿是 Scala 项目最常见的 NPE 来源:
val raw: String = legacyClient.getUserName(id) // 可能返回 null val safe: Option[String] = Option(raw) // null -> None
纪律:所有 Java 边界的返回值立刻 Option 包一层,此后系统内部恢复"类型即承诺"的秩序。Scala 3 还提供了显式可空类型工具,但 Option 包装是零依赖的最低配防线。
Scala 没有 checked exception,Java 的受检异常在 Scala 里就是普通异常,编译器不强迫处理:
try val text = java.nio.file.Files.readString(path) Right(text) catch case e: java.io.IOException => Left(e.getMessage)
推荐在边界处统一转成 Either[String, T] 或自定义错误类型——既接住 Java 异常,又不让 try-catch 在业务代码里蔓延。

背景:团队 Java 侧要复用你写的温度模块,默认导出的函数带 SAM 转换与默认参数,Java 侧读不懂。操作分三步:
package temp object TempApi: def toF(c: Double): Double = c * 9 / 5 + 32 // 直接可调 @annotation.varargs // 第一步:可变参数转 Java 数组风格 def average(xs: Double*): Double = xs.sum / xs.size private[temp] def helper(x: Int): Int = x * 2 // 不想暴露的收进私有
Java 侧对应写法:
import temp.TempApi; double f = TempApi.toF(36.6); double avg = TempApi.average(1.0, 2.0, 3.0); // varargs 生效
结果:Java 调用方零感知差异。解读:互操作的坑集中在四处——默认参数在 Java 里看不到(要显式传全)、Option 要转 java.util.Optional、集合要走 CollectionConverters、特质要靠 abstract class 版本。变式:给样例类加 @JavaDefaultValue 或提供重载工厂,是大型混编项目的常规补丁。
| Scala | Java | 转换方向 |
|---|---|---|
| List / Seq | java.util.List | asJava / asScala |
| Map | java.util.Map | 同上 |
| Option | Optional | toJava / toScala |
| Try | CompletableFuture | 手工包一层 |
方向感比背 API 重要:边界处转换、内部各用各的,转换只发生在模块接口那一行。
第一个,NoSuchMethodError 出现在升级 Scala 版本后:Scala 字节码里的模块类名带 $ 后缀且与版本相关,混编项目必须让 Scala 侧先行发布、Java 侧再升级依赖。第二个,NullPointerException 出现在特质方法带默认参数时:Java 侧调不到默认参数合成的方法,需提供显式重载。第三个,集合转换后 ConcurrentModificationException:CollectionConverters 的 asJava 是视图不是拷贝,跨线程传递要 toList 落地。三个坑的共性解法都是"在边界收紧",边界越薄,坑越少。