Java - 获取泛型对象作为 String 泛型类型抛出异常
Java - Obtaining generic object as String Generic type throws exception
public class Box<T> {
private T element;
public T getElement() {
return element;
}
public void setElement(T element) {
this.element = element;
}
}
public class Test {
public static void main(String[] args) {
List<Box> l = new ArrayList<>(); //Just List of Box with no specific type
Box<String> box1 = new Box<>();
box1.setElement("aa");
Box<Integer> box2 = new Box<>();
box2.setElement(10);
l.add(box1);
l.add(box2);
//Case 1
Box<Integer> b1 = l.get(0);
System.out.println(b1.getElement()); //why no error
//Case 2
Box<String> b2 = l.get(1);
System.out.println(b2.getElement()); //throws ClassCastException
}
}
列表 l
包含 Box
类型的元素。在情况 1 中,我将第一个元素作为 Box<Integer>
获取,在第二种情况下,列表中的第二个元素作为 Box<String>
获取。在第一种情况下不会抛出 ClassCastException。
当我尝试调试时,b1
和 b2
中的 element's
类型分别是 String
和 Integer
。
是否与类型擦除有关?
在编译时,编译器知道类型并将 System.out.println(..)
的调用链接到具有正确参数类型的方法。在第一种情况下,编译器解析对 println(Object)
的调用。因为 b1.getElement()
returns 一个 Object
,String
是一个 Object
,方法调用是正确的,没有引发异常。在第二种情况下,编译器解析对 println(String)
的调用,因为 Box<String>
,但是 b2.getElement()
returns 和 Integer
。这不能转换为 String 并引发 ClassCastException
。
好的,这里的问题是 b2 错误地 标记为 Box<String>
而实际上它是 Box<Integer>
(box2 的类型) - 所以 b2.getElement()
被键入为字符串,即使它实际上包含一个整数。编译器尝试调用采用 String 的重载 println 方法,而不是采用 Object 的方法,因此您会得到 ClassCastException。 println 的对象版本将其参数显式转换为字符串(通过调用 toString()),但该方法的字符串版本不这样做。
潜在的问题是使用原始类型而不是完全指定列表 l
的类型参数 - 它应该是 List<Box<?>>
。然后你会有 b1
和 b2
作为 Box 并且会选择 System.out.println 的正确重载。
准确的说,问题是PrintStream#println
。
让我们使用javap -c Test.class
检查编译后的代码:
72: invokevirtual #12 // Method blub/Box.getElement:()Ljava/lang/Object;
75: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
如您所见,编译器删除了类型并省略了 Integer
的强制转换,因为这里没有必要。编译器已经将使用的重载方法链接到 PrintStream#(Object)
。
它这样做是由于 JLS rule §5.3:
Method invocation conversion is applied to each argument value in a method or constructor invocation (§8.8.7.1, §15.9, §15.12): the type of the argument expression must be converted to the type of the corresponding parameter.
Method invocation contexts allow the use of one of the following:
第三条规则是从子类型到超类型的转换:
A widening reference conversion exists from any reference type S to any reference type T, provided S is a subtype (§4.10) of T.
并且在检查类型是否可以拆箱之前完成(第五次检查:“拆箱转换”)。所以编译器检查 Integer
是 Object
的子类型,因此它必须调用 #println(Object)
(如果你检查被调用的重载版本,你的 IDE 会告诉你相同的) .
另一方面,第二个版本:
95: invokevirtual #12 // Method blub/Box.getElement:()Ljava/lang/Object;
98: checkcast #14 // class java/lang/String
101: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
有一个 checkcast
来检查 Box#getElement
的检索类型确实是 String
。这是必要的,因为您告诉编译器它将是一个 String
(由于通用类型 Box<String> b2 = l.get(1);
)并且它链接了方法 PrintStream#(String)
。此检查因提到的 ClassCastException
而失败,因为 Integer
无法转换为 String
.
未为 Integer 参数定义 println 方法,因此您的代码将调用 println(Object object),后者将调用 object.toString() 以获取需要打印的字符串。没有类型检查,因为一切都是对象。
在第二种情况下,您的代码想要调用 println(String someString),因此它将检查 someString 是否真的是一个 String,因为它不是,所以它会抛出异常。
public class Box<T> {
private T element;
public T getElement() {
return element;
}
public void setElement(T element) {
this.element = element;
}
}
public class Test {
public static void main(String[] args) {
List<Box> l = new ArrayList<>(); //Just List of Box with no specific type
Box<String> box1 = new Box<>();
box1.setElement("aa");
Box<Integer> box2 = new Box<>();
box2.setElement(10);
l.add(box1);
l.add(box2);
//Case 1
Box<Integer> b1 = l.get(0);
System.out.println(b1.getElement()); //why no error
//Case 2
Box<String> b2 = l.get(1);
System.out.println(b2.getElement()); //throws ClassCastException
}
}
列表 l
包含 Box
类型的元素。在情况 1 中,我将第一个元素作为 Box<Integer>
获取,在第二种情况下,列表中的第二个元素作为 Box<String>
获取。在第一种情况下不会抛出 ClassCastException。
当我尝试调试时,b1
和 b2
中的 element's
类型分别是 String
和 Integer
。
是否与类型擦除有关?
在编译时,编译器知道类型并将 System.out.println(..)
的调用链接到具有正确参数类型的方法。在第一种情况下,编译器解析对 println(Object)
的调用。因为 b1.getElement()
returns 一个 Object
,String
是一个 Object
,方法调用是正确的,没有引发异常。在第二种情况下,编译器解析对 println(String)
的调用,因为 Box<String>
,但是 b2.getElement()
returns 和 Integer
。这不能转换为 String 并引发 ClassCastException
。
好的,这里的问题是 b2 错误地 标记为 Box<String>
而实际上它是 Box<Integer>
(box2 的类型) - 所以 b2.getElement()
被键入为字符串,即使它实际上包含一个整数。编译器尝试调用采用 String 的重载 println 方法,而不是采用 Object 的方法,因此您会得到 ClassCastException。 println 的对象版本将其参数显式转换为字符串(通过调用 toString()),但该方法的字符串版本不这样做。
潜在的问题是使用原始类型而不是完全指定列表 l
的类型参数 - 它应该是 List<Box<?>>
。然后你会有 b1
和 b2
作为 Box 并且会选择 System.out.println 的正确重载。
准确的说,问题是PrintStream#println
。
让我们使用javap -c Test.class
检查编译后的代码:
72: invokevirtual #12 // Method blub/Box.getElement:()Ljava/lang/Object;
75: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
如您所见,编译器删除了类型并省略了 Integer
的强制转换,因为这里没有必要。编译器已经将使用的重载方法链接到 PrintStream#(Object)
。
它这样做是由于 JLS rule §5.3:
Method invocation conversion is applied to each argument value in a method or constructor invocation (§8.8.7.1, §15.9, §15.12): the type of the argument expression must be converted to the type of the corresponding parameter.
Method invocation contexts allow the use of one of the following:
第三条规则是从子类型到超类型的转换:
A widening reference conversion exists from any reference type S to any reference type T, provided S is a subtype (§4.10) of T.
并且在检查类型是否可以拆箱之前完成(第五次检查:“拆箱转换”)。所以编译器检查 Integer
是 Object
的子类型,因此它必须调用 #println(Object)
(如果你检查被调用的重载版本,你的 IDE 会告诉你相同的) .
另一方面,第二个版本:
95: invokevirtual #12 // Method blub/Box.getElement:()Ljava/lang/Object;
98: checkcast #14 // class java/lang/String
101: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
有一个 checkcast
来检查 Box#getElement
的检索类型确实是 String
。这是必要的,因为您告诉编译器它将是一个 String
(由于通用类型 Box<String> b2 = l.get(1);
)并且它链接了方法 PrintStream#(String)
。此检查因提到的 ClassCastException
而失败,因为 Integer
无法转换为 String
.
未为 Integer 参数定义 println 方法,因此您的代码将调用 println(Object object),后者将调用 object.toString() 以获取需要打印的字符串。没有类型检查,因为一切都是对象。
在第二种情况下,您的代码想要调用 println(String someString),因此它将检查 someString 是否真的是一个 String,因为它不是,所以它会抛出异常。