Using Java from Scala
|
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 through Scala CLI on JDK 26 (checked on 2026-10-15). Scala and Java share one runtime, so a Scala program can
create Java objects, call Java methods, extend Java classes and implement Java interfaces with no wrapper layer and no
foreign-function glue. What differs is the vocabulary: Java collections are not Scala collections, java.util.Optional
is not Option, a Java method may return null, and Java generics know nothing about Scala variance. This page walks
through each of those seams. The opposite direction, Java calling Scala, is on
Calling Scala from Java. Everything here works the same
on the 3.9 LTS line and on 3.3 LTS unless Scala 3.9 LTS versus 3.10 says otherwise; the examples were run on 3.10.0
only, so statements about other versions come from release notes or from the explicitly labelled runs.
The examples use the trailhead scenario of a toolkit that plans hikes. Some of them need Java types, so they come with a handful of small Java source files. For the Java side see Java Collections Framework, Optional and Lambdas and Method References.
Running Java and Scala together
A Scala CLI project may contain .java files next to .scala files, and the Scala compiler reads the Java sources for
type information (the Java parser built into the compiler provides it). To also produce bytecode for the Java files, Scala CLI hands
the project to its build server (Bloop and Zinc), which compiles Java and Scala in the order they depend on each other.
Scala CLI itself prints this warning when the build server is switched off with --server=false:
[warn] With --server=false, .java files are not compiled to .class files.
scalac parses .java sources for type information (cross-compilation), but without the build server (Bloop/Zinc) nothing compiles them to bytecode.
Remove --server=false or compile Java files separately to avoid runtime NoClassDefFoundError.
The examples on this page were run without the build server, so the Java files were compiled once with javac and the
output directory was added to the Scala CLI class path with --extra-jar (it accepts a directory of classes):
javac -d out Surface.java Waypoint.java Shape.java Registry.java
scala run --server=false --extra-jar out .
The result is the same as a normal mixed project. In an sbt build the equivalent is the compileOrder setting, whose
default Mixed lets Scala sources refer to Java sources and the other way around; see
sbt & Build Tools and
Scala CLI. The one case not run here is the single-command mixed build through the
build server, because the runs on this page deliberately avoided starting it.
Java collections
Scala collections and java.util collections are unrelated types, so a java.util.List[String] cannot be passed where a
Scala List[String] is expected. The standard library bridges them with scala.jdk.CollectionConverters, a set of
extension methods asScala and asJava. Import it with a wildcard, which brings in the extensions.
//> using scala 3.10.0
import java.util.{ArrayList, HashMap, List as JList, Map as JMap}
import scala.jdk.CollectionConverters.*
def javaTrails(): JList[String] =
val l = new ArrayList[String]()
l.add("Ridge"); l.add("Pico"); l.add("Lake")
l
@main def run(): Unit =
val jl = javaTrails()
val sl: List[String] = jl.asScala.toList // copy to an immutable List
println(sl)
val buf = jl.asScala // a live view: mutable.Buffer
println(buf.getClass.getName)
buf += "Col"
println(jl) // the Java list sees the change
val jm = new HashMap[String, Int]()
jm.put("Ridge", 8); jm.put("Pico", 12)
val sm = jm.asScala.toMap
println(sm.toList.sortBy(_._1))
val back: JMap[String, Int] = Map("a" -> 1, "b" -> 2).asJava
println(back.getClass.getName)
try back.put("c", 3) catch case e: UnsupportedOperationException => println("immutable view: " + e.getClass.getSimpleName)
println(List(1, 2, 3).asJava.getClass.getName)
val it = jl.iterator().asScala
println(it.map(_.toUpperCase).mkString(","))
for t <- jl.asScala do println(t)
List(Ridge, Pico, Lake)
scala.collection.convert.JavaCollectionWrappers$JListWrapper
[Ridge, Pico, Lake, Col]
List((Pico,12), (Ridge,8))
scala.collection.convert.JavaCollectionWrappers$MapWrapper
immutable view: UnsupportedOperationException
scala.collection.convert.JavaCollectionWrappers$SeqWrapper
RIDGE,PICO,LAKE,COL
Ridge
Pico
Lake
Col
The class names printed above explain the semantics. asScala and asJava do not copy: they return a thin wrapper
that forwards every call to the original collection. A write through the Scala Buffer showed up in the Java list, and a
Scala immutable Map converted with asJava throws UnsupportedOperationException on put, because the wrapper
cannot change something that is immutable. If a snapshot is wanted, finish the conversion with toList, toMap, toSet
or toVector, which copy into a Scala collection.
The table lists the conversions of CollectionConverters, which is the same set the 2.13 collections guide documents.
| Java type | Scala type from asScala |
Back with asJava |
|---|---|---|
|
|
yes |
|
|
yes ( |
|
|
yes ( |
|
|
yes ( |
|
|
yes ( |
|
|
yes |
|
|
|
|
|
|
Two practical consequences follow from the wrapper design. First, a Java List is viewed as a mutable Buffer, never
as an immutable List, so the type tells you what can be done with it. Second, a Java collection is not a Scala
collection, so it cannot be the generator of a for comprehension. The compiler says so, and asScala is the fix
(see For Comprehensions):
//> using scala 3.10.0
import java.util.ArrayList
@main def run(): Unit =
val jl = new ArrayList[String]()
jl.add("a")
for s <- jl do println(s)
-- [E008] Not Found Error: F.scala:7:11
7 | for s <- jl do println(s)
| ^^
|value foreach is not a member of java.util.ArrayList[String] - did you mean jl.forEach?
1 error found
Java collections are usually mutable, and many Java APIs mutate their arguments, so it is common to use the Java API on the Java collection and convert only at the border. The standard Java methods that take lambdas work directly, as the next example shows. Everything runs on JDK 26 and also relies on SAM conversion, which is explained in Functional interfaces and SAM conversion.
//> using scala 3.10.0
import java.util.{ArrayList, Comparator, Collections, HashMap}
import scala.jdk.CollectionConverters.*
@main def run(): Unit =
val jl = new ArrayList[String]()
jl.add("pico"); jl.add("ridge"); jl.add("lake")
val cmp: Comparator[String] = (a, b) => a.length - b.length
Collections.sort(jl, cmp)
println(jl)
jl.sort(Comparator.comparing[String, Integer](_.length).reversed())
println(jl)
jl.removeIf(_.startsWith("l"))
println(jl)
val m = new HashMap[String, Int]()
m.merge("a", 1, _ + _)
m.merge("a", 1, _ + _)
m.computeIfAbsent("b", _ => 10)
println(m.asScala.toList.sorted)
for e <- m.entrySet().asScala.toList.sortBy(_.getKey) do println(s"${e.getKey} -> ${e.getValue}")
val ints: Array[Int] = Array(3, 1, 2)
java.util.Arrays.sort(ints)
println(ints.toList)
[pico, lake, ridge]
[ridge, pico, lake]
[ridge, pico]
List((a,2), (b,10))
a -> 2
b -> 10
List(1, 2, 3)
A Scala Array[Int] is a real JVM int[], so it can be passed to Java methods such as Arrays.sort directly. Compare
this with Seq or List, which are Scala types and must be converted.
Streams and Optional
Java 8 added java.util.stream.Stream and java.util.Optional, and the standard library has a converter for each:
scala.jdk.StreamConverters and scala.jdk.OptionConverters. The extension toScala turns a Java value into a Scala
one and toJava does the reverse. Both live in scala.jdk. They replace the older scala.collection.JavaConverters (still present on 3.10.0 but
deprecated) and the implicit scala.collection.JavaConversions (which converted silently and no longer exists on 3.10.0;
see Where the books are outdated).
//> using scala 3.10.0
import java.util.Optional
import java.util.stream.{Stream as JStream, IntStream}
import scala.jdk.OptionConverters.*
import scala.jdk.StreamConverters.*
import scala.jdk.CollectionConverters.*
def findTrail(name: String): Optional[String] =
if name == "Ridge" then Optional.of("Ridge trail") else Optional.empty()
@main def run(): Unit =
val a: Option[String] = findTrail("Ridge").toScala
val b: Option[String] = findTrail("Nope").toScala
println((a, b))
val c: Optional[Int] = Some(3).toJava
val d: Optional[Int] = Option.empty[Int].toJava
println((c, d))
// Optional of a primitive
val od = java.util.OptionalDouble.of(2.5)
println(od.toScala)
println(Option(2.5).toJavaPrimitive)
// Streams
val s: JStream[String] = List("a", "bb", "ccc").asJava.stream()
println(s.toScala(List).map(_.length))
println(IntStream.rangeClosed(1, 5).toScala(Vector))
println(List(1, 2, 3).asJavaSeqStream.map(_ * 2).toArray.toList)
println(Vector("x", "y").asJavaSeqStream.toScala(List))
println(Array(1, 2, 3, 4).asJavaSeqStream.sum)
(Some(Ridge trail),None)
(Optional[3],Optional.empty)
Some(2.5)
OptionalDouble[2.5]
List(1, 2, 3)
Vector(1, 2, 3, 4, 5)
List(2, 4, 6)
List(x, y)
10
Notice that toScala on a stream takes the target collection as an argument (toScala(List)), and that
asJavaSeqStream on a List[Int] produces a primitive IntStream, so .sum and .toArray are available while .toList
is not (the method is toScala(List)). The primitive variants of Optional have toScala too, and
toJavaPrimitive makes an OptionalDouble from an Option[Double]. Stream, IntStream, LongStream and
DoubleStream are all covered. Scala has no counterpart of a Java Stream pipeline for its own collections; the
collection methods themselves (see Collection Operations)
and views play that role, so conversion is needed mainly at API boundaries.
Java classes, enums, records and static members
Using a Java class from Scala needs no special syntax: new or the creator-application form Registry(), and member
access with a dot. The next Java sources define an enum, a record, a sealed interface of records and a plain class.
// Surface.java
package trailhead;
public enum Surface {
GRAVEL("gravel", 1.0), ROCK("rock", 1.5), SNOW("snow", 2.0);
private final String label;
private final double effort;
Surface(String label, double effort) { this.label = label; this.effort = effort; }
public String label() { return label; }
public double effort() { return effort; }
}
// Waypoint.java
package trailhead;
public record Waypoint(String name, int elevationM) {
public Waypoint {
if (elevationM < 0) throw new IllegalArgumentException("negative elevation");
}
public static Waypoint summit(String name) { return new Waypoint(name, 3000); }
}
// Shape.java
package trailhead;
public sealed interface Shape permits Shape.Circle, Shape.Rect {
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
}
// Registry.java
package trailhead;
import java.util.*;
public class Registry {
public static final int MAX = 5;
public static int created = 0;
private final Map<String, Waypoint> byName = new LinkedHashMap<>();
public Registry() { created++; }
public void add(Waypoint w) { byName.put(w.name(), w); }
public Waypoint find(String name) { return byName.get(name); } // may return null
public String note() { return null; }
public List<Waypoint> all() { return new ArrayList<>(byName.values()); }
public int size() { return byName.size(); }
public static String describe(String sep, String... parts) { return String.join(sep, parts); }
public static <T extends Comparable<T>> T max(List<? extends T> xs) {
T best = null;
for (T x : xs) if (best == null || x.compareTo(best) > 0) best = x;
return best;
}
}
The Scala side uses all four. A Java enum is an ordinary class with static fields for its constants, values() and
valueOf as static methods, and it can be used in a match (which is checked for exhaustivity, as the next example
shows). A Java record exposes its components as accessor methods named after the components (w.name, not w.getName) and
on Scala 3.10 it can be matched with a constructor pattern, because the compiler reads the record components from the class
file. Record patterns are new in 3.10: the same match fails to compile on 3.9.0 and 3.3.8 with Not found: w.
//> using scala 3.10.0
import trailhead.*
import scala.jdk.CollectionConverters.*
def area(s: Shape): Double = s match
case Shape.Circle(r) => math.Pi * r * r
case Shape.Rect(w, h) => w * h
@main def run(): Unit =
val s = Surface.ROCK
println((s, s.label, s.effort, s.ordinal, s.name))
println(Surface.values.toList)
println(Surface.valueOf("SNOW"))
val label = s match
case Surface.GRAVEL => "easy"
case Surface.ROCK => "careful"
case Surface.SNOW => "gear"
println(label)
val w = Waypoint("Pico", 2500)
println(w)
println(w.name + " " + w.elevationM)
w match
case Waypoint(n, e) if e > 2000 => println(s"$n is high")
case Waypoint(n, _) => println(s"$n is low")
println(Waypoint.summit("Peak"))
println(area(Shape.Circle(1.0)))
println(area(Shape.Rect(2, 3)))
val reg = Registry()
reg.add(w)
println(reg.size) // no parentheses needed for a Java method
println(reg.size())
println(Registry.MAX)
println(Registry.describe("-", "a", "b", "c"))
println(Registry.describe("-"))
val arr = Array("x", "y")
println(Registry.describe("+", arr*))
println(Registry.max(List[Integer](3, 9, 4).asJava))
println(reg.find("Nope"))
println(reg.note())
println(Option(reg.note()).getOrElse("(none)"))
try Waypoint("bad", -1) catch case e: IllegalArgumentException => println(e.getMessage)
(ROCK,rock,1.5,1,ROCK)
List(GRAVEL, ROCK, SNOW)
SNOW
careful
Waypoint[name=Pico, elevationM=2500]
Pico 2500
Pico is high
Waypoint[name=Peak, elevationM=3000]
3.141592653589793
6.0
1
1
5
a-b-c
x+y
2
null
null
(none)
negative elevation
Points to notice, in the order of the example:
-
Java static members are accessed through the class name (
Registry.MAX,Surface.valueOf), because Scala has nostatic. The Java class is seen as a companion-like object; see Objects & Companions. -
s.ordinalands.nameare the ordinaryEnummethods. Java enums are not Scala 3enum`s (see Enums), but a Java enum is matched with the same `casesyntax. -
The sealed interface
Shapeand its nested records are matched with constructor patterns. The compiler treats the Javasealedhierarchy as sealed, so exhaustivity is checked for it and for Java enums, exactly as for Scala types. -
A Java varargs parameter (
String... parts) accepts zero or more arguments, and a ScalaArrayorSeqis passed with the postfix splice*(arr*); see Methods & Parameters. -
findandnotereturnednull; see Nulls from Java.
The exhaustivity checks in the bullet above are real and behave as in Scala. Leaving a case out of a match on a Java enum or a Java sealed interface produces the usual warning:
//> using scala 3.10.0
import trailhead.*
def area(s: Shape): Double = s match
case Shape.Circle(r) => math.Pi * r * r
def level(s: Surface): String = s match
case Surface.GRAVEL => "easy"
case Surface.ROCK => "careful"
-- [E029] Pattern Match Exhaustivity Warning: M.scala:4:29
4 |def area(s: Shape): Double = s match
| ^
| match may not be exhaustive.
|
| It would fail on pattern case: _: trailhead.Shape.Rect
-- [E029] Pattern Match Exhaustivity Warning: M.scala:7:32
7 |def level(s: Surface): String = s match
| ^
| match may not be exhaustive.
|
| It would fail on pattern case: SNOW
2 warnings found
See Pattern Matching for how exhaustivity and sealed types
work, and Records and Sealed Classes for the Java side.
Boxing and generics: Int versus Integer
Java generics work only over reference types, and Scala’s Int is not a java.lang.Integer. The compiler converts
between them automatically for ordinary values (the val ix: Int = bx and val bx: Integer = 5 forms both compile), but
it cannot do so where a Java type parameter carries a bound. Registry.max above is declared as
<T extends Comparable<T>>, and Int does not implement Comparable[Int]. The first attempt fails:
println(Registry.max(List(3, 9, 4).asJava))
-- [E007] Type Mismatch Error: Main.scala:38:37
38 | println(Registry.max(List(3, 9, 4).asJava))
| ^^^^^^^^^^^^^^^^^^^^
| Found: java.util.List[Int]
| Required: java.util.List[? <: T]
|
| where: T is a type variable with constraint <: Comparable[T]
The fix used in the example is to ask for a list of Integer explicitly, List[Integer](3, 9, 4).asJava, which printed
9. In Scala code that must call such APIs often, prefer java.lang.Integer and java.lang.Double at the border or
use an Ordering-based Scala API instead, such as List.max.
A static field is assignable, but a compiler crash lurks
Java static fields are assignable from Scala. Plain assignment worked in every run:
//> using scala 3.10.0
import trailhead.*
@main def run(): Unit =
Registry.created = Registry.created + 1
println(Registry.created)
1
A compound assignment to the same field did not compile in this setup. With Registry.created += 1 (the Registry class
loaded from the compiled javac output, as in all runs on this page) the compiler crashed on both 3.9.0 and 3.10.0,
while 3.3.8 compiled and ran it:
Exception in thread "main" java.lang.AssertionError: an unexpected type representation reached the compiler backend while compiling M.scala: NoType.
at dotty.tools.backend.jvm.BTypeLoader.bTypeFromType(BTypeLoader.scala:121)
at dotty.tools.backend.jvm.BCodeSkelBuilder$PlainSkelBuilder.symInfoTK(BCodeSkelBuilder.scala:155)
...
This crash was seen with a class loaded from a class file and was not tried against Java sources compiled by the build
server. An instance field (p.visits += 1, shown in the next section) is fine, and so is the expanded form
Registry.created = Registry.created + 1. Mutable Java statics are best avoided anyway.
Extending Java interfaces and classes
A Scala class can extend a Java class and mix in Java interfaces with with (or a comma list) in the usual way, and a
Scala trait can extend a Java interface. Java default methods count as concrete members, abstract methods must be
implemented, and super calls reach the Java implementations. Generic parameters, including wildcards such as
List<? extends T>, are translated to Scala’s use-site-variance-free form; a Java Scorer<? super T> is seen as
Scorer[? >: T].
// Scorer.java
package trailhead;
@FunctionalInterface
public interface Scorer<T> {
int score(T item);
default Scorer<T> twice() { return t -> 2 * score(t); }
static <T> Scorer<T> constant(int n) { return t -> n; }
}
// Planner.java
package trailhead;
import java.util.List;
import java.util.function.Function;
public abstract class Planner {
protected final String name;
public int visits;
protected Planner(String name) { this.name = name; }
public abstract String plan(List<String> stops);
public String banner() { return "Planner " + name; }
public <T> int total(List<T> xs, Scorer<? super T> s) {
int sum = 0;
for (T x : xs) sum += s.score(x);
return sum;
}
public String apply(Function<String, String> f, String s) { return f.apply(s); }
public void run(Runnable r) { r.run(); }
}
// Gate.java
package trailhead;
public interface Gate {
boolean open();
default String status() { return open() ? "open" : "closed"; }
}
//> using scala 3.10.0
import trailhead.*
import java.util.List as JList
import scala.jdk.CollectionConverters.*
class RidgePlanner(n: String) extends Planner(n):
override def plan(stops: JList[String]): String =
s"$name: ${stops.asScala.mkString(" -> ")}"
override def banner(): String = super.banner() + "!"
class Lock(var locked: Boolean) extends Gate:
def open(): Boolean = !locked
@main def run(): Unit =
val p = RidgePlanner("R")
println(p.plan(List("a", "b").asJava))
println(p.banner())
p.visits += 1
println(p.visits)
// anonymous implementations
val g = new Gate:
def open() = true
println(g.status())
println(Lock(true).status())
R: a -> b
Planner R!
1
open
closed
Notes on the example:
-
Planner(n)is the superclass constructor call, and theprotected final String namefield is visible in the subclass. -
The Java parameter
List<String>appears asJList[String]. The importjava.util.List as JListavoids a clash withscala.List; the rename syntaxasis described in Packages, Imports & Top-Level Definitions. -
A Java method with no parameters is overridden as
def open(): Boolean, with the empty parameter list. See Auto-application of Java methods for what that implies at call sites. -
An instance field such as
visitsis a plain Scalavar:p.visits += 1compiles and works.
Functional interfaces and SAM conversion
A Java interface with exactly one abstract method is a functional interface (Runnable, Comparator,
java.util.function.*, and any interface such as Scorer). Scala converts a lambda to such an interface automatically
when the expected type is known, which is SAM conversion (single abstract method). The same conversion applies to
Scala traits with one abstract method. It makes Java APIs that take callbacks pleasant to use, and it is the
reason a Scala function literal can be passed straight to Executor.submit, Collections.sort or a Java stream.
//> using scala 3.10.0
import trailhead.*
import java.util.List as JList
import scala.jdk.CollectionConverters.*
class RidgePlanner(n: String) extends Planner(n):
override def plan(stops: JList[String]): String = n
@main def run(): Unit =
val p = RidgePlanner("R")
val byLen: Scorer[String] = s => s.length
println(p.total(List("aa", "bbbb").asJava, byLen))
println(byLen.twice().score("abc"))
println(Scorer.constant[String](7).score("x"))
println(p.apply(s => s.toUpperCase, "hi"))
println(p.apply(_.reverse, "abc"))
p.run(() => println("running"))
6
6
7
HI
cba
running
The expected type must be known for the conversion to happen. Where an overloaded Java generic method gives the compiler
nothing to infer from, the lambda parameter has type Object and the compiler cannot see length on it, so give the
type arguments explicitly:
//> using scala 3.10.0
import java.util.Comparator
@main def run(): Unit =
val c = Comparator.comparing[String, Integer](_.length)
println(c.compare("aa", "b"))
val d: Comparator[String] = Comparator.comparingInt(_.length)
println(d.compare("aa", "b"))
val f = Comparator.comparing(_.length)
-- [E008] Not Found Error: V.scala:11:33
11 | val f = Comparator.comparing(_.length)
| ^^^^^^^^
| value length is not a member of Object
1 error found
The first two forms compile: type arguments (comparing[String, Integer]) or an expected type
(val d: Comparator[String]) are enough. Only the third, which has neither, fails; comparingInt is a convenient
alternative because it avoids the Integer boxing. Scala’s own Ordering.by is simpler, and
Comparator can be built from an Ordering when a Java API needs one (Ordering.by[String, Int](_.length) is
itself a Comparator[String] because Ordering extends java.util.Comparator). For function values in general see
Functions as Values.
Anonymous classes also work when a lambda is not possible, as new Gate: def open() = true in the previous section.
Java annotations
Java annotations are applied with the same @ syntax as Scala annotations, with the element values as arguments. The
single element value can be given positionally, and other elements are passed by name. Array-valued elements are
passed as Array(...), and nested annotations are passed as annotation expressions. Because the annotation here has
RetentionPolicy.RUNTIME, the program can read it back with Java reflection:
// Tag.java
package trailhead;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Tag {
String value();
int priority() default 0;
String[] aliases() default {};
}
//> using scala 3.10.0
import trailhead.*
@Tag("trail")
class Annotated:
@Tag(value = "m", priority = 2, aliases = Array("x", "y"))
def m(): Unit = ()
@main def run(): Unit =
val t = classOf[Annotated].getAnnotation(classOf[Tag])
println((t.value, t.priority, t.aliases.toList))
val tm = classOf[Annotated].getMethod("m").getAnnotation(classOf[Tag])
println((tm.value, tm.priority, tm.aliases.toList))
(trail,0,List())
(m,2,List(x, y))
Source: Tour of Scala: Java annotations
Defaults of the Java annotation (priority = 0, no aliases) are filled in on the Java side. The reverse is not true:
annotations written in Scala (extending scala.annotation.StaticAnnotation) are not emitted as runtime annotations that
Java reflection can read. For the
Scala side of annotations and for the annotations that mean something to the JVM (@volatile, @transient, @native,
@throws, @SerialVersionUID) see Annotations & Visibility. Java’s
@FunctionalInterface on an interface needs no Scala counterpart because SAM conversion looks only at the shape of the
interface.
Nulls from Java
Java has null and no type distinction for it, so any Java method with a reference result may return null. In plain
Scala 3, null is a member of every reference type (except under explicit nulls, below), so nothing stops a
Java null flowing through a String value and failing later with a NullPointerException. In the program above,
reg.find("Nope") and reg.note() printed null. The idiomatic defence is to wrap the value as soon as it arrives:
Option(x) yields None for null and Some(x) otherwise, as shown by Option(reg.note()).getOrElse("(none)").
After that, normal Scala handling applies; see Option & Null Safety
and, for Kotlin’s approach to the same seam, Kotlin Null Safety.
Java’s Optional is the other way of avoiding it, see Optional.
Without any option, the unguarded access fails at the point where the value is used:
//> using scala 3.10.0
import trailhead.*
@main def run(): Unit =
val reg = Registry()
val n: String = reg.note()
println(n)
println(n.length)
null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "n" is null
at N$package$.run(N.scala:7)
at run.main(N.scala:3)
Explicit nulls
Scala 3 has an opt-in mode, -Yexplicit-nulls, that removes Null from the reference types, so String no longer
contains null and a nullable value has the union type String | Null (see Union & Intersection Types). Java code
cannot be annotated with these types, so the compiler reads Java signatures as flexible types: a Java String result
is seen as String | Null when that is convenient and as String when that is. The mode is documented in the
Scala 3 reference.
//> using scala 3.10.0
//> using option -Yexplicit-nulls
import trailhead.*
@main def run(): Unit =
val reg = Registry()
val w: Waypoint | Null = reg.find("none")
println(w)
val n: String | Null = reg.note()
println(n.nn.length)
null
Exception in thread "main" java.lang.NullPointerException: tried to cast away nullability, but value is null
at scala.runtime.Scala3RunTime$.nnFail(Scala3RunTime.scala:32)
at N$package$.run(N.scala:10)
at run.main(N.scala:5)
The extension .nn ("not null") asserts non-nullness and throws a descriptive NullPointerException (tried to cast away
nullability) when it is wrong, which is much easier to trace than an NPE deep in Java. Explicit nulls does not make Java
safe, though. Because Java types are flexible, assigning a Java result straight to a non-null type still compiles, and the
value is only checked when it is used:
//> using scala 3.10.0
//> using option -Yexplicit-nulls
import trailhead.*
@main def run(): Unit =
val reg = Registry()
val n: String = reg.note()
println(n.length)
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "n" is null
at N$package$.run(N.scala:8)
at run.main(N.scala:5)
So the discipline is the same with or without the flag: wrap Java results in Option(...) at the boundary. What explicit nulls does
prevent is Scala code accidentally producing or storing null: in the same mode val k: Int = null is a compile error, and
val s: String = null too (both print Found: Null and Required: ...). The flag begins with -Y, the prefix of advanced
options, so check Compiler Options, Linting & Errors
before turning it on in a large codebase. The import scala.language.unsafeNulls switches the check off in a scope.
Auto-application of Java methods
In Scala 3, a method defined with an empty parameter list, def open(): Boolean, must be called with parentheses,
l.open(). The dropped "auto-application" of Scala 2 would have allowed l.open. Java methods are exempt, because Java
has no way of saying whether a method is a property or an action, and so are Scala methods that override a Java
method. The next program shows all three behaviours, and the error for a plain Scala method:
//> using scala 3.10.0
import trailhead.*
import java.util.List as JList
class Lock(var locked: Boolean) extends Gate:
def open(): Boolean = !locked
class Alarm:
def ring(): String = "ring"
override def toString(): String = "alarm"
class Banner(n: String) extends Planner(n):
def plan(stops: JList[String]): String = "p"
override def banner(): String = "b"
@main def run(): Unit =
val reg = Registry()
println(reg.size) // Java method: ok
println(reg.toString.length) // Object method: ok
val l = Lock(false)
println(l.open) // Scala override of Java method: ok
println(Banner("x").banner) // Scala override of Java method: ok
val a = Alarm()
println(a.toString) // overrides Object.toString: ok
println(a.ring) // plain Scala method: error
-- [E100] Syntax Error: A.scala:25:12
25 | println(a.ring)
| ^^^^^^
| method ring in class Alarm must be called with () argument
|
| longer explanation available when compiling with `-explain`
1 error found
With that last line removed, the program printed 0, 27, true, b and alarm. The exemption means Java-style calls
such as list.size and obj.hashCode keep working. See the
Scala 3 reference on auto-application
and Methods & Parameters.
Checked exceptions and varargs
Scala has no checked exceptions. A Java method that throws IOException can be called without try/catch, and the
exception simply propagates when it is thrown. This is why a Scala caller must read the Javadoc to know what can fail;
see Exceptions, boundary & Early Exit and
Either & Try for converting thrown exceptions into values.
// Reader.java
package trailhead;
import java.io.IOException;
public class Reader {
public String read(String path) throws IOException {
if (path.isEmpty()) throw new IOException("empty path");
return "data:" + path;
}
}
//> using scala 3.10.0
import trailhead.*
import scala.util.Try
@main def run(): Unit =
val r = Reader()
println(r.read("f")) // no try needed
try r.read("") catch case e: java.io.IOException => println("caught " + e.getMessage)
println(Try(r.read(""))) // turn the exception into a value
println(r.read("")) // not caught: propagates
data:f
caught empty path
Failure(java.io.IOException: empty path)
Exception in thread "main" java.io.IOException: empty path
at trailhead.Reader.read(Reader.java:7)
at T$package$.run(T.scala:10)
at run.main(T.scala:5)
Java varargs (String...) take zero or more arguments, or a splice. Both Registry.describe("-", parts*) with a
List[String] and parts.toArray* compiled and printed a-b in a separate run: Scala converts a Seq to an array for a
Java varargs call.
Virtual threads and other new JDK APIs
Anything in the JDK is available the moment the JDK supports it, because Scala compiles against the JDK class path and
needs no binding code. JDK 21 introduced virtual threads, and they work from Scala 3 with lambdas as Runnable and
Callable. Note the type ascription that gives the lambdas a Callable type when they are placed in a List. Scala’s own
Using.resource closes the executor, which implements AutoCloseable. See
Virtual Threads for the Java side.
//> using scala 3.10.0
import java.util.concurrent.{Callable, Executors}
import scala.jdk.CollectionConverters.*
import scala.util.Using
@main def run(): Unit =
val tasks: List[Callable[String]] = List("a", "b", "c").map(s => (() => s.toUpperCase): Callable[String])
Using.resource(Executors.newVirtualThreadPerTaskExecutor()) { ex =>
val results = ex.invokeAll(tasks.asJava).asScala.map(_.get()).toList
println(results)
}
val t = Thread.ofVirtual().start(() => println("virtual: " + Thread.currentThread().isVirtual))
t.join()
val r: Runnable = () => println("runnable")
r.run()
println(String.format("%s has %d trails", "Pico", 3))
println("%s has %d trails".format("Pico", 3))
List(A, B, C)
virtual: true
runnable
Pico has 3 trails
Pico has 3 trails
Scala’s own concurrency types (Future, see Futures & Promises) run on
Java executors, so the two models mix, and for a Kotlin comparison see
Kotlin Coroutines Basics.
Pitfalls
-
A
java.utilcollection is never a Scala collection: convert withasScala/asJavaand remember that the result is a live wrapper unless you finish withtoList,toMapor another copy. -
asJavaon an immutable Scala collection gives a read-only Java view that throwsUnsupportedOperationExceptionon writes. -
Intis notComparable[Int]andList[Int]is notjava.util.List[Integer]: bounded Java generics needInteger. -
A lambda needs an expected type to be converted; an overloaded generic method may need explicit type arguments.
-
Java
nullis not tracked by the type, with or without-Yexplicit-nulls; useOption(...)at the boundary. -
Mutable Java static fields should not be updated with a compound assignment; see the crash above.
-
Java checked exceptions are not checked in Scala.
-
Java methods with no parameters can be called without parentheses, but Scala methods declared with
()cannot.
Scala 3.9 LTS versus 3.10
-
The converters
CollectionConverters,OptionConvertersandStreamConverters, SAM conversion, Java annotation syntax, enum and sealed matching,-Yexplicit-nullsand auto-application exemption for Java methods are the same on the 3.3 LTS, 3.9 LTS and 3.10. Only the 3.10.0 run was done here. -
Matching a Java record with a constructor pattern (
case Shape.Rect(w, h),case Waypoint(n, e)) is new in 3.10. On 3.9.0 and 3.3.8 the record has no extractor and the match fails to compile (Not found: wfor thew * hofarea). -
The generic signatures that Scala 3.10 writes into class files are more precise; this matters when Java calls Scala (see Calling Scala from Java) and not when Scala calls Java.
-
The
Registry.created += 1compiler crash was reproduced on 3.9.0 and 3.10.0 and not on 3.3.8. It was not checked in other situations. -
On 3.10.0 the class path of these runs contained both
scala3-library_3-3.10.0.jarandscala-library-3.10.0.jar.
Where the books are outdated
-
Older books use
scala.collection.JavaConversions, which converted implicitly. On 3.10.0import scala.collection.JavaConversions.*fails withvalue JavaConversions is not a member of collection.scala.collection.JavaConvertersstill compiles with a deprecation warning;scala.jdk.CollectionConvertershas the sameasScalaandasJavamethods and is the replacement. -
Scala 2 code calls any method with an empty parameter list without parentheses; Scala 3 allows that for Java methods only (see Auto-application of Java methods).
-
The Scala 3 Book’s section on Java interoperability covers collections and
Optional; it does not cover Java records, sealed interfaces, explicit nulls or virtual threads, which this page does.