If I have this value class:
class ActionId(val value: Int) extends AnyVal
Then, in all the examples below, an object will be allocated for the value class? (It'll be "boxed" — it will not simply be unwrapped to a plain 32 bit integer, right?)
A function that returns a value class — the value class escapes the scope and will hence be "boxed"?
def someFunction(): ActionId = {
...
return ActionId(123)
}
A function that returns an object with a value class member — the value class escapes the scope and will hence be "boxed"?
case class Post(id: ActionId, ...) { ... }
def someFunction(): Post = {
...
val somePost = Post(ActionId(123), ...) // ActionId will be "boxed", right?
return somePost
}
Even if the object with a value class member is not returned (doesn't really escape the scope), the value class will still be "boxed", when it is used as a member of another class (as a field in the Post
class, in this example)?
def anotherFunction() {
...
val somePost = Post(ActionId(123), ...) // "Boxed" here too, right?
// ... do something with somePost
// But don't: return somePost
// However some *other* similar functions *do* return `somePost` — so
// class `Post` must be able to box the ActionId? Hence it's boxed (above)?
}
Related to this is this answer, which says that when the value class doesn't escape the scope, it's effectively being inlined. It refers to Scala Improvement Process document SIP-15 for more details. However as far as I can tell SIP-15 actually doesn't mention that a value class instance that escapes the scope will be "boxed". But I think it seems reasonable that it would have to be "boxed". (Why doesn't the SIP explicitly state that it will be boxed if it escapes?)
None of your examples result in boxing. Value classes are only boxed with generics, with arrays, and when typed as a superclass/trait (e.g. Any/AnyVal)
They're boxed with generics because otherwise you can't distinguish them from the value (and primitives need a box anyway). Same deal with Any, and other superclasses/traits need a box or the type relationship is wrong.
They're boxed with arrays because arrays need to know the type of the contents, but the JVM doesn't understand the concept of a "value type". So you'd end up with an array that said it was the type being boxed, but which Scala was pretending was an array of the value type; a decision was made (based on previous issues with Array when it wasn't just a plain Java/JVM array) that this would lead to too many subtle bugs and corner cases.
Here's an example of the three ways to get boxing:
trait Q extends Any {}
class X(val x: String) extends AnyVal with Q {}
// Array
val a = Array(new X("salmon")) // boxed
// Generic
val b = Option(new X("minnow")) // boxed
// Upcast
val c = (new X("perch"): Any) // boxed
val d = (new X("cod"): AnyVal) // boxed
val e = (new X("herring"): Q) // boxed
Everything else--passing around through various functions, etc.--doesn't require boxing, including all of your examples.
Arrays are a bit of a special case because you can store the primitives and pull them out again as value classes with zero bytecode overhead, but a lot of syntactic overhead:
class Y(val repr: String) extends AnyVal {}
val y1 = new Y("apple") // unboxed
val y2 = new Y("orange") // unboxed
val ys: Array[String] = Array(y1.repr, y2.repr) // no overhead
val y3 = new Y(ys(0)) // no overhead
In all three cases there will be no boxing at all.
It's quite easy to check by your self:
class ActionId(val value: Int) extends AnyVal
object Foo {
def someFunction(): ActionId = {
new ActionId(123)
}
}
Now lets run scalac, and we will have a bunch of class files (files with bytecode). The one that we need is Foo\$.
» javap Foo\$
Compiled from "Boxing.scala"
public final class Foo$ extends java.lang.Object{
public static final Foo$ MODULE$;
public static {};
public int someFunction();
}
As you can see, even if value class leaks from function generally it wouldn't be boxed.
case class Post(id: ActionId, notion: String)
object Foo2 {
def someFunction(): Post = {
Post(new ActionId(123), "So ActionID will be boxed?")
}
}
scalac => javap:
» javap Post
Compiled from "Boxing.scala"
public class Post extends java.lang.Object implements scala.Product,scala.Serializable{
public static scala.Function1 tupled();
public static scala.Function1 curried();
public int id();
public java.lang.String notion();
public Post copy(int, java.lang.String);
public int copy$default$1();
public java.lang.String copy$default$2();
public java.lang.String productPrefix();
public int productArity();
public java.lang.Object productElement(int);
public scala.collection.Iterator productIterator();
public boolean canEqual(java.lang.Object);
public int hashCode();
public java.lang.String toString();
public boolean equals(java.lang.Object);
public Post(int, java.lang.String);
}
As you can see id here represented as plain int (e.g. third method).
Finally, will value class boxed if the object with a value class member is not returned (doesn't really escape the scope)?
case class Post(id: ActionId, notion: String)
object Foo3 {
def anotherFunction(): Unit {
val post = Post(new ActionId(123), "Will be boxed?")
}
}
If we look closely at bytecode of the method, here is what we will see:
Code:
Stack=4, Locals=2, Args_size=1
0: new #15; //class Post
3: dup
4: bipush 123
6: ldc #17; //String Will be boxed?
8: invokespecial #20; //Method Post."<init>":(ILjava/lang/String;)V
11: astore_1
12: return
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this LFoo3$;
12 0 1 post LPost;
There is no boxing of int in ActionId. If it would box you would see something like this one:
Code:
Stack=5, Locals=2, Args_size=1
0: new #15; //class Post
3: dup
4: new #17; //class ActionId
7: dup
8: bipush 123
10: invokespecial #20; //Method ActionId."<init>":(I)V
13: ldc #22; //String Will be boxed?
15: invokespecial #25; //Method Post."<init>":(LActionId;Ljava/lang/String;)V
18: astore_1
19: return
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 this LFoo3$;
19 0 1 post LPost;
You see, the difference is bipush 123
versus
4: new #17; //class ActionId
7: dup
8: bipush 123
10: invokespecial #20; //Method ActionId."<init>":(I)V
With some implicit casts it is actually possible to get around the array-issue without the syntactic required by rex-kerr. I used it in conjunction with How to reduce the number of objects created in Scala?
Y.scala:
import scala.language.implicitConversions
class Y(val repr: String) extends AnyVal {}
object Y {
implicit def stringToY (v:String) = new Y(v)
implicit def yToString (v:Y) = v.repr
}
Main file:
import Y._
val y1 = new Y("apple") // unboxed
val y2 = new Y("orange") // unboxed
val ys: Array[String] = Array(y1, y2) // Implicit cast
val y3:Y = ys(0)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With