Java interoperability

Scala · Advanced · 12 min

The whole JVM ecosystem is available to Scala, but the seams need care.

Scala compiles to JVM bytecode, so any Java library is usable directly: import java.time.LocalDate and call methods as usual. Java classes are instantiated without new in Scala 3 too, Java static methods are called on the class name, and Java getters work with or without parentheses.

The seams to watch:

  • Collections: Java and Scala collections are different types. import scala.jdk.CollectionConverters.* adds .asScala and .asJava, which wrap without copying.
  • Null: Java methods may return null. Wrap results with Option(javaCall()). (The -Yexplicit-nulls compiler flag makes the type system track nullable Java types.)
  • Functional interfaces: Scala lambdas convert automatically to Java SAM types like Runnable, Comparator or java.util.function.Function. scala.jdk.FunctionConverters and scala.jdk.FutureConverters cover other conversions, and scala.jdk.OptionConverters handles java.util.Optional.
  • Checked exceptions don't exist in Scala; use @throws[IOException] to declare them for Java callers.
  • Varargs: annotate with @varargs to make a Scala varargs method callable from Java naturally.

Calling Scala from Java is possible but less pleasant: object members are reachable through static forwarders, default parameters aren't visible, and Scala collections look odd. For libraries consumed from Java, expose a thin Java-friendly facade.

Example

import java.time.{LocalDate, DayOfWeek}
import java.util.ArrayList
import scala.jdk.CollectionConverters.*
import scala.jdk.OptionConverters.*

val d = LocalDate.of(2026, 10, 17)
println(d.getDayOfWeek)                          // SATURDAY
println(d.plusDays(2).getDayOfWeek == DayOfWeek.MONDAY)  // true

val jl = ArrayList[String]()
jl.add("pear"); jl.add("fig")
val sl: List[String] = jl.asScala.toList
println(sl.map(_.toUpperCase))                   // List(PEAR, FIG)

jl.sort((a: String, b: String) => a.length - b.length)  // lambda -> Comparator
println(jl)                                      // [fig, pear]

val props = System.getProperties
println(Option(props.getProperty("no.such.key")).getOrElse("default"))  // default
println(java.util.Optional.of(5).toScala)        // Some(5)
println(sl.asJava.isInstanceOf[java.util.List[?]])  // true (a wrapper, no copy)

Remember

  • Java classes are used directly; static methods via the class name
  • scala.jdk.CollectionConverters adds .asScala/.asJava wrappers
  • Wrap possibly-null Java results in Option(...)
  • Lambdas convert to Java functional interfaces automatically

Watch out for

  • Passing a Scala List to a Java API expecting java.util.List without .asJava.

Check yourself

  1. How do you safely handle a Java method that may return null?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scala-lang.org/scala3/book/interacting-with-java.html