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.
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
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
$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
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.
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
<method_name>$default$<argument_pos>$5. And we are not telling
Mockito to what to return in that case… so we get
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
nulls 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!).
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.
Evident but “ugly as a bare foot” as we say in Spanish ↩
Almost all abstractions are leaky. Almost. ↩
Apart from not using mocks… ↩