Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why wrapping a generic method call with Option defers ClassCastException?

Lets say I have an array like this*:

val foo: Any = 1 : Int
Option(foo.asInstanceOf[String])

which fails for obvious reason:

// java.lang.ClassCastException: java.lang.Integer cannot be cast to 
// java.lang.String
// ... 48 elided

Next lets consider a following class:

case class DummyRow() {
  val foo: Any = 1 : Int
  def getAs[T] = foo.asInstanceOf[T]
  def getAsOption[T] = Option(foo.asInstanceOf[T])
}

As far as I can tell getAs should behave the same way as the previous apply followed by asInstanceOf.

Surprisingly it is not the case. When called alone it throws an exception:

DummyRow().getAs[String]
// java.lang.ClassCastException: java.lang.Integer cannot be cast to 
// java.lang.String
// ... 48 elided

but when wrapped with Option succeeds:

val stringOption = Option(DummyRow().getAs[String])
// Option[String] = Some(1)

DummyRow().getAsOption[String]
// Option[String] = Some(1)

and fails only when I try to access wrapped value:

stringOption.get
// java.lang.ClassCastException: java.lang.Integer cannot be cast to 
// java.lang.String
// ... 48 elided

So what happens here? It seems to be limited ClassCastException so I guess it is related to some ugly thing like type erasure.


* Any and asInstanceOf are there to mimic a behavior of the 3rd party code so please lets not dwell on that.

** Tested in Scala 2.10.5, 2.11.7

*** If you're interested in the context you can take a look at Using contains in scala - exception

**** Other relevant questions linked in the comments:

  • Why does `.asInstanceOf` sometimes throw, and sometimes not?,
  • Why asInstanceOf doesn't throw a ClassCastException?
like image 338
zero323 Avatar asked Mar 21 '16 18:03

zero323


Video Answer


2 Answers

Below is a simplified version of your problem with an additional case for Any

def getAs[T] = (1:Int).asInstanceOf[T]

//blows up
getAs[String]

//blows up
def p(s:String): Unit = {}
p(getAs[String])

//works
def p[T](s:T): Unit = {}
p(getAs[String])

//works
def p(s:Any): Unit = {}
p(getAs[String])

Because you create a method with a generic parameter, the runtime doesn't need to "touch" the value because it does not care. Generic will be treated as Any/Object at runtime.

like image 176
almendar Avatar answered Oct 17 '22 02:10

almendar


Take a look at the following (slightly edited for reading purposes) REPL session:

scala> class Foo(foo: Any) {
     | def getAs[T] = foo.asInstanceOf[T]
     | def getAsString = foo.asInstanceOf[String]
     | }
defined class Foo

scala> :javap Foo
  Size 815 bytes
  MD5 checksum 6d77ff638c5719ca1cf996be4dbead62
  Compiled from "<console>"
public class Foo
{
  public <T extends java/lang/Object> T getAs();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: getfield      #11                 // Field foo:Ljava/lang/Object;
         4: areturn       
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   LFoo;
      LineNumberTable:
        line 12: 0
    Signature: #35                          // <T:Ljava/lang/Object;>()TT;

  public java.lang.String getAsString();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: getfield      #11                 // Field foo:Ljava/lang/Object;
         4: checkcast     #17                 // class java/lang/String
         7: areturn       
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       8     0  this   LFoo;
      LineNumberTable:
        line 13: 0
}

You can see in the bytecode of getAsString that a checkcast instruction is executed when casting to a String. On the other hand in getAs[T] no such instruction gets executed even though there is a cast in the code. The reason for that is that T gets erased to Any at runtime, so that would simply become a cast to Any (which would never fail). So casting to a type parameter is only necessary for the compiler's sake, not the JVM's. So no casting has to happen when you wrap that call in Option which is generic too. It's only when you want to get the value out of the Option and treat it as a String that a cast is executed and an exception is thrown.

scala> class Bar() {
     | def getString: String = new Foo(3).getAs[String]
     | def get[T]: T = new Foo(3).getAs[T]
     | }
defined class Bar

scala> :javap Bar
  Size 1005 bytes
  MD5 checksum 4b7bee878db4235ca9c011c6f168b4c9
  Compiled from "<console>"
public class Bar
{
  public java.lang.String getString();
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: new           #9                  // class Foo
         3: dup           
         4: iconst_3      
         5: invokestatic  #15                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
         8: invokespecial #19                 // Method Foo."<init>":(Ljava/lang/Object;)V
        11: invokevirtual #23                 // Method Foo.getAs:()Ljava/lang/Object;
        14: checkcast     #25                 // class java/lang/String
        17: areturn       
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      18     0  this   LBar;
      LineNumberTable:
        line 13: 0

  public <T extends java/lang/Object> T get();
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: new           #9                  // class Foo
         3: dup           
         4: iconst_3      
         5: invokestatic  #15                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
         8: invokespecial #19                 // Method Foo."<init>":(Ljava/lang/Object;)V
        11: invokevirtual #23                 // Method Foo.getAs:()Ljava/lang/Object;
        14: areturn       
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      15     0  this   LBar;
      LineNumberTable:
        line 14: 0
    Signature: #51                          // <T:Ljava/lang/Object;>()TT;
}

As you can see checkcast is executed after getAs instead of during, and only in a non-generic context.

like image 2
Jasper-M Avatar answered Oct 17 '22 01:10

Jasper-M