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
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)
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