9

I was trying to answer this question, as I thought I knew the answer. Turns out, I did not know quite enough :/

Here is a test I have done:

class Inst[T] { 
  def is(x: Any) = scala.util.Try { as(x) }.isSuccess
  def as(x: Any): T = x.asInstanceOf[T]
}

scala> new Inst[String].is(3)
res17: Boolean = true

scala> new Inst[String].as(3)
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
 ... 33 elided

What is going on here? Why does only the second call to as throw, but not the first one?

Community
  • 1
  • 1
Dima
  • 39,570
  • 6
  • 44
  • 70

2 Answers2

10

This is because the class-cast-exception is only thrown when you do something with the value, call a method on it after the cast. In the REPL for example, you would have a toString call in the second case. Note:

new Inst[String].as(3); ()           // nothing happens
new Inst[String].as(3).toString; ()  // exception

The reason why this takes the extra step is that Inst[T] is generic with type parameter T which is erased at runtime; only when the call-site (that has a static knowledge of type T) tries to call a method on the result, the actual type check occurs.


For your follow-up question, toString is defined on any object and since T is generic you have a boxed integer (<: AnyRef) and toString and println succeed within the is method. So another example where the Try would fail is this:

class Inst[T] { 
  def foo(x: Any)(implicit ev: T <:< String) = scala.util.Try {
    ev(as(x)).reverse
  }.isSuccess
}

new Inst[String].foo(3)  // false!
0__
  • 66,707
  • 21
  • 171
  • 266
  • No, this does not seem to explain much: I changed the `is` definition to: `def is(x: Any) = scala.util.Try { as(x).toString }.isSuccess`, and it still returns `true` (i.e., the cast does not throw). Even this `def is(x: Any) = scala.util.Try { println(as(x).toString) }.isSuccess;` happily prints out "3" and returns true :-/ – Dima Dec 10 '15 at 14:46
  • Ah, it makes sense now, thanks! `is` does not know what `T` is, so it treats the argument as `Any`. I tried this: `trait Foo { def foo = ??? } class Inst[T <: Foo] { def is(x: Any) = scala.util.Try { as(x).foo }.isSuccess; def as(x: Any): T = x.asInstanceOf[T]; }`. Now `new Inst[Foo].is(3)` returns `false` as expected. – Dima Dec 10 '15 at 14:55
  • Yes, the `toString` is not a good example (I used it because that was what was happening in the REPL for `as`), because inside `Inst` you have type `T` erased to `java.lang.Object` aka `scala.AnyRef` for which `toString` is defined, thus no exception there. – 0__ Dec 10 '15 at 14:59
2

While @0__'s answer explains why it doesn't work, here is how to make it work:

class Inst[T](implicit tag: scala.reflect.ClassTag[T]) {
  def is(x: Any) = tag.runtimeClass.isInstance(x) 
  // scala.util.Try { as(x) }.isSuccess will work as well
  def as(x: Any): T = tag.runtimeClass.cast(x).asInstanceOf[T]
}

object Main extends App {
  println(new Inst[String].is(3))
  println(new Inst[String].as(3))
}


false
java.lang.ClassCastException: Cannot cast java.lang.Integer to java.lang.String
    at java.lang.Class.cast(Class.java:3369)
...
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487