Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala unexplainable program behavior

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?

like image 864
Gregory Avatar asked Oct 30 '17 16:10

Gregory


1 Answers

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
like image 168
Mike Allen Avatar answered Oct 16 '22 16:10

Mike Allen