Why wrapping a generic method call with Option defers ClassCastException?
Asked Answered
P

2

16

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:

Pelota answered 21/3, 2016 at 18:6 Comment(7)
This confused me too earlier. Take a look at this: #34204973 It is not exactly the same question as yours, but the answer applies to your case as well.Cordeliacordelie
OK, this is confusing. I'll need some time to wrap my head around it. Thanks for pointing this out @CordeliacordeliePelota
Could be related to this question (although it's on lists, but probably applies to every complex type F[_]).Despinadespise
@EndeNeu I actually started with a sequence and reduced this to the provided example later. It looks like almendar is onto something here with pointing out that it is related to generics.Pelota
Read your compiler warnings.Busse
@ChrisMartin This always a good advice but compiler seems to perfectly happy about it. Do you have any particular options in mind?Pelota
@Pelota My mistake, sorry for commenting without testing. I thought .asInstanceOf[T] was an unchecked cast due to partial erasure, but on second look I see it isn't.Busse
J
9

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.

Jairia answered 21/3, 2016 at 18:50 Comment(0)
Z
2

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.

Zia answered 24/3, 2016 at 14:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.