为什么使用 Option 包装泛型方法调用会延迟 ClassCastException



假设我有一个这样的数组*:

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

由于显而易见的原因而失败:

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

接下来让我们考虑以下类:

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

据我所知,getAs的行为方式应该与前一个apply然后是asInstanceOf

令人惊讶的是,情况并非如此。单独调用时,它会抛出异常:

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

但是当用Option包装时成功:

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

并且仅在我尝试访问包装的值时才失败:

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

那么这里会发生什么?它似乎ClassCastException有限,所以我想它与一些丑陋的事情有关,比如类型擦除。


* AnyasInstanceOf是为了模仿第三方代码的行为,所以请不要纠缠于此。

** 在 Scala 2.10.5、2.11.7 中测试

如果你对上下文感兴趣,可以看看在scala中使用包含 - 例外

评论中链接的其他相关问题:

  • 为什么".asInstanceOf"有时会抛出,有时不会?
  • 为什么asInstanceOf 不抛出 ClassCastException?

下面是问题的简化版本,其中包含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])

由于使用泛型参数创建方法,因此运行时不需要"接触"值,因为它不关心。泛型将在运行时被视为Any/Object

看看下面的(为了阅读目的而略有编辑)REPL会议:

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
}

您可以在getAsString的字节码中看到,在转换为String时会执行checkcast指令。另一方面,getAs[T]即使代码中有强制转换,也不会执行这样的指令。原因是T在运行时被擦除为Any,因此它只会成为Any的强制转换(永远不会失败)。因此,仅出于编译器的缘故才需要强制转换为类型参数,而不是 JVM。因此,当您将该调用包装在也是通用的Option时,不必发生转换。仅当您想要从Option中获取值并将其视为String时,才会执行强制转换并引发异常。

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;
}

如您所见checkcast是在getAs之后而不是期间执行的,并且仅在非通用上下文中执行。

相关内容

  • 没有找到相关文章

最新更新