My post “The High Cost of AnyVal subclasses” covers most of the ground of how, in many practical cases, extends AnyVal
actually allocates more instances of the class, rather than fewer, compared to a normal class, with many bytecode examples to demonstrate.
There is one appropriate use case for extends AnyVal
: writing extension methods.
// the new way
implicit final class Hello(private val self: Int) extends AnyVal {
def hello = self + 42
}
// the old-fashioned way (often more flexible)
final class Goodbye(private val self: Int) extends AnyVal {
def goodBye = self - 42
}
implicit def Goodbye(self: Int): Goodbye = new Goodbye(self)
// trying them out
0.hello.goodBye
The last line desugars as
Goodbye(Hello(0).hello).goodBye
At the bytecode level, both Goodbye
and Hello
merely return their int
argument. The hello
method compiles as
public final int hello$extension(int);
Code:
0: iload_1
1: bipush 42
3: iadd
4: ireturn
And it is similar for the goodBye
method.
Informally, the key to a good use case for extends AnyVal
is lifetime. In the desugared code, a Hello
is created, then immediately discarded in favor of the call to its hello
method, whose result is not an AnyVal
subclass. So if you use extends AnyVal
only in this way, you will not fall into unexpected allocation traps.
Otherwise, you almost certainly will end up allocating in unexpected ways. If you want a true unboxed newtype, use the existential technique I describe in the original “High Cost” article, or simply use Scalaz tags.
Tested with Scala 2.12.11.