For the following code:
object Test {
class MapOps(map: Map[String, Any]) {
def getValue[T](name: String): Option[T] = {
map.get(name).map{_.asInstanceOf[T]}
}
}
implicit def toMapOps(map: Map[String, Any]): MapOps = new MapOps(map)
def main(args: Array[String]): Unit = {
val m: Map[String, Any] = Map("1" -> 1, "2" -> "two")
val a = m.getValue[Int]("2").get.toString
println(s"1: $a")
val b = m.getValue[Int]("2").get
println(s"2: $b")
}
}
val a
is computed without exception and the console prints 1: two
,
but when computing val b
, the java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
is thrown.
Besides, if I execute
val c = m.getValue[Int]("2").get.getClass.toString
println(s"1: $c")
The console prints "int".
Can someone explain why this code behaves like this?
This is certainly odd.
If you look at the following statement in the Scala REPL:
scala> val x = m.getValue[Int]("2")
x: Option[Int] = Some(two)
What I think is happening is this: the asInstanceOf[T]
statement is simply flagging to the compiler that the result should be an Int
, but no cast is required, because the object is still just referenced via a pointer. (And Int
values are boxed inside of an Option
/Some
) .toString
works because every object has a .toString
method, which just operates on the value "two" to yield "two". However, when you attempt to assign the result to an Int
variable, the compiler attempts to unbox the stored integer, and the result is a cast exception, because the value is a String
and not a boxed Int
.
Let's verify this step-by-step in the REPL:
$ scala
Welcome to Scala 2.12.1 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_151).
Type in expressions for evaluation. Or try :help.
scala> class MapOps(map: Map[String, Any]) {
| def getValue[T](name: String): Option[T] = {
| map.get(name).map{_.asInstanceOf[T]}
| }
| }
defined class MapOps
scala> import scala.language.implicitConversions
import scala.language.implicitConversions
scala> implicit def toMapOps(map: Map[String, Any]): MapOps = new MapOps(map)
toMapOps: (map: Map[String,Any])MapOps
scala> val a = m.getValue[Int]("2").get.toString
a: String = two
scala> println(s"1: $a")
1: two
So far so good. Note that no exceptions have been thrown so far, even though we have already used .asInstanceOf[T]
and used get
on the resulting value. What's significant is that we haven't attempted to do anything with the result of the get
call (nominally a boxed Int
that is actually the String
value "two") except to invoke it's toString
method. That works, because String
values have toString
methods.
Now let's perform the assignment to an Int
variable:
scala> val b = m.getValue[Int]("2").get
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:101)
... 29 elided
Now we get the exception! Note also the function in the stack trace that caused it: unboxToInt
- it's clearly trying to convert the value stored in the Some
to an Int
and it fails because it's not a boxed Int
but a String.
A big part of the problem is type erasure. Don't forget that a Some(Banana)
and a Some(Bicycle)
are - at runtime - both just Some
instances with a pointer to some object. .asInstanceOf[T]
cannot verify the type, because that information has been erased. However, the compiler is able to track what the type should be based upon what you've told it, but it can only detect the error when its assumptions are proven wrong.
Finally, with regard to the getClass
call on the result. This is a bit of compiler sleight-of-hand. It's not actually calling a getClass
function on the object, but - because it thinks it's dealing with an Int
, which is a primitive - it simply substitutes an int
class instance.
scala> m.getValue[Int]("2").get.getClass
res0: Class[Int] = int
To verify that the object actually is a String
, you can cast it to an Any
as follows:
scala> m.getValue[Int]("2").get.asInstanceOf[Any].getClass
res1: Class[_] = class java.lang.String
Further verification about the return value of get
follows; note the lack of an exception when we assign the result of this method to a variable of type Any
(so no casting is necessary), the fact that the valid Int
with key "1" is actually stored under Any
as a boxed Int
(java.lang.Integer
), and that this latter value can be successfully unboxed to a regular Int
primitive:
scala> val x: Any = m.getValue[Int]("2").get
x: Any = two
scala> x.getClass
res2: Class[_] = class java.lang.String
scala> val y: Any = m.getValue[Int]("1").get
y: Any = 1
scala> y.getClass
res3: Class[_] = class java.lang.Integer
scala> val z = m.getValue[Int]("1").get
z: Int = 1
scala> z.getClass
res4: Class[Int] = int
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