scala - ¿Por qué envolver una llamada a un método genérico con la opción difiere la excepción ClassCastException?
generics scala-option (2)
Digamos que tengo una matriz como esta *:
val foo: Any = 1 : Int
Option(foo.asInstanceOf[String])
que falla por razones obvias:
// java.lang.ClassCastException: java.lang.Integer cannot be cast to
// java.lang.String
// ... 48 elided
A continuación consideremos una clase siguiente:
case class DummyRow() {
val foo: Any = 1 : Int
def getAs[T] = foo.asInstanceOf[T]
def getAsOption[T] = Option(foo.asInstanceOf[T])
}
Por lo que sé, getAs
debe comportarse de la misma manera que la apply
anterior, seguido de asInstanceOf
.
Sorprendentemente no es el caso. Cuando se llama solo lanza una excepción:
DummyRow().getAs[String]
// java.lang.ClassCastException: java.lang.Integer cannot be cast to
// java.lang.String
// ... 48 elided
pero cuando se envuelve con la Option
tiene éxito:
val stringOption = Option(DummyRow().getAs[String])
// Option[String] = Some(1)
DummyRow().getAsOption[String]
// Option[String] = Some(1)
y falla solo cuando intento acceder al valor envuelto:
stringOption.get
// java.lang.ClassCastException: java.lang.Integer cannot be cast to
// java.lang.String
// ... 48 elided
Entonces, ¿qué pasa aquí? Parece ser una ClassCastException
limitada, así que supongo que está relacionada con una cosa fea como el borrado de tipo.
* Any
y asInstanceOf
están ahí para imitar un comportamiento del código de un tercero, así que, por favor, no nos detengamos en eso.
** Probado en Scala 2.10.5, 2.11.7
*** Si está interesado en el contexto, puede echar un vistazo a Uso de contiene en scala - excepción
**** Otras preguntas relevantes vinculadas en los comentarios:
A continuación se muestra una versión simplificada de su problema con un caso adicional para 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])
Debido a que crea un método con un parámetro genérico, el tiempo de ejecución no necesita "tocar" el valor porque no le importa. Los genéricos se tratarán como Any
/ Object
en tiempo de ejecución.
Eche un vistazo a la siguiente sesión REPL (ligeramente editada para fines de lectura):
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
}
Puede ver en el getAsString
de getAsString
de getAsString
que se checkcast
una instrucción de getAsString
cuando se checkcast
en un String
. Por otro lado, en getAs[T]
no se ejecuta tal instrucción aunque haya una conversión en el código. La razón de esto es que T
se borra a Any
en el tiempo de ejecución, por lo que simplemente se convertiría en un elenco a Any
(que nunca fallaría). De modo que la conversión a un parámetro de tipo solo es necesaria para el compilador, no para las JVM. Por lo tanto, no se debe realizar ningún lanzamiento cuando se ajusta esa llamada en Option
que también es genérica. Solo cuando se desea obtener el valor de la Option
y tratarlo como una String
se ejecuta una conversión y se lanza una excepción.
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;
}
Como puede ver, checkcast
se ejecuta después de getAs
lugar de durante, y solo en un contexto no genérico.