Scala for Java, Python & JavaScript Developers
|
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. |
If you already write Java, Python or JavaScript, you know most of the ideas Scala uses: classes, lambdas, collections, generics, async code. What changes is the spelling and the defaults. This page is a translation table with working examples, one section per area, followed by the places where assuming "it works like my language" gives a compile error or, worse, a different answer. The baseline is Scala 3.10.0 (released 2026-10-12, JDK 17 or newer) Every example was executed with Scala CLI on Scala 3.10.0 and JDK 26 on 2026-10-15, and the output shown is what the run printed. The syntax used dates from Scala 3.0, so none of it is tied to a 3.10-only feature, and the 3.9 LTS line accepts the same programs, but only 3.10.0 was run here.
The examples use the trailhead scenario of this section, a toolkit that plans hikes on a network of mountain trails. The official Scala 3 Book has one page per source language, and this page follows their structure but writes its own examples: Scala for Java Developers, Scala for Python Developers and Scala for JavaScript Developers. The language sections of this site cover the other side of each comparison: Java, Python, JavaScript and Kotlin (the closest relative if you come from the JVM).
The shared picture
At the highest level the three comparisons agree on what Scala is. It is statically typed like Java, but type inference
makes it read almost like Python or JavaScript. It compiles to JVM class files like Java, so every Java library is
available, and the same source can also target JavaScript (Scala.js) or native code (Scala Native). Everything is an
object, and operators such as + are methods. Almost every construct is an expression that produces a value, and
immutability is the default style.
| Idea | Java | Python | JavaScript | Scala 3 |
|---|---|---|---|---|
Immutable binding |
|
(a convention only) |
|
|
Mutable binding |
|
|
|
|
Data carrier |
|
|
object literal or |
|
Interface or mixin |
|
|
(none; mixin functions) |
|
Enumeration |
|
|
frozen object |
|
Absent value |
|
|
|
|
Conditional value |
|
|
|
|
Multi-way branch |
|
|
|
|
Async result |
|
|
|
|
Entry point |
|
|
top-level code or module |
|
The rest of the page expands each row.
Variables, types and naming
val is a binding that cannot be reassigned (Java final, JavaScript const) and var can be reassigned (JavaScript
let). The type is inferred from the right-hand side, and you add it only where the compiler needs it or you want it
documented: on public method results and parameters. Naming follows Java and JavaScript (lengthKm, not Python’s
length_km), classes are UpperCamelCase, and there are no semicolons or new for most classes.
//> using scala 3.10.0
enum Difficulty:
case Easy, Moderate, Hard
case class Trail(name: String, lengthKm: Double, ascentM: Int, difficulty: Difficulty)
@main def a1(): Unit =
val name = "Pico Alto" // like final String / const
var lengthKm = 12.5 // like double / let (reassignable)
lengthKm += 1.5 // no ++ and -- in Scala
val trail = Trail(name, lengthKm, 940, Difficulty.Hard) // no `new` needed
val renamed = trail.copy(name = "Pico Bajo")
println(trail)
println(renamed)
println(f"${trail.name}%s: ${trail.lengthKm}%.1f km, ${trail.ascentM} m")
assert(trail == Trail("Pico Alto", 14.0, 940, Difficulty.Hard)) // == compares fields
assert(trail != renamed)
Trail(Pico Alto,14.0,940,Hard)
Trail(Pico Bajo,14.0,940,Hard)
Pico Alto: 14,0 km, 940 m
The last line shows a trap for anyone used to JavaScript template strings or Python f-strings. Scala has three
interpolators: s"..." (any expression with ${...}), f"..." (printf-style formats, checked at compile time) and
raw"...". The f interpolator formats through Java’s String.format, so it uses the JVM’s default locale, and the
machine that produced this output uses a decimal comma. Pass an explicit Locale with "%.1f".formatLocal(Locale.ROOT, x)
when the output must not depend on the machine. Strings are always double-quoted: 'a' is a Char, and multi-line text
uses triple quotes plus stripMargin (the equivalent of a Java text block or a Python triple-quoted string). See
Values, Variables & Literals and
Strings & Interpolation.
Source: Scala 3 Book: Variables and data types and Scala for JavaScript Developers, variables and types.
Types are the same family as in Java, with two differences. There is no split between primitives and wrappers: Int,
Double, Boolean and the others are ordinary classes that the compiler represents as JVM primitives when it can, so
3.max(9) is valid and Int can be a generic argument without boxing syntax. Generics use square brackets, List[Int],
because < and > are ordinary operator names. Numeric types do not have ++ or --, and an Int is 32-bit exactly
as in Java, while the JavaScript number corresponds to Double and the JavaScript bigint to BigInt.
Classes, records and traits
A Scala class declares its fields in the constructor: class Hut(val name: String, val beds: Int) is a Java class with
two fields, getters and a constructor. val makes a read-only field, var a read-write one, and no keyword makes a plain
constructor parameter. A case class adds equals, hashCode, toString, copy and a pattern-matching extractor, and
it is the equivalent of a Java record or a frozen Python dataclass (the first example already used copy).
Case classes and
enums cover both in depth.
Scala has no interface. A trait is an interface whose members can have implementations and state, and a class can
extend several traits. The compiler linearizes them, which is why Java’s diamond problem becomes a defined order instead
of a compile error. A class lists its parents after extends, separated by commas.
//> using scala 3.10.0
trait Describable:
def describe: String
trait Timed:
def minutes: Int
def label: String = s"$minutes min"
class Hut(val name: String, val beds: Int) extends Describable, Timed:
def minutes = 45
def describe = s"$name ($beds beds), $label"
@main def a2(): Unit =
val h = Hut("Refugio", 30)
println(h.describe)
// an anonymous mix-in at the call site, like a Java anonymous class
val extra = new Describable with Timed:
def minutes = 5
def describe = s"Water point, $label"
println(extra.describe)
Refugio (30 beds), 45 min
Water point, 5 min
Source: Scala 3 Book: Domain modeling, tools and Scala for Java Developers, interfaces, traits and inheritance.
Three defaults differ from Java. A member with no modifier is public, not package-private. A class meant for
extension is declared open class, which documents the "design for inheritance or forbid it" rule
(reference: open classes). The
compiler does not enforce it yet: on 3.10.0, extending a class without open from another file compiled with no
message, and only -source:future adds a feature warning (silenced by import scala.language.adhocExtensions). And Scala’s
protected is narrower than Java’s, because it does not grant access to other classes in the same package. See
Traits & Mixins and
Annotations & Visibility.
A Python developer will notice that the indentation after the colon is the block. Scala 3 allows either braces or significant indentation, and you can mix them across files; the section on optional braces has the exact rules and the compiler flags that rewrite code from one style to the other.
Expressions everywhere
The most useful mental shift is that Scala has almost no statements. if, match, try and for ... yield each
produce a value, so you assign their result instead of declaring a variable first and mutating it. if takes then
instead of parentheses, match replaces switch, and for loops use <- generators.
//> using scala 3.10.0
enum Difficulty:
case Easy, Moderate, Hard
case Alpine(grade: Int)
def parse(s: String): Option[Int] =
try Some(s.toInt) catch case _: NumberFormatException => None
@main def a3(): Unit =
val grade = 3
// if, match and try are expressions: each one produces a value
val label = if grade > 2 then "steep" else "gentle"
val d = grade match
case 0 => Difficulty.Easy
case 1 | 2 => Difficulty.Moderate
case g if g > 5 => Difficulty.Hard
case g => Difficulty.Alpine(g)
val n = try "12x".toInt catch case _: NumberFormatException => -1
println(s"$label $d $n")
// for ... yield builds a collection; for ... do just loops
val squares = for i <- 1 to 4 yield i * i
println(squares)
for i <- 1 to 3; j <- 'a' to 'b' do print(s"$i$j ")
println()
println(parse("42"))
println(parse("x"))
println(parse("42").map(_ + 1).getOrElse(0))
assert(label == "steep" && n == -1)
steep Alpine(3) -1
Vector(1, 4, 9, 16)
1a 1b 2a 2b 3a 3b
Some(42)
None
43
Compared with the other languages:
-
Java.
switchfalls through unless you writebreak, and the older form only matches constants. A Scalamatchnever falls through, matches types, tuples and case-class shapes, takes guards, and on anenumorsealedtype the compiler warns about missing cases. Java 21 pattern matching closes much of that gap (see Java pattern matching). The ternary operator does not exist, becauseif a then b else cis already an expression. -
Python.
matchappeared in Python 3.10 and is a statement. A comprehension[i * 10 for i in xs if i % 2 == 0]isfor i <- xs if i % 2 == 0 yield i * 10, and a Python generator expression corresponds to aLazyList. -
JavaScript.
===and!==do not exist:==and!=are the only comparisons, and they never coerce between unrelated types.
Pattern Matching and For Comprehensions go much further.
Collections
Scala’s default collections are immutable and persistent: List, Vector, Set and Map never change, and
operations such as :+, + or updated return a new collection that shares structure with the old one. Java’s
List.of also gives an immutable list, but it has no such operations. The mutable classes live in
scala.collection.mutable. The Python list and the JavaScript array are mutable, so the nearest match is ArrayBuffer
or an Array, not List.
//> using scala 3.10.0
import scala.collection.mutable.ArrayBuffer
@main def a4(): Unit =
val lengths = List(12.5, 8.0, 21.3) // immutable, like List.of in Java
val more = lengths :+ 5.5 // a NEW list; `lengths` is unchanged
println(lengths)
println(more)
val byName = Map("Pico" -> 940, "Lago" -> 120) // immutable Map
println(byName("Pico")) // parentheses, not [] or .get
println(byName.get("Cumbre")) // Option, never null
val buf = ArrayBuffer(1, 2, 3) // the mutable list (Python list, JS array)
buf(0) = 10 // update with parentheses
buf += 4
println(buf)
val (short, long) = lengths.partition(_ < 15)
println(s"$short $long")
println(lengths.map(_ * 2).filter(_ > 20).sum)
assert(lengths.size == 3 && more.size == 4)
List(12.5, 8.0, 21.3)
List(12.5, 8.0, 21.3, 5.5)
940
None
ArrayBuffer(10, 2, 3, 4)
List(12.5, 8.0) List(21.3)
67.6
Equivalents to keep at hand:
| Java | Python | JavaScript | Scala |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Two differences deserve emphasis. First, the methods are defined on the collection itself (xs.map(...)), eagerly,
without a .stream() step as in Java; use .view or LazyList for laziness. Second, a Scala Array[Int] is the real
JVM int[], so it interoperates with Java arrays, but it is rarely the right default. Java collections are converted with
scala.jdk.CollectionConverters
(Using Java from Scala), and the whole library is covered from
Collections Overview. The Java side is in
Collections Framework and
Streams & Collectors.
Functions and lambdas
A method is declared with def, and a lambda is written (x: Int) => x * 2. Most of the time you pass a lambda where
the parameter type is already known, so the type is dropped (xs.map(x => x * 2)), and for one-use parameters the
placeholder form xs.map(_ * 2) is idiomatic. A method used where a function is expected is converted automatically
(eta-expansion), so you can write xs.map(parse) where Java would write Main::parse. Unlike Java, a lambda is not
restricted to effectively-final captured variables, and unlike JavaScript, this is not rebound inside it.
Lambdas and method references shows the Java
equivalents.
Parameters may have default values and be passed by name (plan(trail, speed = 4.0)), a method can have several
parameter lists, and a block can be passed as the last argument, which is how Scala libraries build their own control
structures. A Python developer can think of these as the function-call features they already use, and a Java developer
gets the equivalent of overloaded methods and builders for free. The details are in
Methods & Parameters.
Null, absence and errors
Idiomatic Scala does not use null. A value that may be missing has the type Option[A], which is either Some(a) or
None; a computation that may fail has the type Either[E, A] or Try[A]. The compiler makes you handle the empty case
before using the value, so the Java NullPointerException, the Python AttributeError: 'NoneType' and the JavaScript
Cannot read properties of undefined become type errors or explicit defaults.
//> using scala 3.10.0
import java.util.Optional
@main def n1(): Unit =
val fromJava: String | Null = System.getProperty("trailhead.missing") // a Java method may return null
val safe = Option(fromJava).getOrElse("default") // wrap at the boundary
println(safe)
val opt: Optional[String] = Optional.empty()
println(opt.isPresent)
val hut: Option[String] = None
println(hut.fold("no hut")(h => s"hut: $h"))
default
false
no hut
null still exists because Java returns it, and Option(x) turns a possibly-null reference into an Option at the
boundary. By default val s: String = null compiles. The compiler flag -Yexplicit-nulls (listed under the experimental
features of the reference) removes Null from the reference types, so the same line becomes an error, as this run shows:
[error] ./n2.scala:4:19
[error] Found: Null
[error] Required: String
[error] Note that implicit conversions were not tried because the result of an implicit conversion
[error] must be more specific than String
[error] val s: String = null
[error] ^^^^
Exceptions also differ. Scala has no checked exceptions: a method never declares throws, and the compiler does not
force a catch. The catch block is a pattern match (catch case e: IOException => ...), and the idiom for expected
failures is to return Option, Either or Try rather than throw. When Java code must see the exception type, annotate
the method with @throws. See Option & Null Safety and
Exceptions, boundary & Early Exit.
Concurrency and asynchronous code
The standard asynchronous type is scala.concurrent.Future, with Promise as its write side. A Future starts running as
soon as it is created, on an ExecutionContext (a thread pool). Transformations use map, flatMap and recover, and
a for comprehension chains them the way await chains async calls. There are no async/await keywords in the
language, so the for comprehension fills that role.
//> using scala 3.10.0
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
def forecast(hut: String): Future[Int] = Future { hut.length * 3 } // runs on a pool thread
@main def a5(): Unit =
val f = for
a <- forecast("Refugio")
b <- forecast("Pico")
yield a + b // like thenCompose / await / .then
val recovered = Future.failed[Int](RuntimeException("no signal")).recover { case _ => -1 }
println(Await.result(f, 2.seconds))
println(Await.result(recovered, 2.seconds))
33
-1
| Need | Java | Python | JavaScript | Scala |
|---|---|---|---|---|
Async value |
|
coroutine, |
|
|
Chain steps |
|
|
|
|
Recover |
|
|
|
|
Where it runs |
common pool or an |
event loop |
event loop |
an implicit |
Blocking |
|
|
(not possible) |
|
Python and JavaScript run on one event-loop thread, so shared mutable state is safe between awaits. A Scala Future
runs on real threads, so, as in Java, mutable state shared between futures needs synchronization or, better, an immutable
design. Threads on JDK 21 or newer, including virtual threads
(Virtual Threads), are available to Scala code through the Java
APIs. Parallel collections and the larger concurrency libraries are covered in
Futures & Promises and
Parallel Collections & Concurrency Libraries.
Source: Scala 3 Book: Concurrency and Futures and Promises.
Tooling
You do not need a heavy toolchain to start. Scala CLI compiles and runs a single file (every example above was run as
scala run file.scala), and sbt, Mill, Maven or Gradle handle multi-module projects. Packages come from Maven Central,
the same repository Java uses, so your existing Maven knowledge transfers.
| Task | Java | Python | JavaScript | Scala |
|---|---|---|---|---|
Run a single file |
|
|
|
|
Interactive shell |
|
|
|
|
Build and dependencies |
Maven, Gradle |
|
|
sbt, Mill, Scala CLI, Maven, Gradle |
Tests |
JUnit |
|
Jest |
MUnit, ScalaTest, ScalaCheck |
Formatter |
|
|
Prettier |
Scalafmt |
IDE |
IntelliJ IDEA, VS Code |
PyCharm, VS Code |
VS Code, WebStorm |
IntelliJ IDEA with the Scala plugin, VS Code with Metals |
See Installing Scala, Scala CLI and sbt & Build Tools. JavaScript developers can also target the browser and Node with Scala.js; the official Scala.js site has tutorials aimed at JavaScript programmers, and the Scala 3 Book page for JavaScript developers includes a short DOM sample.
Valid Java is not Scala
Scala looks familiar enough that it is tempting to type Java and hope. The following surprises cover most first-week errors. The compiler messages are real, from Scala 3.10.0.
Declarations and operators
A declaration is val name: Type = value, with the type after the name. Java-style int count = 0; is rejected:
[error] ./e1.scala:3:7
[error] end of statement expected but identifier found
[error] int count = 0;
[error] ^^^^^
[error] ./e1.scala:3:3
[error] Not found: int - did you mean Int?
[error] int count = 0;
[error] ^^^
Several habits fail in the same way:
[error] ./e2.scala:4:4
[error] end of statement expected but identifier found
[error] i++
[error] ^^
[error] ./e2.scala:6:11
[error] method apply in trait LinearSeqOps does not take type parameters
[error] println(xs[0])
[error] ^^^^^
and, for reassigning a val and calling a parameterless method with parentheses:
[error] ./e3.scala:4:3
[error] Reassignment to val n
[error] n = 6
[error] ^^^^^
[error] ./e3.scala:6:11
[error] method length in class List does not take parameters
[error] println(xs.length())
[error] ^^^^^^^^^
So: use i = 1` instead of `i+, index with xs(0) because indexing is an apply method call, and call methods
declared without a parameter list, such as length, without parentheses. Scala 3 enforces this for methods defined in
Scala; methods that come from Java may be called either way.
Equality, expressions and numbers
//> using scala 3.10.0
class Trail(val name: String)
@main def e4(): Unit =
val a = Trail("Pico")
val b = Trail("Pico")
println(a == b)
println(a eq b)
println("Pico" == String("Pico"))
println(List(1, 2) == Vector(1, 2))
val x = if false then 1
println(x)
val big = 2147483647
println(big + 1)
println(7 / 2)
println(-7 % 3)
println(1 == 1L)
[warn] ./e4.scala:10:25
[warn] Discarded non-Unit value of type Int. Add `: Unit` to discard silently.
[warn] val x = if false then 1
[warn] ^
false
false
true
true
()
-2147483648
3
-1
true
Reading the output line by line:
-
a == bisfalsebecauseTrailis a plain class that does not overrideequals. In Scala==isequals(null-safe, sonull == xdoes not throw), andeqis reference identity, the Java==on objects. That is why"Pico" == String("Pico")istrue, whereas Java’s==would compare references. Acase classgenerates a structuralequals, and two collections of different types but equal contents (List(1, 2) == Vector(1, 2)) are equal too. Multiversal equality can forbid meaningless comparisons at compile time; see Multiversal Equality. -
An
ifwithoutelseis an expression whose type isUnit, the value(), when the condition is false. The compiler warns that1was thrown away. Always give anelsewhen you use the value. -
Intarithmetic is exactly Java’s: overflow wraps (2147483647 + 1is-2147483648),/truncates and%keeps the sign of the dividend. Scala does not widen toDoublefor you, and comparing1 == 1Listruebecause the numeric values are equal.
The next program shows the remaining differences from Java: no throws clause, object instead of static, and an
Int that is both a JVM int and an object.
//> using scala 3.10.0
import java.io.IOException
def read(): String = throw IOException("disk") // no `throws` clause required
object Util: // replaces static members
val Max = 10
def twice(i: Int) = i * 2
@main def e5(): Unit =
val r = try read() catch case e: IOException => s"caught ${e.getMessage}"
println(r)
println(Util.twice(Util.Max))
val i: Int = 3
val o: Any = i // boxing is hidden: Int is also an object
println(i.toString + " " + i.max(9) + " " + o.isInstanceOf[Int])
println(classOf[Int])
caught disk
20
3 9 true
int
-
No
static. A singletonobjectholds what Java puts instaticmembers, and a class and an object with the same name in the same file are companions that can see each other’s private members. Top-leveldefandvalare also allowed, so a file does not need to be wrapped in a class, and its name does not have to match anything it defines. -
No checked exceptions.
read()throws anIOExceptionwith no declaration, and the caller catches it with a pattern. -
No primitive/wrapper split.
3.max(9)works,Anyaccepts anIntwithout ceremony, andclassOf[Int]is the JVMintclass. -
Operators are methods.
1 + 2is1.+(2), and any method with one argument can be used infix, which is how libraries build DSLs. -
Wildcards are
?. A JavaList<?>is writtenList[?], and a JavaList<? extends T>isList[? <: T], or better a covariant type declared once withList[+A]on the definition (see Variance). -
mainis not special syntax.@main def run(): Unitis the entry point, and arguments can be typed parameters that the compiler parses for you.
Where the books and older material are outdated
-
The Scala 3 Book comparison for Java developers says that some Scala concepts have no equivalent in "Java 11" and compares case classes with "records in Java 14". Records are final in Java 16, sealed types in 17 and pattern matching for
switchin 21, so those comparisons are now closer than the page suggests. The Java section here is the reference for the current Java side. -
The JavaScript page shows a jQuery-based Scala.js sample. It illustrates syntax only; the current Scala.js documentation is the place to start a new project.
-
Scala 2-era books and tutorials teach braces,
implicitparameters and classes,sealed traitplus case classes instead ofenum, andimport scala.collection._instead ofimport scala.collection.*. Much of that is still accepted by Scala 3, some of it only with warnings or migration flags (see Incompatibilities & Dropped Features), but the idiomatic Scala 3 forms on this page are what new code should use. -
Python comparisons written before Python 3.10 say that only Scala has pattern matching. Both languages have it now, though Scala’s
matchis an expression. -
The comparison pages were written for the first Scala 3 releases and do not mention later additions such as named tuples; Release History & What’s New lists them.
-
The books used for this section are listed in the bibliography, with the editions they cover.