Decompiling Traits with Scala 2.11 and 2.12
Introduction
When working with Scala, one quickly gets familiar with the notation of Traits. Traits enable powerful language features such as Subtype Polymorphism and Mixins.
Scala 2.12 (currently at preview stage M4) takes advantage of Java 8 Default Methods to generate optimized JVM byte code for traits. We’ll take a short detour of how Scala 2.11 generates byte code for traits and then look at Scala 2.12 , both compiled with Java 8.
Compiling traits with Scala 2.11
Traits in Scala allow us to provide a default implementation to interface methods:
trait X {
def helloWorld: String = "hello world"
}
Using Scala on the JVM, the compiler needs to work around the fact that Java <= v7 doesn’t allow default implementations on interfaces, which is the relative cousin of traits.
Lets see what the Scala compiler does to work around that. We’ll compile trait X
and then look at the generated byte code:
[root@localhost yuvie]# scalac X.scala
[root@localhost yuvie]# cd yuvie/
[root@localhost yuvie]# ll
total 12
-rw-r--r--. 1 root root 448 Jun 24 17:58 X.class
-rw-r--r--. 1 root root 416 Jun 24 17:58 X$class.class
Scala generated two class files for our X
trait. One called X.class
, and one called X$class.class
. Let’s look at what each of these contain:
[root@localhost yuvie]# javap -c -p X
Warning: Binary file X contains yuvie.X
Compiled from "X.scala"
public interface yuvie.X {
public abstract java.lang.String helloWorld();
}
[root@localhost yuvie]# javap -c -p X\$class.class
Compiled from "X.scala"
public abstract class yuvie.X$class {
public static java.lang.String helloWorld(yuvie.X);
Code:
0: ldc #9 // String hello world
2: areturn
public static void $init$(yuvie.X);
Code:
0: return
}
Looking at the code we can see that Scalas compiler generates:
- An interface called
X
, matching the trait declaration. This interface has a single method calledhelloWorld
without the implementation. - An abstract class called
X$class
with a statichelloWorld
method, which provides the implementation.
Lets see what happens when we extend X
with some class M
:
class M extends X
Bytecode:
[root@localhost yuvie]# javap -c -p M.class
Compiled from "M.scala"
public class yuvie.M implements yuvie.X {
public java.lang.String helloWorld();
Code:
0: aload_0
1: invokestatic #17 // Method yuvie/X$class.helloWorld:(Lyuvie/X;)Ljava/lang/String;
4: areturn
public yuvie.M();
Code:
0: aload_0
1: invokespecial #23 // Method java/lang/Object."<init>":()V
4: aload_0
5: invokestatic #27 // Method yuvie/X$class.$init$:(Lyuvie/X;)V
8: return
}
When we extend / mixin X
and want to invoke helloWorld
, a call to the abstract class X$class
is made which provides the implementation
for helloWorld
. This duo of interface and abstract class allows Scala to generate valid byte code.
Leveraging Default Methods with Scala 2.12
We just saw how Scala 2.11 deals with compiling traits, let’s see how Scala 2.12 leverages default methods. Taking the same code and compiling it again only now with Scala 2.12-M4 yields the following:
[root@localhost test-project]# cd target/scala-2.12.0-M4/classes/yuvie/
[root@localhost yuvie]# ll
total 16
-rw-rw-r--. 1 yuvali yuvali 680 Jun 24 18:17 X.class
[root@localhost yuvie]# javap -c -p X.class
Compiled from "X.scala"
public interface yuvie.X {
public java.lang.String helloWorld();
Code:
0: ldc #12 // String hello world
2: areturn
public void $init$();
Code:
0: return
}
The compiler generates a single interface
called X
which has the default implementation of the trait.
Let’s see what happens now when we extend M
with X
:
[root@localhost yuvie]# javap -c -p M.class
Compiled from "M.scala"
public class yuvie.M implements yuvie.X {
public java.lang.String helloWorld();
Code:
0: aload_0
1: invokespecial #14 // Method yuvie/X.helloWorld:()Ljava/lang/String;
4: areturn
public yuvie.M();
Code:
0: aload_0
1: invokespecial #20 // Method java/lang/Object."<init>":()V
4: aload_0
5: invokespecial #23 // Method yuvie/X.$init$:()V
8: return
}
The compiler now generates an invokespecial
instruction for the instance method created in X
, instead of invokestatic
previously for the static method
created inside X$class.class
.
Conclusion
Thanks to Java 8 and Default Method, Scala 2.12 is now able to emit less byte code for traits. My example was very simplified, and in real case scenarios this should save decent amount of bytecode generation for the Scala compiler and enables Scala to be more aligned with Java without needing to jump the hoops to make traits work.