Do you know what happens after you compile? Let’s take a look at what the Scala compiler tells us, and what scalap and javap can reveal about .class files.

Creating a Minimal Example with sbt

First, we’ll need something very small to compile, to still understand all of the output.

build.sbt
name := "scala-start"
version := "1"
scalaVersion := "2.13.5"
scalacOptions := Seq("-target:11")
project/build.properties
sbt.version=1.4.9
src/main/scala/Main.scala
object Main {
    def main(args: Array[String]) = {
        println("Hello.")
    }
}

run

$ sbt package

The result is in target/scala-2.13/scala-start_2.13-1.jar. You can look at/unpack the contents via

$ jar -xvf target/scala-2.13/scala-start_2.13-1.jar             
 inflated: META-INF/MANIFEST.MF
 inflated: Main$.class
 inflated: Main.class

To run it via java, you need to add both the jar and the scala-library.jar to the classpath. If you use sbt stage, it gets copied next to the output, but you can also reference the one that is already present on your machine (in this example installed via Homebrew):

$ java -cp "/usr/local/Cellar/scala/2.13.5/libexec/lib/*:." Main  
Hello.

Creating a Really Minimal Example (without sbt)

To go even more minimal, you can work with a single scala-file:

Main.scala
object Main {
    def main(args: Array[String]) = {
        println("Hello.")
    }
}

Compile and run it via

scala Main.scala 
Hello.

or

$ scalac Main.scala 
$ scala Main       
Hello.

The second version will create two class files (that you also found inside the jar earlier):

  • Main.class
  • Main$.class (the object)

Running scalap and javap

Now that you have class-files, you can take them apart with

$ scalap Main Main$
object Main extends scala.AnyRef {
  def this() = { /* compiled code */ }
  def main(args: scala.Array[scala.Predef.String]): scala.Unit = { /* compiled code */ }
}

package Main$;
final class Main$ extends scala.AnyRef {
  def this(): scala.Unit;
  def main(scala.Array[java.lang.String]): scala.Unit;
}
object Main$ {
  final val MODULE$: Main$;
}

You can also use javap, since it’s just class-files:

$ javap Main Main$
Compiled from "Main.scala"
public final class Main {
  public static void main(java.lang.String[]);
}
Compiled from "Main.scala"
public final class Main$ {
  public static final Main$ MODULE$;
  public static {};
  public void main(java.lang.String[]);
}

Excursion: Scala 3 / dotty

If you compile with the Scala 3 compiler (dotty), the result of scalap will be slightly different:

$ /usr/local/Cellar/dotty/3.0.0-RC1/bin/scalac Main.scala
$ scalap Main Main$
package Main;
final class Main extends scala.AnyRef {
}
object Main {
  def main(scala.Array[java.lang.String]): scala.Unit;
}
package Main$;
final class Main$ extends scala.AnyRef with java.io.Serializable {
  def main(scala.Array[java.lang.String]): scala.Unit;
  def writeReplace(): scala.Any;
  def this(): scala.Unit;
}
object Main$ {
  final val MODULE$: Main$;
}

…while the javap output stays almost the same (except for implements java.io.Serializable).

$ javap Main Main$
Compiled from "Main.scala"
public final class Main {
  public static void main(java.lang.String[]);
}
Compiled from "Main.scala"
public final class Main$ implements java.io.Serializable {
  public static final Main$ MODULE$;
  public static {};
  public void main(java.lang.String[]);
}

Printing just before JVM Bytecode Generation

The Scala compiler allows you to get a preview with the print flag:

$ scalac -print:true Main.scala
[[syntax trees at end of                   cleanup]] // Main.scala
package <empty> {
  object Main extends Object {
    def main(args: Array[String]): Unit = scala.Predef.println("Hello.");
    def <init>(): Main.type = {
      Main.super.<init>();
      ()
    }
  }
}

The “cleanup” refers to the phase of the compiler. You can see them with

$ scalac -Vphases // or -Xshow-phases         
    phase name  id  description
    ----------  --  -----------
        parser   1  parse source into ASTs, perform simple desugaring
         namer   2  resolve names, attach symbols to named trees
packageobjects   3  load package objects
         typer   4  the meat and potatoes: type the trees
