Scala compiles to JVM bytecode and shares a runtime with Java. Calling Java from Scala is effortless. Exposing Scala to Java requires more thought.
In this chapter:
- Calling Java from Scala
- Java collections in Scala
- Calling Scala from Java
- SAM types and Java functional interfaces
- Annotations and
@varargs,@BeanProperty - Exception handling across the boundary
Largely seamless. Just import and use:
import java.util.{ArrayList, HashMap}
import java.time.LocalDate
val list = new ArrayList[Integer]()
list.add(1); list.add(2); list.add(3)
val today = LocalDate.now()
val parsed = LocalDate.parse("2026-05-03")A few translations to keep in mind:
- Java primitives (
int,boolean) ≡ Scala value types (Int,Boolean) — they auto-box where needed. - Java's
nullis allowed for anyAnyRef. (UseOption(value)to wrap safely.) - Java's checked exceptions don't exist in Scala — they compile, but the compiler doesn't enforce
throwsdeclarations.
Scala uses operators where Java uses methods:
val sum: Int = Integer.valueOf(2) + Integer.valueOf(3) // unboxesDon't use Java's ArrayList/HashMap from Scala code if you don't have to — Scala's collections are nicer. To bridge:
import scala.jdk.CollectionConverters._
// Java -> Scala
val javaList: java.util.List[Int] = ???
val scalaList: List[Int] = javaList.asScala.toList
val javaMap: java.util.Map[String, Int] = ???
val scalaMap: Map[String, Int] = javaMap.asScala.toMap
// Scala -> Java
val scalaList: List[Int] = List(1, 2, 3)
val javaList: java.util.List[Int] = scalaList.asJava
val scalaMap: Map[String, Int] = Map("a" -> 1)
val javaMap: java.util.Map[String, Int] = scalaMap.asJavaThe .asScala / .asJava methods are wrappers — they don't copy. Mutating one mutates the other (where allowed). For a deep copy, follow .asScala.toList / .toMap etc.
Java 8+ has Optional[T]. Scala has Option[T]. Convert:
import scala.jdk.OptionConverters._
val javaOpt: java.util.Optional[String] = ???
val scalaOpt: Option[String] = javaOpt.toScala
val s: Option[Int] = Some(42)
val j: java.util.Optional[Int] = s.toJavaimport scala.jdk.FutureConverters._
val cf: java.util.concurrent.CompletableFuture[String] = ???
val sf: scala.concurrent.Future[String] = cf.asScala
val sf2: scala.concurrent.Future[Int] = ???
val cf2 = sf2.asJavaYou can, but it requires understanding what Scala generates.
// Scala
object Greeter {
def hello(name: String): String = s"hello $name"
}// Java — two ways
String s1 = Greeter$.MODULE$.hello("Bhaskar");
String s2 = Greeter.hello("Bhaskar"); // static forwarderThe compiler synthesizes a static forwarder on a Java-style class for each method on the object — usable directly from Java.
trait Animal {
def speak(): String = "...silence..."
}
class Dog extends Animal {
override def speak(): String = "woof"
}Java sees Animal as an interface with a default method speak(). Dog is a regular class implementing it.
case class Person(name: String, age: Int)Person p = Person$.MODULE$.apply("Bhaskar", 30); // factory
// or
Person p = new Person("Bhaskar", 30); // constructor works tooA val in Scala compiles to a private field plus a getter. Java callers should use the getter:
class Foo {
val name: String = "X"
}Foo f = new Foo();
String s = f.name(); // call the gettervar adds a setter name_$eq(value). Use @BeanProperty if you want JavaBean-style getName / setName (next section).
Java 8 introduced functional interfaces (single abstract method). Scala 2.12+ supports passing a Scala lambda directly as a SAM:
// Java
interface Validator {
boolean validate(String s);
}
// Scala
val v: Validator = (s: String) => s.length > 0 // lambda becomes a Validator
v.validate("hi") // trueThis works for Runnable, Consumer<T>, Function<T, R>, all of java.util.function.*, and your own SAM interfaces.
Two compiler annotations make Scala APIs friendlier from Java.
A Scala method with * (varargs) compiles to taking a Seq parameter. To also expose Java-style varargs, add @varargs:
import scala.annotation.varargs
class Logger {
@varargs def log(parts: String*): Unit = parts.foreach(println)
}Now Java can call logger.log("a", "b", "c") with native varargs syntax.
Generates JavaBean-style getX / setX accessors:
import scala.beans.BeanProperty
class User(@BeanProperty var name: String, @BeanProperty var age: Int)User u = new User("X", 30);
u.getName(); // "X"
u.setName("Y");Useful for serialization libraries (Jackson, etc.) that expect JavaBean conventions.
If you want Scala to declare a throws clause that Java sees as checked:
import scala.annotation.throws
class IO {
@throws(classOf[java.io.IOException])
def read(): String = ???
}Java callers will be required to catch or declare IOException.
- Scala doesn't enforce checked exceptions, but the JVM still throws them.
- Java catches
Throwable,RuntimeException, etc., uniformly across both languages. - Be aware: a checked exception thrown from Scala that crosses to Java without a declared throws clause is technically a JVM contract violation, but in practice it works.
For pure FP code, prefer typed errors (Either, Try) over throws. At the Java boundary, convert as needed:
// expose to Java in a friendly way
class JavaFriendlyApi(svc: ScalaService) {
@throws(classOf[Exception])
def doWork(input: String): String =
svc.run(input) match {
case Right(v) => v
case Left(e) => throw new Exception(e)
}
}- Calling Java from Scala is effortless; just import and use.
scala.jdk.CollectionConverters.*for.asScala/.asJavabridges.OptionConvertersandFutureConvertersfor the modern Java types.- Java sees Scala objects via
MODULE$(or static forwarders) and traits as Java interfaces. - SAM types let Scala lambdas pass as Java functional interfaces.
@varargsand@BeanPropertymake Scala APIs nicer for Java callers.- Be deliberate about exceptions at the boundary.
The next chapter (Chapter 25 — Best Practices) is opinionated guidance for writing good Scala.
← Previous: Chapter 23 — Testing | Back to README | Next: Chapter 25 — Best Practices →