Scala · lesson 11 of 11
Calling Java from Scala, and Scala from Java
Convert java.util.Optional and java.util.List at the boundary, then read the javap output that shows why Java sees two .class files for one Scala object.
By SkillByExample · Updated
Scala and Java share one bytecode format, so nothing stops a Scala program from calling a
java.util.List, or a Java program from calling a Scala object. What actually needs
attention is the standard library: Java and Scala each have their own Optional/Option
and collection types, and Scala’s object compiles to something Java code was never
designed to call directly.
Java’s Optional and List, called from Scala
// Inventory.java
import java.util.Optional;
import java.util.List;
import java.util.ArrayList;
public class Inventory {
public static Optional<String> findSku(String name) {
if (name.equals("widget")) {
return Optional.of("SKU-001");
}
return Optional.empty();
}
public static List<String> listNames() {
List<String> names = new ArrayList<>();
names.add("widget");
names.add("gadget");
return names;
}
}
// CallJava.scala
//> using scala 3.6.2
import scala.jdk.OptionConverters.*
import scala.jdk.CollectionConverters.*
@main def callJavaRun(): Unit =
val found: Option[String] = Inventory.findSku("widget").toScala
val missing: Option[String] = Inventory.findSku("sprocket").toScala
println(s"found: $found")
println(s"missing: $missing")
val javaNames = Inventory.listNames()
val scalaNames: List[String] = javaNames.asScala.toList
println(s"scalaNames: $scalaNames")
println(s"scalaNames class: ${scalaNames.getClass}")
javac Inventory.java
scala run CallJava.scala --classpath . --main-class callJavaRun
found: Some(SKU-001)
missing: None
scalaNames: List(widget, gadget)
scalaNames class: class scala.collection.immutable.$colon$colon
Inventory.findSku(...) returns a java.util.Optional<String>, which Scala can call
directly but cannot pattern-match like an Option. .toScala (from
scala.jdk.OptionConverters) converts it. .asScala does the same job for the List —
without it, javaNames would stay a java.util.ArrayList with none of Scala’s collection
methods. The last line is the real surprise: printing the class of a converted Scala List
shows scala.collection.immutable.$colon$colon, not List — List is a trait; :: (the
cons cell, name-mangled to $colon$colon in bytecode) is the concrete class every non-empty
List actually is.
A Scala object, compiled and inspected
// Pricing.scala
object Pricing:
def discount(price: Double, pct: Int): Double =
price * (1 - pct / 100.0)
val defaultCurrency: String = "USD"
scalac Pricing.scala
ls *.class
Pricing$.class
Pricing.class
One object definition, two .class files. javap shows why:
javap -p Pricing.class
Compiled from "Pricing.scala"
public final class Pricing {
public static java.lang.String defaultCurrency();
public static double discount(double, int);
}
javap -p 'Pricing$.class'
Compiled from "Pricing.scala"
public final class Pricing$ implements java.io.Serializable {
private static final java.lang.String defaultCurrency;
public static final Pricing$ MODULE$;
private Pricing$();
public static {};
private java.lang.Object writeReplace();
public double discount(double, int);
public java.lang.String defaultCurrency();
}
Pricing$.class is the real singleton: a private constructor, a MODULE$ static field
holding the one instance, and the actual method bodies as instance methods on that
instance. Pricing.class is a synthetic static forwarder — plain static methods that
exist purely so Java code can write Pricing.discount(...) instead of the awkward
Pricing$.MODULE$.discount(...). The Scala compiler generates the forwarder automatically;
nothing in the source above asks for it.
Calling the object from Java
// CallScala.java
public class CallScala {
public static void main(String[] args) {
double price = Pricing.discount(100.0, 20);
System.out.println("discounted: " + price);
System.out.println("currency: " + Pricing.defaultCurrency());
}
}
Put Pricing.scala and CallScala.java in the same directory and let scala run compile
both languages together — it resolves the mixed sources on its own, no manual classpath
needed:
scala run . --main-class CallScala
discounted: 80.0
currency: USD
Java calls Pricing.discount(...) through the static forwarder without ever referencing
Pricing$ or MODULE$ — the two-file split is invisible from the call site. It only
matters when something goes looking for it: a reflection-based framework, a serialization
library, or a stack trace that names Pricing$ instead of Pricing will expose the
mechanism this lesson just showed you directly.
Practice
1. Convert a Scala Option to a java.util.Optional and pass it to a Java method.
import scala.jdk.OptionConverters.*
val scalaOpt: Option[Int] = Some(42)
val javaOpt: java.util.Optional[Int] = scalaOpt.toJava
println(javaOpt.get())
42
.toJava is the mirror of .toScala — the conversion works in both directions from the
same scala.jdk.OptionConverters import.
2. Run javap -p on a Scala case class and find the auto-generated methods a Java caller gets for free.
scalac Point.scala # case class Point(x: Int, y: Int)
javap -p Point.class
public class Point implements scala.Product,java.io.Serializable {
private final int x;
private final int y;
public static Point apply(int, int);
public static Point fromProduct(scala.Product);
public static Point unapply(Point);
public Point(int, int);
public scala.collection.Iterator productIterator();
public scala.collection.Iterator productElementNames();
public int hashCode();
public boolean equals(java.lang.Object);
public java.lang.String toString();
public boolean canEqual(java.lang.Object);
public int productArity();
public java.lang.String productPrefix();
public java.lang.Object productElement(int);
public java.lang.String productElementName(int);
public int x();
public int y();
public Point copy(int, int);
public int copy$default$1();
public int copy$default$2();
public int _1();
public int _2();
}
equals, hashCode, toString, and even tuple-style _1()/_2() accessors come from
case class for free — a Java caller gets value equality and a constructor without
writing a line of boilerplate, because the Scala compiler generated all of it at compile
time, not through any interop layer.
3. Call a Scala method that takes a by-name parameter from Java, and see what actually gets passed.
// ByName.scala
object ByName:
def logIfSlow(threshold: Int, msg: => String): Unit =
if threshold > 10 then println(msg)
// CallByName.java — naive attempt
public class CallByName {
public static void main(String[] args) {
ByName.logIfSlow(20, "this is slow");
}
}
CallByName.java:3: error: incompatible types: String cannot be converted to Function0<String>
ByName.logIfSlow(20, "this is slow");
^
The by-name parameter msg: => String compiles to scala.Function0<String> in
bytecode — javap on ByName$.class shows public void logIfSlow(int, scala.Function0<java.lang.String>). Scala’s => String syntax hides this; Java code has
to construct the thunk explicitly:
import scala.Function0;
public class CallByNameFixed {
public static void main(String[] args) {
ByName.logIfSlow(20, new Function0<String>() {
public String apply() { return "this is slow"; }
});
}
}
this is slow
4. Make a Scala trait with a concrete method and implement it in a Java class.
// Greeter.scala
trait Greeter:
def name: String
def greet(): String = s"Hello, $name"
javap -p Greeter.class
public interface Greeter {
public abstract java.lang.String name();
public static java.lang.String greet$(Greeter);
public default java.lang.String greet();
}
public class MyGreeter implements Greeter {
public String name() { return "Java"; }
public static void main(String[] args) {
MyGreeter g = new MyGreeter();
System.out.println(g.greet());
}
}
Hello, Java
Scala 3 compiles a trait’s concrete method to a genuine Java default method — greet()
needs no override. Add a val field to the trait instead of an abstract def and the
Java implementation breaks: Java interfaces cannot hold instance state the way a Scala
trait can.