[英]What causes this “unchecked cast” warning?
在 java 中,我已经做了(并且正在使用没有问题),这个方法:
private final HashMap<String, Object> data = new HashMap<>();
public final @NotNull <clazz> Collection<clazz> getCollection(@NotNull String key, @NotNull Object clazz) {
Object object = this.data.get(key);
if (object instanceof Collection) {
Collection<?> collection = (Collection<?>) object;
for (Object o : collection) {
if (o.getClass().equals(clazz)) {
return (Collection<clazz>) collection;
}
break;
}
}
return Collections.emptyList();
}
具体来说,行return (Collection<clazz>) collection;
正在产生警告:
Unchecked cast: 'java.util.Collection<capture<?>>' to 'java.util.Collection<clazz>'
在适当的情况下诚实:大约一年前,在我开始学习编码后不久,我出于需要即兴创作了这种方法。 不幸的是,即使是现在,我仍然不明白我放在那里的<clazz>
究竟是做什么的,它为什么起作用以及它是如何起作用的。
从这个意义上说:我希望了解 - 当然,不要被提供删除警告信息的解决方案。 (而且我在这篇文章之前完全没有使用@SuppressWarnings("unchecked")
。)
/编辑:
在阅读了到目前为止的回复之后,我正在理解为什么会产生这个警告。
现在,对于应该解决问题的理论解决方法:
public final @NotNull <T> Collection<T> getCollection(@NotNull String key, @NotNull Class<T> dataType) {
Object object = this.data.get(key);
Collection<T> r = new ArrayList<>();
if (object instanceof Collection) {
Collection<?> collection = (Collection<?>) object;
for (Object o : collection) {
if (o.getClass().equals(dataType)) {
r.add((T) o);
}
}
}
return r;
}
但现在行r.add((T) o);
正在产生相同的警告。 除了o.getClass().equals(dataType)
之外,必须有更好的条件才能让 IDE 理解。 (顺便说一下,IntelliJ 社区版)
这里有两个clazz
变量:第一个是方法的第二个参数,另一个是方法的类型参数<clazz>
。 我强烈建议重命名第二个以避免这种歧义(通常类型参数以单个大写字母命名)`。
编译器显示此警告的原因是它不能保证(即在编译时检查)强制转换是安全的。 没有任何东西可以保证您对元素类型进行检查的参数类型与类型参数相同。 因此,您可以将Integer
作为参数传递,同时通过使用String
类型参数调用方法返回Collection<String>
:
data.put("a", List.of(1, 2));
Collection<String> collection = getCollection("a", Integer.class);
String first = collection.stream().findAny().get(); // ClassCastException at runtime
您可以通过让 class 使用相同的类型参数来稍微改进一下:
public final @NotNull <T> Collection<T> getCollection(@NotNull String key, @NotNull Class<T> clazz) {
Collection<?> object = data.get(key);
if (object instanceof Collection) {
Collection<?> collection = (Collection<?>) object;
for (Object o : collection) {
if (o.getClass().equals(clazz)) {
return (Collection<T>) collection;
}
break;
}
}
return Collections.emptyList();
}
但是这个警告仍然会保留,因为您需要保证所有集合元素在运行时都是T
类型:
data.put("a", List.of("x", 2));
Collection<String> collection = SOTest.getCollection("a", String.class);
for(String s : collection) {
System.out.println(s); // second element will still cause ClassCastException
}
除了M Anouti 回答中的(正确)观察结果:
您只是在检查第一个成员的类型是否符合您的期望。 这通常不足以确定集合的类型。 例如:
public static void main(String[] args) {
Main m = new Main();
List<Number> numbers = List.of(1, 0.0, BigInteger.valueOf(4));
m.data.put("nums", numbers);
Collection<Integer> coll = m.getCollection("nums", Integer.class);
List<Integer> output = new ArrayList<>(coll);
System.out.println(output);
output.sort(Integer::compare);
}
Output:
[1, 0.0, 4]
Exception in thread "main" java.lang.ClassCastException: class java.lang.Double cannot be cast to class java.lang.Integer (java.lang.Double and java.lang.Integer are in module java.base of loader 'bootstrap')
at java.base/java.util.TimSort.countRunAndMakeAscending(TimSort.java:355)
at java.base/java.util.TimSort.sort(TimSort.java:220)
at java.base/java.util.Arrays.sort(Arrays.java:1306)
at java.base/java.util.ArrayList.sort(ArrayList.java:1721)
at world.Main.main(Main.java:23)
如您所见,不安全强制转换导致结果中的泛型类型不正确,并且仅在运行时实际尝试将成员用作声明类型的实例时才检测到。 在此示例中, println
无一例外地接受不正确的类型,因为该方法不关心 - 它只是调用toString
,并且每个 Object 都有。 只有当我们尝试调用Integer.compare
时,才会失败。
即使您检查所有元素,仅仅因为集合当前不包含给定类型的元素并不意味着它永远不会 - 如果集合实际上是Collection<Number>
,但当前仅包含Double
s,这并不意味着它可以安全地用作Collection<Double>
。
测试o.getClass().equals(clazz)
并不意味着它是Collection<clazz>
。
Collection<clazz>
可以被认为是两种类型的交集:
Collection<? extends clazz>
Collection<? extends clazz>
是一个Collection
,其中所有元素都是clazz
的实例,或者是 null。Collection<? super clazz>
Collection<? super clazz>
是一个Collection
,可以向其中添加clazz
或 null 的元素。 在测试第一个元素是clazz
的实例时,您所确定的只是将clazz
的实例添加到该Collection
是可以接受的:也就是说,您只能确定它是Collection<? super clazz>
Collection<? super clazz>
,因为clazz
实例的存在表明添加另一个clazz
实例是安全的。
而且您只检查第一个元素的事实是无关紧要的:您可以检查列表中的所有元素,但您仍然不知道更多信息:您所能确定的是在您测试元素时,集合中没有另一个 class 的实例。
一般来说,没有任何保证会随着时间的推移而保持不变。 如果您的代码的其他部分可以访问同一个Collection
实例,但是这具有允许将更通用类型的实例添加到Collection
的附加类型信息,这可能会破坏getCollection
返回值的类型安全性。
举一个实际的例子:
List<Object> list = new ArrayList<>();
list.add(0);
list.add(1);
// This is the class in the question. I don't know the API for
// adding stuff to it, so I'm just making it up.
YourClass yourClass = new YourClass();
yourClass.put("key", list);
// Invoke the method in the question to get an unsafe collection.
Collection<Integer> unsafeCollection = yourClass.getCollection("key", Integer.class);
// This affects the contents of unsafeCollection.
list.clear();
list.add("hello");
// Now you will get a ClassCastException, because list (and collection,
// which is the same instance as list) only contains Strings.
for (Integer i : unsafeCollection) {}
但是,如果您已声明您的方法返回Collection<? super Integer>
Collection<? super Integer>
:
Collection<? super Integer> safeCollection = yourClass.getCollectionSafe("key", Integer.class);
假设Integer
元素类型,您将无法迭代:
for (Integer i : safeCollection) {} // Compiler error.
但是您可以为Object
元素类型:
for (Object o : safeCollection) {} // Fine.
因此,是否调用了list.add("hello")
并不重要。
但现在行
r.add((T) o);
正在产生相同的警告
了解未经检查的警告实际上告诉您什么很重要:编译器无法插入任何指令来确定该行上的转换是否正确(与检查转换相反,这将导致检查checkcast
字节码指令)。 checkcast
需要静态地知道它正在检查的类型; 它不知道T
在这里是什么,因为它是一个类型变量。
您可以通过@SuppressWarnings
告诉编译器:“相信我,我知道这是安全的”; 但在这样做的过程中,你承担了确保它是的责任。 在这种情况下,您可以确定它是安全的,因为前面的检查,所以抑制是合适的。
您可能会考虑使用局部变量重写,以便能够仅将抑制应用于该强制转换,而不必将其应用于整个方法:
if (o.getClass().equals(dataType)) {
@SuppressWarnings("unchecked") // Safe, because of preceding check.
T castO = (T) o;
r.add(castO);
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.