superaccessors   5  add super accessors in traits and nested classes
    extmethods   6  add extension methods for inline classes
       pickler   7  serialize symbol tables
     refchecks   8  reference/override checking, translate nested objects
        patmat   9  translate match expressions
       uncurry  10  uncurry, translate function values to anonymous classes
        fields  11  synthesize accessors and fields, add bitmaps for lazy vals
     tailcalls  12  replace tail calls by jumps
    specialize  13  @specialized-driven class and method specialization
 explicitouter  14  this refs to outer pointers
       erasure  15  erase types, add interfaces for traits
   posterasure  16  clean up erased inline classes
    lambdalift  17  move nested functions to top level
  constructors  18  move field definitions into constructors
       flatten  19  eliminate inner classes
         mixin  20  mixin composition
       cleanup  21  platform-specific cleanups, generate reflective calls
    delambdafy  22  remove lambdas
           jvm  23  generate JVM bytecode
      terminal  24  the last phase during a compilation run

You can also get the output after a specific compiler phase with

$ scalac -Vprint:24 Main.scala
[[syntax trees at end of                   pickler]] // Main.scala
package <empty> {
  object Main extends scala.AnyRef {
    def <init>(): Main.type = {
      Main.super.<init>();
      ()
    };
    def main(args: Array[String]): Unit = scala.Predef.println("Hello.")
  }
}

You can also specify ranges:

$ scalac -Vprint:1-5 Main.scala

or all the steps with underscore:

$ scalac -Vprint:_ Main.scala

You can find more information about the compiler-flags

The Bytecode

The final step is to see the actual bytecode:

$ javap -private -c -s -l Main
Compiled from "Main.scala"
public final class Main {
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    Code:
       0: getstatic     #17                 // Field Main$.MODULE$:LMain$;
       3: aload_0
       4: invokevirtual #19                 // Method Main$.main:([Ljava/lang/String;)V
       7: return
}

You can also use a tool such as jclasslib to inspect the bytecode.

The Java Code

Okay, Bytecode was not the final step, you can take one further and convert it back to Java code! I use cfr for that, and add the scala-library to the classpath:

$ java -jar cfr.jar Main Main$ --extraclasspath /usr/local/Cellar/scala/2.13.5/libexec/lib/scala-library.jar
/*
 * Decompiled with CFR 0.151.
 */
import scala.reflect.ScalaSignature;

@ScalaSignature(bytes="\u0006\u0005... (this is 'pickled scala')")
public final class Main {
    public static void main(String[] stringArray) {
        Main$.MODULE$.main(stringArray);
    }
}
/*
 * Decompiled with CFR 0.151.
 */
import scala.Predef$;

public final class Main$ {
    public static final Main$ MODULE$ = new Main$();

    public void main(String[] args) {
        Predef$.MODULE$.println("Hello.");
    }

    private Main$() {
    }
}

Errors you can run into

NoClassDefFoundError: scala/Predef$

Exception in thread "main" java.lang.NoClassDefFoundError: scala/Predef$
	at Main$.main(Main.scala:3)
	at Main.main(Main.scala)
Caused by: java.lang.ClassNotFoundException: scala.Predef$
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)

=> your jar is missing the scala-library. Either package it (using sbt stage for example) or add it to the classpath. Either way you probably want to use

$ java -cp "path/to/scala/lib/*:path/to/your/jar" NameOfTheMainClass

for example

$ java -cp "/usr/local/Cellar/scala/2.13.5/libexec/lib/*:target/scala-2.13/scala-start_2.13-1.jar" Main

NoClassDefFoundError: scala/Function0

Error: Unable to initialize main class Main
Caused by: java.lang.NoClassDefFoundError: scala/Function0

This is the same as above, but you used the

object Main extends App {
  println("Hello.")
}

code, which gives a slightly nicer error message. The solution is the same: put the scala-library and your jar in the classpath.

No such file or class on classpath: Main

$ scala Main       
No such file or class on classpath: Main

The scala command can do two things:

  • run something from a *.class file if you provide a name
  • compile and run something if you provide a file name

Solution: Either run $ scala Main.scala or $ scalac Main.scala first and then $ scala Main.

class/object Main not found.

$ scalap Main      
class/object Main not found.
$ scalap Main.scala 
class/object Main.scala not found.

The scalap tool actually needs compiled classes. Run $ scalac Main.scala first.