I think a lot of confusion stems from the fact that type erasure is not exactly intuitive. Decompiling Scala code to Java code won’t help IMO. First, consider a Scala code:
object Scala {
class Something
class Something1 extends Something
class Something2 extends Something
def cast[A <: Something](param: Any): A =
param.asInstanceOf[A]
def main(args: Array[String]): Unit = {
val x = cast[Something2](arg)
println(x)
}
// val arg = new Object
// val arg = new Something1
}
You need to uncomment one of the lines that define arg
. If you uncomment val arg = new Object
then ClassCastException will be thrown inside def cast
. If you uncomment val arg = new Something1
then ClassCastException will be thrown outside of def cast
, in the line val x = cast[Something2](arg)
.
Why the cast fails in different places if there is only one cast in source code? Because type erasure causes some but sometimes not all checks to be moved from definition site to use site. What can be checked inside a erased method is checked there, what can’t is moved to use site. In this case param.asInstanceOf[A]
inside def cast
is erased to param.asInstanceOf[Something]
as Something
is the only known upper bound of A
during compilation of def cast
. A more precise cast to Something2
is thus moved to line val x = cast[Something2](arg)
as that’s the line where type A
of value returned from def cast
is statically known.
java -p -c
confirms my explanation:
Compiled from "Scala.scala"
public final class temp.Scala$ {
public static temp.Scala$ MODULE$;
private final java.lang.Object arg;
public static {};
Code:
0: new #2 // class temp/Scala$
3: invokespecial #22 // Method "<init>":()V
6: return
public <A extends temp.Scala$Something> A cast(java.lang.Object);
Code:
0: aload_1
// first cast to `Something`
1: checkcast #7 // class temp/Scala$Something
4: areturn
public void main(java.lang.String[]);
Code:
0: aload_0
1: aload_0
2: invokevirtual #33 // Method arg:()Ljava/lang/Object;
5: invokevirtual #35 // Method cast:(Ljava/lang/Object;)Ltemp/Scala$Something;
// second, more precise cast to `Something2`
8: checkcast #12 // class temp/Scala$Something2
11: astore_2
12: getstatic #40 // Field scala/Predef$.MODULE$:Lscala/Predef$;
15: aload_2
16: invokevirtual #44 // Method scala/Predef$.println:(Ljava/lang/Object;)V
19: return
public java.lang.Object arg();
Code:
0: aload_0
1: getfield #49 // Field arg:Ljava/lang/Object;
4: areturn
private temp.Scala$();
Code:
0: aload_0
1: invokespecial #50 // Method java/lang/Object."<init>":()V
4: aload_0
5: putstatic #52 // Field MODULE$:Ltemp/Scala$;
8: aload_0
9: new #4 // class java/lang/Object
12: dup
13: invokespecial #50 // Method java/lang/Object."<init>":()V
16: putfield #49 // Field arg:Ljava/lang/Object;
19: return
}
In def f[A]: A = new Object().asInstanceOf[A]
there’s no upper bound of A
thus no casts are done inside of def f
and instead they are moved to all use sites. If there are many use sites of method f
then in each one compiler can decide to handle the cast differently.
Changing value discarding semantics would suprisingly change semantics of some valid (?) programs. Following code prints null
:
object Scala {
def execute[R](param: Any): R =
param.asInstanceOf[R]
def main(args: Array[String]): Unit = {
val x = execute[Unit](null)
println(x)
}
}
What is more important then:
-
anything.asInstanceOf[Unit] == ()
for any type ofanything
- or
-
null.asInstanceOf[A] == null
for any typeA
?
These requirements are contradictory as it seems.