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 }
  • asScala and asJava produce views, not copies. Mutating the Scala view changes the Java collection.
  • A Java null is not a Scala Option. Wrap every value coming back from Java that may be null in Option(...) at the boundary.
  • Optional has no null-safety by itself: Optional.of(null) throws, while ofNullable(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
ScalaJava seesNote
object FooA class with a MODULE$ fieldStatic forwarders make members callable as Foo.bar()
case classA normal class, no pattern matchingUse the accessor methods, not the extractor
valA method, not a fieldUse @BeanProperty for a real getter
def f(xs: Int*)An array parameterUse @varargs for a Java varargs signature
Either, OptionOpaque generic classesConvert 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.
  • finally runs on both the normal and the exceptional path, which is why resource cleanup belongs there or in Using.
  • A Try at the boundary stops Java's checked-exception style from spreading into your pure domain code.
  • When a trace shows a long chain of flatMap frames, 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 -Xlint and 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.

Concurrency: Futures, actors and effect systems Traits, classes and object-oriented Scala

Last refreshed 2026-09-18.