Java interop and the JVM ecosystem
Call Java from Scala and back, convert collections, handle nulls and exceptions at the boundary, and read a stack trace.
Calling Java from Scala
import java.time.{Duration, Instant}
import java.util.concurrent.ConcurrentHashMap
import scala.jdk.CollectionConverters.*
// Java APIs are used directly: no wrapper is needed for a class
val started: Instant = Instant.now()
val elapsed: Duration = Duration.between(started, Instant.now())
// Java collections convert with the explicit converters
val jMap = new ConcurrentHashMap[String, Integer]()
jMap.put("a", 1)
val sMap: scala.collection.mutable.Map[String, Integer] = jMap.asScala
sMap("b") = 2
val jList: java.util.List[String] = scala.collection.immutable.List("x", "y").asJava
val back: scala.collection.immutable.Seq[String] = jList.asScala.toSeq
// an Optional from Java becomes an Option in Scala
val opt: java.util.Optional[String] = java.util.Optional.of("value")
val scalaOpt: Option[String] = opt.toScala
// a Java functional interface takes a lambda directly
val runnable: Runnable = () => println("from a lambda")
val comparator = new java.util.Comparator[Int] { def compare(a: Int, b: Int) = a - b }asScalaandasJavaproduce views, not copies. Mutating the Scala view changes the Java collection.- A Java
nullis not a ScalaOption. Wrap every value coming back from Java that may be null inOption(...)at the boundary. Optionalhas nonull-safety by itself:Optional.of(null)throws, whileofNullable(null)returns empty.- Java's checked exceptions do not exist in Scala. A Scala method may throw anything, and calling a Scala method from Java means the compiler will not force you to handle it.
Annotations for Java callers
import scala.annotation.static
import scala.beans.BeanProperty
// a static method, so Java can call it without an instance
object MathUtils:
@static def double(n: Int): Int = n * 2
// a JavaBean-style accessor for frameworks that require one
final class Person(@BeanProperty val name: String, @BeanProperty var age: Int)
// static forwarders: the companion object's methods appear as statics on the class
final case class Point(x: Int, y: Int)
object Point:
val Origin: Point = Point(0, 0)
// Java sees: Point.Origin and Point.apply(...) as static members
// varargs must be annotated to become a Java varargs parameter
def sum(values: Int*): Int = values.sum
// an overloaded method with a default argument is awkward from Java:
// Java sees every overload, so prefer explicit overloads at a Java boundary| Scala | Java sees | Note |
|---|---|---|
object Foo | A class with a MODULE$ field | Static forwarders make members callable as Foo.bar() |
case class | A normal class, no pattern matching | Use the accessor methods, not the extractor |
val | A method, not a field | Use @BeanProperty for a real getter |
def f(xs: Int*) | An array parameter | Use @varargs for a Java varargs signature |
Either, Option | Opaque generic classes | Convert to Java types at the boundary |
If a Java framework reads your Scala classes through reflection, give it what it expects: @BeanProperty for getters and setters, real methods instead of default arguments, and a plain class rather than a case class where the framework needs a no-argument constructor.
Exceptions and stack traces
import scala.util.{Try, Using}
// Using closes the resource even when the body throws
def readHead(path: String): Try[String] =
Using(scala.io.Source.fromFile(path)) { src =>
src.getLines().nextOption().getOrElse("")
}
// a Java exception crossing into Scala: catch it precisely
def parseOrZero(s: String): Int =
try Integer.parseInt(s)
catch case _: NumberFormatException => 0
finally ()
// converting a throwing call to a value, once, at the boundary
val attempt: Try[Int] = Try(Integer.parseInt("12x"))
// Java's stack traces show a Scala method as ClassName.methodName;
// a lambda or a for-comprehension body appears as an anonymous class
// with a name like App$$anonfun$1 or an invokedynamic frame.
// Read the whole trace: the useful frame is rarely the first line.finallyruns on both the normal and the exceptional path, which is why resource cleanup belongs there or inUsing.- A
Tryat the boundary stops Java's checked-exception style from spreading into your pure domain code. - When a trace shows a long chain of
flatMapframes, the real failure is at the innermost frame with your own package name; search the trace for it rather than reading from the top. - Enable
-Xlintand log the full cause chain. Losing the cause by catching and throwing a new exception without it is the most common way to make a production incident unsolvable.
⚠️
Java serialization is not a safe or stable interchange format. It executes code during deserialization, breaks whenever a class changes, and is a known attack surface. Use JSON, Avro, Protobuf or any explicit format for anything that leaves the JVM.
FAQ
Do I pay for converting collections?
asScala and asJava wrap in a view, so the conversion is cheap; the cost is that every access goes through the adapter. Convert once, outside a hot loop.How do I call a Scala method from Java?
Call the static forwarder on the class if the method is on an
object. For a case class, call the accessors. For an Option, convert to Optional or a nullable value first.Related
Concurrency: Futures, actors and effect systems Traits, classes and object-oriented Scala
Last refreshed 2026-09-18.