Knowing what the compiler emits explains boxing, binary compatibility issues and profiler output.
scalac compiles in two outputs: JVM class files and TASTy files (.tasty), a typed abstract syntax tree of every definition. TASTy lets later compilations see full Scala types (including those erased in bytecode), lets Scala 2.13 consume Scala 3 libraries through its TASTy reader, and powers macros and tools like documentation and IDE support.
How constructs map to bytecode:
- A class is a JVM class; a
valfield is a private field plus an accessor method. - An
objectbecomes a className$with a staticMODULE$instance, plus static forwarders onNamefor Java callers. - A trait becomes a JVM interface with default methods; fields in traits are implemented in the classes that mix them in.
- Lambdas compile via
invokedynamicandLambdaMetafactory, like Java's. - Top-level definitions go into a synthetic
File$packageobject. lazy valuses a thread-safe initialisation scheme with extra checks on each access.
Erasure removes type arguments, so generic code works on Object: an Int in a List[Int] or passed to a generic method is boxed to java.lang.Integer. Hot numeric code should use Array[Int] (a real int[]) or specialised structures; opaque types and inline methods avoid boxing that wrappers would add.
Binary compatibility is subtle: adding a parameter with a default or a method to a trait can break already compiled code. Library authors check it with MiMa (sbt-mima-plugin). Inspect output with javap -c -p on the class files.
Example
// Wallet.scala, compile with: scala compile Wallet.scala
// then: javap -p -cp <output dir> Wallet Wallet$ Shape
trait Shape:
def area: Double
def describe: String = s"area $area" // -> interface default method
class Wallet(val owner: String): // -> private field + owner() accessor
lazy val id: Int = owner.hashCode // -> guarded lazy initialisation
object Wallet: // -> class Wallet$ with MODULE$
def empty = Wallet("nobody") // + static forwarder Wallet.empty()
def sumAll(xs: List[Int]): Int = xs.sum // top level -> Wallet$package
def sumArray(xs: Array[Int]): Int = // Array[Int] is a real int[]: no boxing
var t = 0; var i = 0
while i < xs.length do { t += xs(i); i += 1 }
t
@main def jvm(): Unit =
println(Wallet.empty.owner) // nobody
println(sumArray(Array(1, 2, 3))) // 6
println(classOf[Wallet].getDeclaredFields.map(_.getName).toList.contains("owner")) // trueRemember
- Objects compile to
Name$with aMODULE$singleton; traits to interfaces - TASTy files keep full types for later compilation, macros and tools
- Erasure boxes primitives in generic code; use arrays for hot numeric loops
- Check library binary compatibility with MiMa
Watch out for
- Assuming a source-compatible change to a library is also binary compatible.