19

Consider the following Scala code:

case class Data[T](value: Option[T]) {
  def get: T = try {
    doGet
  } catch {
    case e: Exception => throw new IllegalArgumentException
  }

  def doGet: T = value match {
    case Some(v) => v
    case None => ().asInstanceOf[T]
  }
}

Data[Unit](None).get
Data[Integer](None).get // which exception is thrown here?

[spoiler] It is a ClassCastException; who can explain why it is not caught and replaced by an IllegalArgumentException?

PS: To preempt any questions on why I would want to do this: this is a simplified version of some code that uses json4s to parse some string into an Option[T]; if the parsing fails None is returned, which is OK if T was Unit and not OK if T is some other type.

Erik Kaplun
  • 37,128
  • 15
  • 99
  • 111
Hugo Zwaal
  • 193
  • 3

1 Answers1

22

Explanation

Exception isn't thrown here:

().asInstanceOf[T]

because this is an unchecked cast - JVM cannot verify if it is possible to cast () into T, because it has no information about T due to type erasure.

Instead, exception is thrown here

Data[Integer](None).get

because the result of get is cast into an Integer and that is something that JVM can verify. So, ClassCastException is actually thrown outside of get.

BTW, javac always warns about unchecked casts, I don't know why scalac doesn't.

Workaround

To some extent, it is possible to work around type erasure here using ClassTag and reflection-based casting:

import scala.reflect.{ClassTag, classTag}

case class Data[T: ClassTag](value: Option[T]) {
  def get: T = try {
    doGet
  } catch {
    case e: Exception => throw new IllegalArgumentException
  }

  def doGet: T = value match {
    case Some(v) => v
    case None => classTag[T].runtimeClass.asInstanceOf[Class[T]].cast(())
  }
}

Hackaround

For this use case, you can inspect the ClassTag directly:

scala> case class Data[T](value: Option[T])(implicit t: ClassTag[T]) {
     | def get: T = value getOrElse (t match {
     |   case ClassTag.Unit => ().asInstanceOf[T]
     |   case _ => throw new IllegalArgumentException
     | })
     | }
defined class Data

scala> Data[Unit](None)
res6: Data[Unit] = Data(None)

scala> .get

scala> Data[Int](None).get
java.lang.IllegalArgumentException
som-snytt
  • 39,429
  • 2
  • 47
  • 129
ghik
  • 10,706
  • 1
  • 37
  • 50
  • Aha! Erasure! That's the key. – Daniel C. Sobral Sep 12 '13 at 20:22
  • Can the code be modified to make the `get` throw the exception? – Hugo Zwaal Sep 12 '13 at 20:42
  • 1
    Note that your `ClassTag` workaround would not work if you try this reflection-based casting to cast, e.g., a `List[Int]` to a `List[String]`. It will silently work and then fail later, throwing again a `ClassCastException`. There's a reason why `runtimeClass` doesn't return `Class[T]`. – Jean-Philippe Pellet Sep 12 '13 at 21:50
  • The `ClassTag` hackaround works well in this case, since only the `Unit` type needs special treatment and all other types (including parameterized ones) should cause the `IllegalArgumentException`. It is also possible to move the `implicit ClassTag` to the `get` definition. – Hugo Zwaal Sep 13 '13 at 11:17