One of the main strengths of Scala is that you can parasite the ecosystem of libraries of Java and infrastructure and tooling of the JVM1. Of course, this is not a free lunch as some gotchas of Scala come from that permeability to Java (like nullability, universal equality and the like).
Interop, or the ability or use Java code from Scala and vice versa, works reasonably well in the Scala-calling-Java direction and not so well in the opposite direction and that is a conscious design decision of the language. To understand why, we need to deep dive in how Scala and Java share the JVM.
Name mangling
In order to execute in the JVM, scalac
is forced to compile to the same
bytecode and artefacts that javac
. Scala concepts that can be mapped
directly to Java are no surprise. For instance, you are going to see something
like def square(n: Int): Int
as int square(int n)
from Java. But, what
about concepts that cannot be cleanly mapped?
This little snipped is going to be compiled into two classes: an unsurprising
Point.class
and a more interesting Point$.class
. Scala’s compiler needs to
generate a class for the companion object and it’s using the reserved
character $
2 to avoid colliding with user defined names. This process is
called name mangling and is at least as older as compilers are.
Using javap we can take a look at the class
files:
After looking at this it is evident3 how to use the companion object from Java.
Leaky abstractions are going to leak
When mangling is a leaky abstraction? If you limit yourself to calling Java code from Scala you need to consider mangling almost never. Almost.
Recently I got bitten by this leaky abstraction4 while using Mockito from Scala. For the sake of simplicity (and to avoid exposing my employer’s IP), let’s see that we have this service interface:
We want to unit test this class that gets one instance of the service injected:
Note that we have two cases to test: when tags are passed explicitly and when a default argument is used. Let’s say we write two test cases to cover both cases:
What do we get when running them? The first passes but the second produces a null pointer exception out of the blue.
[info] Mocking service
[info] - should work when all arguments are explicitly passed
[info] - should work when using default arguments *** FAILED ***
[info] java.lang.NullPointerException:
...
This kind of problem can be a huge time hole as the source of the exception is
literally invisible, in code we cannot see. Let’s use javap
on the
service (slightly cleaned up for readability):
Wow, default arguments become new “invisible” methods with a mangled name in
the form <method_name>$default$<argument_pos>$
5. And we are not telling
Mockito to what to return in that case… so we get null
.
Workarounds and solutions
There are several unsatisfactory workarounds we can use to overcome this gotcha:
- We can avoid using default arguments in our production code. This is difficult to enforce and frankly depressing.
- We can expect
null
s when mocking the main method of the service. Be prepared to write an apologizing comment in your test.
- We can mock the default argument… from Java! I’m mentioning it just for completeness (makes my eyes bleed!).
However, the real solution to this problem6 is to use a mocking library designed for Scala rather than Java. With ScalaMock both test cases will work seamlessly:
Check out this repo if you want to run this and the previous sample snippets.
In conclusion, it’s wonderful to piggy-back on the JVM ecosystem but sometimes we got bitten by leaky abstractions. It’s in those cases when knowing what’s under the hood can save you hours of frustration.
-
You can also parasite the Javascript ecosystem with scala.js or fly by yourself with scala native ↩
-
You can actually try to use it in your class and variable names but it might interfere with mangling. We can say that it is “reserved” in the spirit of JavaScript’s
undefined
↩ -
Evident but “ugly as a bare foot” as we say in Spanish ↩
-
Almost all abstractions are leaky. Almost. ↩
-
These names can grow so large when you have lambdas within lambdas that you might hit the path length limit of Linux ↩
-
Apart from not using mocks… ↩