Calling Scala from Java
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10 (3.10.0, released 2026-10-12) and every example was compiled and run with Scala
3.10.0 and javac 26 on JDK 26 (checked on 2026-10-15). Scala compiles to ordinary JVM class files, so a Java program can
call Scala code with no bridge, but the class files show Scala’s own vocabulary: an object is a class with a
MODULE$ field, a trait is an interface with default methods, a List is scala.collection.immutable.List, and a
function is a scala.Function1. This page shows what Java source has to write for each construct, which annotations improve
the Java view, and which Scala designs are awkward to use from Java. The opposite direction is covered in
Using Java from Scala. Everything described applies on the 3.3 LTS
and 3.9 LTS lines as well, except where Scala 3.9 LTS versus 3.10 says otherwise. Only 3.10.0 was run for the
examples, with two explicit comparisons against 3.9.0.
The Java examples use the Scala types of the trailhead scenario, a toolkit that plans hikes. For the Java side of the language see Java Collections Framework, Optional and Lambdas and Method References.
Building a mixed project
A Java compiler needs the compiled Scala classes and the Scala runtime jars. With Scala CLI the three steps are: compile
the Scala sources to a class directory, ask Scala CLI for the class path (which includes scala3-library_3 and, since the
standard library is published as its own artifact, scala-library), then compile and run the Java sources against both.
The commands below were used for every example on this page. The build server was switched off with --server=false,
so the Scala and Java sources are compiled by separate commands instead of by one mixed build:
scala compile --server=false -d out .
CP="out:$(scala compile --server=false -p .)"
javac -cp "$CP" -d jout Main.java
java -cp "jout:$CP" Main
In a real project a build tool does this for you: with sbt, the compileOrder setting selects Mixed (the default),
JavaThenScala or ScalaThenJava. A project in which Scala code refers to Java
code and the Java code refers to Scala code at the same time needs a mixed build (Scala parses the Java sources, then
javac compiles them against the Scala output). That single-command form through the Scala CLI build server was not run
here. See sbt & Build Tools and
Scala CLI.
The Java program needs these jars at run time. On 3.10.0 they are scala3-library_3-3.10.0.jar and
scala-library-3.10.0.jar, and scala compile -p printed both, together with the class directory of the project.
The Scala library used in this page
All the examples call one small Scala library, written with the trailhead types. It is shown in excerpts below and it is
one file, Lib.scala, in package trailhead:
//> using scala 3.10.0
package trailhead
import java.io.IOException
import scala.annotation.{targetName, varargs}
trait Described:
def name: String
def describe(): String = s"trail $name"
case class Trail(name: String, lengthKm: Double) extends Described
object Trail:
def quick(name: String): Trail = Trail(name, 1.0)
val Default: Trail = Trail("Default", 0.5)
object Planner:
val version = "1.0"
var visits = 0
def total(trails: List[Trail]): Double = trails.map(_.lengthKm).sum
def find(trails: Seq[Trail], name: String): Option[Trail] = trails.find(_.name == name)
def byName(trails: List[Trail]): Map[String, Trail] = trails.map(t => t.name -> t).toMap
@varargs def names(prefix: String, rest: String*): String = (prefix +: rest).mkString(",")
def namesNoVarargs(prefix: String, rest: String*): String = (prefix +: rest).mkString(",")
@throws[IOException]("when the file is missing")
def load(path: String): String =
if path.isEmpty then throw IOException("empty") else "data:" + path
def loadUnchecked(path: String): String =
if path.isEmpty then throw IOException("empty") else "data:" + path
def withDefault(a: Int, b: Int = 10): Int = a + b
@targetName("plusTrail")
def +(a: Trail): Int = 1
def map(f: Int => String, n: Int): String = f(n)
def lazyValue(x: => Int): Int = x + 1
def sorted[A](xs: List[A])(using ord: Ordering[A]): List[A] = xs.sorted
def twoLists(a: Int)(b: Int): Int = a + b
enum Level:
case Low, High
case Custom(n: Int)
enum Color:
case Red, Green
class Box[A](val value: A):
def map[B](f: A => B): Box[B] = Box(f(value))
def get: A = value
extension (s: String) def shout: String = s.toUpperCase + "!"
def helper(n: Int): Int = n * 2
val pi2 = 6.28
What the class files contain
The tool javap (shipped with the JDK) lists the members of a class file. It is the quickest way to see what Java can
call, because the Java compiler reads exactly the same members. The listings below were produced for the library above.
A trait with only abstract members becomes an interface, and a trait with concrete members becomes an interface with default methods (this is how Scala 2.12 and later compile traits):
public interface trailhead.Described {
public abstract java.lang.String name();
public static java.lang.String describe$(trailhead.Described);
public default java.lang.String describe();
}
A case class and its companion object give one class Trail, a second class Trail$ for the object, and static
forwarders in Trail for the companion members:
public class trailhead.Trail implements trailhead.Described, scala.Product, java.io.Serializable {
public trailhead.Trail(java.lang.String, double);
public java.lang.String describe();
...
public java.lang.String name();
public double lengthKm();
public trailhead.Trail copy(java.lang.String, double);
public java.lang.String copy$default$1();
public double copy$default$2();
public java.lang.String _1();
public double _2();
public static trailhead.Trail Default();
public static trailhead.Trail apply(java.lang.String, double);
public static trailhead.Trail fromProduct(scala.Product);
public static trailhead.Trail quick(java.lang.String);
public static trailhead.Trail unapply(trailhead.Trail);
}
A standalone object with no companion class, like Planner, becomes a final class whose members are all static
forwarders (the singleton itself is in Planner$):
public final class trailhead.Planner {
public static scala.collection.immutable.Map<java.lang.String, trailhead.Trail> byName(scala.collection.immutable.List<trailhead.Trail>);
public static scala.Option<trailhead.Trail> find(scala.collection.immutable.Seq<trailhead.Trail>, java.lang.String);
public static int lazyValue(scala.Function0<java.lang.Integer>);
public static java.lang.String load(java.lang.String) throws java.io.IOException;
public static java.lang.String loadUnchecked(java.lang.String);
public static java.lang.String map(scala.Function1<java.lang.Integer, java.lang.String>, int);
public static java.lang.String names(java.lang.String, scala.collection.immutable.Seq<java.lang.String>);
public static java.lang.String names(java.lang.String, java.lang.String...);
public static java.lang.String namesNoVarargs(java.lang.String, scala.collection.immutable.Seq<java.lang.String>);
public static int plusTrail(trailhead.Trail);
public static <A> scala.collection.immutable.List<A> sorted(scala.collection.immutable.List<A>, scala.math.Ordering<A>);
public static double total(scala.collection.immutable.List<trailhead.Trail>);
public static int twoLists(int, int);
public static java.lang.String version();
public static int visits();
public static void visits_$eq(int);
public static int withDefault(int, int);
public static int withDefault$default$2();
}
Top-level definitions (helper, pi2 and the extension method shout) are collected in a class named after the source
file, Lib$package:
public final class trailhead.Lib$package {
public static int helper(int);
public static double pi2();
public static java.lang.String shout(java.lang.String);
}
The mapping, in table form:
| Scala construct | What Java sees | How to use it |
|---|---|---|
|
class |
|
|
accessors |
|
|
interface |
|
|
class |
|
companion object members |
static forwarders in the class |
|
|
class with |
|
top-level |
static members of the class |
|
extension method |
static method with the receiver as first parameter |
|
|
abstract class |
|
|
one method with all parameters flattened |
|
|
an ordinary last parameter |
pass it explicitly |
Classes, case classes and companions
Constructing and reading a Scala class needs no ceremony. A case class also generates copy, equals, hashCode and
a readable toString. The companion object’s members have two entry points: the static forwarder in the class (Trail.apply)
and the real method on the singleton (Trail$.MODULE$.apply). The forwarder is shorter and is the one to use.
import trailhead.*;
public class Main {
public static void main(String[] args) throws Exception {
Trail t = new Trail("Ridge", 8.5);
System.out.println(t + " " + t.name() + " " + t.lengthKm());
System.out.println(Trail.apply("Pico", 12.0)); // static forwarder
System.out.println(Trail$.MODULE$.apply("Pico", 12.0)); // the module instance
System.out.println(Trail.quick("S"));
System.out.println(Trail.Default());
System.out.println(t.copy("Copy", 1.0));
System.out.println(Planner.version());
Planner.visits_$eq(3);
System.out.println(Planner.visits());
System.out.println(Lib$package.shout("hi"));
System.out.println(Lib$package.helper(4) + " " + Lib$package.pi2());
}
}
Trail(Ridge,8.5) Ridge 8.5
Trail(Pico,12.0)
Trail(Pico,12.0)
Trail(S,1.0)
Trail(Default,0.5)
Trail(Copy,1.0)
1.0
3
HI!
8 6.28
A var is exposed as a getter visits() and a setter visits_$eq(int): the name is the Scala setter name
visits_= with the symbol = encoded as $eq. Java identifiers may contain $, so this is valid Java, but it is not
pleasant; where a Java caller is expected, offer a method such as def setVisits(n: Int) or use a Scala class
with explicit getters. A val is only a getter. See Objects & Companions and
Case Classes for the Scala side.
A static forwarder is created only when it cannot clash with an instance member. If the class and the companion define a
member with the same name, the forwarder is skipped and Java must go through MODULE$:
class Foo:
def hello(): Int = 1
def only(): Int = 3
object Foo:
def hello(): Int = 2
def other(): Int = 4
public class trailhead2.Foo {
public trailhead2.Foo();
public int hello();
public int only();
public static int other();
}
public final class trailhead2.Foo$ {
public static final trailhead2.Foo$ MODULE$;
public static {};
public int hello();
public int other();
}
Foo.other() is a static call and Foo.hello() is an instance method, so the companion’s version needs
Foo$.MODULE$.hello(). Writing Foo.hello() in Java gives a compile error:
E6.java:4: error: non-static method hello() cannot be referenced from a static context
System.out.println(Foo.hello());
^
A Java program that calls Foo$.MODULE$.hello(), new Foo().hello() and Foo.other() printed 2 1 4.
Traits
A trait with abstract methods is the easiest to use: Java implements the interface and inherits the default methods. This is also how a Scala library offers extension points to Java users.
import trailhead.*;
public class Main {
static class JTrail implements Described {
public String name() { return "J"; }
}
public static void main(String[] args) {
System.out.println(new JTrail().describe());
}
}
trail J
A trait with state is not like that. A Scala trait that has a val or var with an initial value also needs an
initializer that Scala runs when a class mixes the trait in, and Java never runs it. Take this trait:
trait Limited:
val limit: Int = 42
def room: Int = limit - 1
class ScalaLimited extends Limited
public interface trailhead.Limited {
public static void $init$(trailhead.Limited);
public abstract int limit();
public abstract void trailhead$Limited$_setter_$limit_$eq(int);
public static int room$(trailhead.Limited);
public default int room();
}
The interface has a synthetic abstract setter that a Java class must implement before it even compiles:
static class JLimited implements Limited {
private int limit;
public int limit() { return limit; }
}
E5.java:3: error: JLimited is not abstract and does not override abstract method trailhead$Limited$_setter_$limit_$eq(int) in Limited
static class JLimited implements Limited {
^
1 error
Implementing it makes the class compile, but the value 42 is missing, because the initializer Limited.$init$ was never
called. The call can be made by hand from a constructor, which restores the value:
public class E5b {
static class JLimited implements Limited {
private int limit;
public int limit() { return limit; }
public void trailhead$Limited$_setter_$limit_$eq(int n) { limit = n; }
}
public static void main(String[] a) {
System.out.println(new ScalaLimited().room());
System.out.println(new JLimited().room());
JLimited j = new JLimited();
Limited.$init$(j);
System.out.println(j.room());
}
}
41
-1
41
The lesson is to keep traits meant for Java implementors stateless: abstract and concrete def`s only, no `val or
var with an initializer. A trait with a plain abstract var count: Int and a concrete method also works only if the
Java class implements the accessors itself. See Traits & Mixins.
Collections
Scala collections are Scala classes, so the Java signature of Planner.total takes a
scala.collection.immutable.List<Trail>. To build one from a Java collection use scala.jdk.javaapi.CollectionConverters,
which is the Java-friendly twin of scala.jdk.CollectionConverters (the Scala one is made of extension methods, which Java
cannot call). Its static methods are called asScala and asJava and are overloaded by argument type. The javaapi
package has one converter object for each kind of conversion: CollectionConverters, OptionConverters,
StreamConverters, DurationConverters, FutureConverters and FunctionConverters.
import trailhead.*;
import scala.Option;
import scala.Some;
import scala.collection.immutable.List;
import scala.jdk.javaapi.CollectionConverters;
import scala.math.Ordering;
public class E9 {
public static void main(String[] args) {
List<String> names = List.from(CollectionConverters.asScala(java.util.List.of("pico", "ridge", "lake")));
System.out.println(names.size() + " " + names.head() + " " + names.apply(1) + " " + names.mkString(","));
java.util.List<String> back = CollectionConverters.asJava(names);
System.out.println(back);
System.out.println(Planner.sorted(names, Ordering.String$.MODULE$));
Option<String> a = Option.apply(null);
Option<String> b = Some.apply("x");
System.out.println(a + " " + b + " " + a.isEmpty() + " " + b.getOrElse(() -> "dflt") + " " + a.getOrElse(() -> "dflt"));
System.out.println(b.map(s -> s + "!").get());
Option<String> e = Option.empty();
System.out.println(e);
System.out.println(Planner.twoLists(1, 2));
System.out.println(scala.jdk.javaapi.OptionConverters.toJava(b).get());
System.out.println(scala.jdk.javaapi.OptionConverters.toScala(java.util.Optional.of(3)));
scala.collection.immutable.Seq<Integer> sq = CollectionConverters.asScala(java.util.List.of(1, 2, 3)).toSeq();
System.out.println(sq);
}
}
3 pico ridge pico,ridge,lake
[pico, ridge, lake]
List(lake, pico, ridge)
None Some(x) true x dflt
x!
None
3
x
Some(3)
List(1, 2, 3)
Points to notice:
-
CollectionConverters.asScala(javaList)returns a mutableBuffer.List.from(...)copies it into an immutablescala.collection.immutable.List, which is what most Scala APIs take.asJava(names)wraps the Scala list as a read-onlyjava.util.Listwithout copying. -
The instance methods of the Scala collections (
size,head,apply,mkString) are callable from Java as usual. The higher-order ones (map,filter) takescala.Function1, which is a functional interface (see Functions). -
scala.Optionworks withOption.apply(null)(givesNone),Some.apply(x),Option.empty(),isEmpty,getandgetOrElse.getOrElsetakes ascala.Function0for the default, so a Java lambda() -> "dflt"works. Go tojava.util.Optionaland back withscala.jdk.javaapi.OptionConverters. -
The
using Ordering[A]parameter ofPlanner.sortedbecame an ordinary second argument, and Java had to supply anOrderingobject.Ordering.String$.MODULE$is the string ordering. ForIntegerthe same can be written asOrdering.comparatorToOrdering(java.util.Comparator.<Integer>naturalOrder()), and both forms compiled and ran. -
Multiple parameter lists are flattened:
Planner.twoLists(1, 2)fordef twoLists(a: Int)(b: Int).
Passing Scala immutable collections to library APIs is cheap, but converting Java collections into them copies. For
big data, design the Scala API to accept java.util.List or an Iterable and convert inside. A neater alternative is to
offer a Java-friendly overload on the Scala side; see Collections Overview
and Option & Null Safety. Kotlin faces the same split between
kotlin.collections and java.util; see Kotlin Null Safety for that
language’s treatment of the null boundary.
Functions
A Scala function type A => B is scala.Function1<A, B>, an interface with one abstract method (apply) and default
methods (compose, andThen), so Java lambdas and method references convert to it directly. () => A is Function0
and a by-name parameter x: => Int is also a Function0 in Java. A Unit result is scala.runtime.BoxedUnit, because
a generic type argument cannot be void.
import trailhead.*;
import trailhead2.*;
public class Main {
public static void main(String[] args) {
System.out.println(Planner.map(i -> "n=" + i, 4));
System.out.println(Planner.lazyValue(() -> 41));
System.out.println(Shapes.thunk().apply());
Shapes.unitFun(i -> { System.out.println("got " + i); return scala.runtime.BoxedUnit.UNIT; });
System.out.println(Shapes.intFun(i -> i * 10));
}
}
n=4
42
5
got 1
20
The Scala definitions used above, besides Planner, are in Shapes:
object Shapes:
def thunk: () => Int = () => 5
def unitFun(f: Int => Unit): Unit = f(1)
def intFun(f: Int => Int): Int = f(2)
The Java-friendly alternative is to accept a java.util.function.Function (or a Runnable, Supplier…) on the Scala
side and call it with apply. scala.jdk.javaapi.FunctionConverters converts between the two families. See
Functions as Values and
Lambdas and Method References.
Exceptions with @throws
Java checks exceptions at compile time and Scala does not. The Java compiler only knows an exception can be thrown if the
class file says so, which is what @throws records. Compare the two methods of Planner: both throw IOException, but
only load is annotated. The javap output above showed load(String) throws java.io.IOException and
loadUnchecked(String) without a throws clause. A catch of the checked exception compiles for the first and is
rejected for the second:
import trailhead.*;
public class E1 {
public static void main(String[] a) {
try {
System.out.println(Planner.loadUnchecked(""));
} catch (java.io.IOException e) {
System.out.println("never");
}
}
}
E1.java:6: error: exception IOException is never thrown in body of corresponding try statement
} catch (java.io.IOException e) {
^
1 error
With the annotated Planner.load("") the same shape of try compiled, and the exception was caught and printed
caught empty. Add @throws to every Scala method that Java users are expected to handle. See
Scala 3 Book: How to handle Scala methods that throw exceptions in Java code
and Exceptions, boundary & Early Exit.
Varargs with @varargs
A Scala String* parameter is a Seq[String], so Java would have to build a Scala Seq to call such a method. The
annotation @varargs makes the compiler generate a second, real Java varargs method. The javap listing above showed
both names(String, Seq<String>) and names(String, String...) for the annotated names, and only the Seq form for
namesNoVarargs. A call with several strings only works on the first:
import trailhead.*;
public class E3 {
public static void main(String[] a) {
System.out.println(Planner.namesNoVarargs("a", "b", "c"));
}
}
E3.java:5: error: method namesNoVarargs in class Planner cannot be applied to given types;
System.out.println(Planner.namesNoVarargs("a", "b", "c"));
^
required: String,Seq<String>
found: String,String,String
reason: actual and formal argument lists differ in length
1 error
With @varargs, Planner.names("a", "b", "c") printed a,b,c and Planner.names("a") printed a.
Symbolic names and @targetName
JVM method names cannot contain most symbols, so the compiler encodes them: + becomes $plus, :: becomes
$colon$colon, and = in a setter becomes $eq. Java can call such a method because $ is legal in identifiers, but the
result is unreadable. @targetName chooses the JVM name:
object Ops:
def +(n: Int): Int = n + 1
def ::(n: Int): Int = n
@targetName("plusTwo")
def ++(n: Int): Int = n + 2
public final class trailhead3.Ops {
public static int $plus(int);
public static int $colon$colon(int);
public static int plusTwo(int);
}
System.out.println(Ops.$plus(1) + " " + Ops.plusTwo(1) + " " + Ops.$colon$colon(5));
2 3 5
Source: Scala 3 Book: Create alternate names to use Scala methods in Java and Scala 3 Reference: The @targetName annotation
The same annotation resolves clashes between overloads that have the same erased signature, which makes it useful beyond
Java use. The Planner listing earlier showed @targetName("plusTrail") producing the Java method plusTrail. See
Scala 3 Book: Create alternate names to use Scala methods in Java
and the reference page for @targetName.
A related trap is a Scala name that is a Java keyword. short is a legal Scala method name, and Java cannot call it:
object Shapes:
def short(n: Int): Int = n
System.out.println(Shapes.short(1));
E2.java:5: error: <identifier> expected
System.out.println(Shapes.short(1));
^
E2.java:5: error: ';' expected
System.out.println(Shapes.short(1));
^
2 errors
Use @targetName("shortOf") in that case, or choose another name. The same holds for int, class, new, default,
switch and the other Java reserved words when they appear as Scala members.
Default arguments and @unroll
A method with default arguments is one method on the JVM, so Java must pass every argument. The compiler also generates
accessor methods for the default values, named method$default$N, that Java can call:
System.out.println(Planner.withDefault(1, Planner.withDefault$default$2()));
11
A change that adds a defaulted parameter to a public method also changes its JVM signature, which breaks binary
compatibility of existing callers, Java or Scala. The @unroll annotation of SIP-61 solves both problems: it generates
one overload for each prefix of the optional parameters. In Scala 3.10 the feature is a preview feature (switched on
with -preview; it was experimental in 3.9). The listing below was produced with //> using option -preview:
//> using scala 3.10.0
//> using option -preview
package trailhead2
import scala.annotation.unroll
object Defaults:
def show(a: Int, b: Int = 10): Int = a + b
def unrolled(a: Int, @unroll b: Int = 10, @unroll c: Int = 20): Int = a + b + c
public final class trailhead2.Defaults {
public static int show(int, int);
public static int unrolled(int);
public static int unrolled(int, int);
public static int unrolled(int, int, int);
public static int show$default$2();
public static int unrolled$default$2();
public static int unrolled$default$3();
}
System.out.println(Defaults.unrolled(1) + " " + Defaults.unrolled(1, 2) + " " + Defaults.unrolled(1, 2, 3));
31 23 6
All three overloads are real methods, so Java callers get the ergonomics that Scala callers have. Preview features may change before they are final; see the SIP-61 page and Language Evolution: SIPs, Preview & Experimental.
Enums, opaque types and unions
A Scala 3 enum is a class that extends scala.reflect.Enum; it is not a java.lang.Enum, so it cannot be used in a
Java switch over enum constants. The constants are static fields of the generated E$ class, and an enum with only simple
cases (Color) also has static values(), valueOf and fromOrdinal on Color. An enum that has a parameterised case
(Level.Custom(n)) has only fromOrdinal as a static method on the enum class, and the nested case class is
Level.Custom:
System.out.println(Level$.High); // a parameterless case is a static field of Level$
System.out.println(Level.fromOrdinal(0));
System.out.println(Level.Custom.apply(5));
System.out.println(Color.values().length + " " + Color.valueOf("Green") + " " + Color$.Red);
High
Low
Custom(5)
2 Green Red
Types that exist only in the Scala compiler are erased in the signatures that Java sees. A union Int | String becomes
Object, an opaque type becomes its underlying type, and a tuple is a scala.Tuple2<Integer, String>:
object Shapes:
def either(x: Int | String): String = x.toString
def pair(): (Int, String) = (1, "a")
def firstLen(xs: List[String]): Option[Int] = xs.headOption.map(_.length)
opaque type Km = Double
object Km:
def apply(d: Double): Km = d
def distance(k: Km): Km = k
public static java.lang.String either(java.lang.Object);
public static scala.Tuple2<java.lang.Integer, java.lang.String> pair();
public static scala.Option<java.lang.Integer> firstLen(scala.collection.immutable.List<java.lang.String>);
public static double distance(double);
A Java caller sees Object for the union and double for the opaque Km, so the type safety of opaque types exists only
on the Scala side. See Enums,
Opaque Types & Value Classes and
Union & Intersection Types. A Java program calling these printed
x3 for Shapes.either("x") + Shapes.either(3), 1 a for the pair and None for firstLen of an empty list.
More precise generic signatures in Scala 3.10
Scala 3.10 changed the generic signature stored in class files: primitive type arguments are now written as their boxed
types. The JVM method descriptor (the erased signature) is the same, so Scala-to-Scala linking is not affected, but what
javac reads from the generic signature changes. The same three-method object compiled with 3.9.0 and with 3.10.0:
package sig
object Api:
def firstLen(xs: List[String]): Option[Int] = xs.headOption.map(_.length)
def intFun(f: Int => Int): Int = f(2)
def pair(): (Int, String) = (1, "a")
3.9.0
public final class sig.Api {
public static scala.Option<java.lang.Object> firstLen(scala.collection.immutable.List<java.lang.String>);
public static int intFun(scala.Function1<java.lang.Object, java.lang.Object>);
public static scala.Tuple2<java.lang.Object, java.lang.String> pair();
}
3.10.0
public final class sig.Api {
public static scala.Option<java.lang.Integer> firstLen(scala.collection.immutable.List<java.lang.String>);
public static int intFun(scala.Function1<java.lang.Integer, java.lang.Integer>);
public static scala.Tuple2<java.lang.Integer, java.lang.String> pair();
}
Java code written against the 3.9 signature, using Function1<Object, Object> and Option<Object>, compiles against 3.9.0 and fails
against 3.10.0 with the following errors:
Function1<Object, Object> f = x -> (Integer) x + 1;
System.out.println(Api.intFun(f));
Option<Object> o = Api.firstLen(scala.collection.immutable.List$.MODULE$.empty());
E8.java:8: error: incompatible types: Function1<Object,Object> cannot be converted to Function1<Integer,Integer>
System.out.println(Api.intFun(f));
^
E8.java:9: error: incompatible types: Option<Integer> cannot be converted to Option<Object>
Option<Object> o = Api.firstLen(scala.collection.immutable.List$.MODULE$.empty());
^
2 errors
The new signatures are the more natural ones to program against: Function1<Integer, Integer> accepts i -> i * 10
directly, as in the earlier example. The release notes also say that a library republished with 3.10 can trigger MiMa
IncompatibleSignatureProblem reports, and that MiMa 1.2.1 or later is needed to check Scala 3.9 and 3.10 artifacts. The
3.9 LTS remains the recommended line for libraries that need the longest support window, see
the 3.10 release notes and
Versions, Compatibility & JDKs.
Designing a Scala API that Java users enjoy
-
Use stateless traits (no initialised
valorvar) for extension points, or abstract classes with constructors. -
Add
@throwsfor exceptions callers should handle,@varargsfor repeated parameters, and@targetNamefor symbolic operators and Java reserved words. -
Prefer
java.util.List,Optional,java.util.function.FunctionandIterablein the public signature of an API meant for both languages; do the conversion inside. -
Avoid
usingparameters, extension methods and opaque types in the Java-facing surface, because Java sees them as ordinary parameters, static methods and raw types. -
Avoid a
case classwith a defaulted parameter in the public API of a library that Java calls; add an overload or use@unrollwhere the preview feature is acceptable. -
Check the result with
javap -cp classes your.Class: what is listed is exactly what Java can call.
Pitfalls
-
traitmembers with initialisers cannot be implemented from Java without calling$init$by hand. -
The member of a companion object has no static forwarder when the class has a member of the same name; use
MODULE$. -
scala.Optionhas noisPresentand itsgetOrElseandorElsetake aFunction0, not a value: useisDefinedandgetOrElse(() -> value), or convert toOptional. -
Scala collections are immutable and have no
add;CollectionConverters.asJavaviews are read-only for them. -
Java sees
Unitin a function type asBoxedUnit, and must returnBoxedUnit.UNITfrom such a lambda. -
A method named after a Java keyword (
short,int,new…) cannot be called from Java. -
The Scala runtime jars (
scala-libraryandscala3-library_3) have to be on the Java class path at run time.
Scala 3.9 LTS versus 3.10
-
Traits as default-method interfaces, static forwarders,
MODULE$,@throws,@varargsand@targetNameare the same on 3.3 LTS, 3.9 LTS and 3.10. -
Generic signatures use boxed type arguments from 3.10 on (
Option<Integer>, notOption<Object>); the comparison above was run on 3.9.0 and 3.10.0. -
@unrollis a preview feature on 3.10 (needs-preview); it was not run on 3.9. -
The class path needs both
scala3-library_3andscala-libraryjars; on 3.10.0 both were version 3.10.0. -
Not run here: the one-command mixed build through the Scala CLI build server and a Maven or sbt mixed project.
Where the books are outdated
-
Books and posts that predate Scala 2.12 describe traits as compiling to an interface plus an
Implclass; since 2.12 and in Scala 3 a trait is one interface with default methods, as the listings above show. -
The Scala 3 Book section on Scala collections in Java uses
scala.jdk.javaapi.CollectionConverters; older Scala 2 code usesscala.collection.JavaConvertersorJavaConversions, neither of which is the recommended path any more. -
Scala 2 books do not mention
@targetName,@unrollor the precise generic signatures of 3.10. -
Scala 2
implicitparameters and Scala 3usingparameters look the same from Java: they are ordinary last arguments.