Gregory
Gregory

Reputation: 83

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?

Upvotes: 8

Views: 171

Answers (3)

Joe Pallas
Joe Pallas

Reputation: 2155

Expanding on the last part of eje211's answer.

You told the compiler that a String was an Int, and now you're looking for a sensible explanation of the resulting behavior. That's understandable, but it's not really useful. Once you tell the compiler a lie, all bets are off. You can spend time investigating exactly when and where the compiler inserts checks that happen to discover your deceit, but your time would probably be better spent writing code that doesn't cause you to lie.

As the earlier answer pointed out, you can do that (avoid accidental lying) by using pattern matching. You'll need a ClassTag to make pattern matching work in cases like the above, but the end result will be code that is type-safe and correct.

Upvotes: 0

Mike Allen
Mike Allen

Reputation: 8279

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

Upvotes: 6

eje211
eje211

Reputation: 2429

It's an Int because you request an Int on this line :

val b = m.getValue[Int]("2").get

This calls this method:

def getValue[T](name: String): Option[T] = {
  map.get(name).map{_.asInstanceOf[T]}
}

and applies it this way:

def getValue[Int](name: String): Option[Int] = {
  map.get(name).map{_.asInstanceOf[Int]}
}

So if you ask for an Int, you get an Int.

In the case of "two", this is what happens in that case:

"two".asInstanceOf[Int]

That's what throws your exception.

A String is not an Int. You can't cast it that way. You can cast it this way:

"2".toInt

But that's different.

In general, using asInstanceOf[] is dangerous. Try pattern matching instead. If you must use, it's up you to make sure that the casts you attempt are valid. You're basically telling the compiler to bypass its own type-checks, particularly when you cast from Any.

It works when you add .toString because then, you change the type back to String, which is what it really was in the first place. The lie of what type the data was is corrected.

Upvotes: -1

Related Questions