我该如何处理未经检查的强制类型转换警告?

698

Eclipse给我一个警告,如下所示:

类型安全性: 未经检查的从Object到HashMap的转换

这是从一个我无法控制其返回类型为Object的API调用中出现的警告:

HashMap<String, String> getItems(javax.servlet.http.HttpSession session) {
  HashMap<String, String> theHash = (HashMap<String, String>)session.getAttribute("attributeKey");
  return theHash;
}

如果可能的话,我希望避免Eclipse警告,因为从理论上讲它们表示至少存在潜在的代码问题。然而,我尚未找到有效消除此警告的好方法。但是,我可以将涉及此行的单行提取出来形成一个独立的方法,并添加@SuppressWarnings("unchecked")到该方法中,从而减少忽略警告的代码块所带来的影响。是否有更好的选择?我不想在Eclipse中关闭这些警告。

在我接手代码之前,它更简单,但仍会引发警告:

HashMap getItems(javax.servlet.http.HttpSession session) {
  HashMap theHash = (HashMap)session.getAttribute("attributeKey");
  return theHash;
}

当您尝试使用哈希时,问题实际上出现在其他地方,您会收到警告:

HashMap items = getItems(session);
items.put("this", "that");

Type safety: The method put(Object, Object) belongs to the raw type HashMap.  References to generic type HashMap<K,V> should be parameterized.

如果您正在使用HttpSession,可以查看Brian Goetz在以下主题上的文章:http://www.ibm.com/developerworks/library/j-jtp09238.html - Tom Hawtin - tackline
如果无法避免未经检查的转换,一个好主意是将其与逻辑上表示其类型的东西(如enum甚至是Class<T>的实例)紧密耦合,这样您可以一眼看出它是安全的。 - Philip Guin
4
相关/重复:类型安全:未检查的转换请注意,以下是对原问题的翻译:原问题标题:类型安全:未检查的转换问题描述:我如何在Java中执行未检查的转换?解答:如果您需要执行未检查的转换,则可以使用Java的强制类型转换操作符,并使用“@SuppressWarnings("unchecked")”注释来抑制编译器警告。但是,需要注意的是,如果执行不正确,则可能会导致运行时异常。因此,请确保在进行未检查的转换之前仔细考虑所有情况,并尽可能地避免使用此类转换。 - blahdiblah
3
可能是类型安全:未检查的转换的重复内容。 - Raedwald
1
我想补充一下,我发现只能在包含有问题代码的方法级别上添加@SuppressWarnings("unchecked")。因此,我将代码拆分到一个例程中,在那里我不得不这样做。我一直以为可以立即在相关行的上方这样做。 - JGFMK
22个回答

610
当然,显而易见的答案是不要进行未经检查的转换。
如果确实必要,那么至少尝试限制@SuppressWarnings注释的范围。根据其Javadocs,它可以应用在局部变量上;这样就不会影响整个方法。
示例:
@SuppressWarnings("unchecked")
Map<String, String> myMap = (Map<String, String>) deserializeMap();

无法确定Map是否真的应该具有泛型参数<String, String>。您必须事先知道参数应该是什么(否则会在获得ClassCastException时发现)。这就是为什么代码会生成警告的原因,因为编译器不可能知道是否安全。


126
谢谢指出可以在局部变量上使用该功能,Eclipse只提供将其添加到整个方法的选项。 - thSoft
17
Eclipse 3.7(Indigo)支持将unchecked添加到局部变量。 - sweetfa
80
警告不仅是因为编译器不知道强制转换是否安全。例如 String s = (String) new Object() ; 没有警告,即使编译器不知道强制转换是否安全。警告是因为编译器(a)不知道强制转换是否安全,并且(b)在强制转换点不会生成完整的运行时检查。将进行一个检查以确保它是一个“HashMap”,但不会检查它是一个“HashMap<String,String>”。 - Theodore Norvell
10
可悲的是,尽管类型转换和警告是针对“赋值”的,但注释必须写在变量声明上...因此,如果声明和赋值在不同的位置(比如,在一个“try”块之外和之内分别),Eclipse现在会生成两个警告:原始的未检查类型转换和新的“无用注释”诊断。 - Ti Strga
6
如果需要在不同的作用域或不同行上进行强制转换,但注释却需要与本地变量声明一起出现,可以通过在强制转换的作用域内创建一个本地变量来解决。这个本地变量专门用于在声明的同一行执行强制转换。然后将此变量分配给不同作用域中的实际变量。我也使用了这种方法来消除对实例变量转换的警告,因为注释不能在此处应用。 - Jeff Lockhart
显示剩余3条评论

186

很遗憾,这里没有很好的选择。请记住,所有这些的目标都是为了保持类型的安全性。《Java Generics》提供了一种解决非泛型化遗留库的方法,其中特别介绍了第8.2节中的“空循环技巧”。基本上,进行不安全转换并抑制警告。然后像这样循环遍历地图:

@SuppressWarnings("unchecked")
Map<String, Number> map = getMap();
for (String s : map.keySet());
for (Number n : map.values());

如果遇到意外类型,您将会得到一个运行时的ClassCastException,但至少它会在问题源头附近发生。


7
比skiphoppy提供的答案好很多,原因如下:1)这段代码要短得多。2)这段代码实际上按预期会抛出ClassCastException。3)这段代码不会完全复制源映射。4)循环可以轻松地包装在一个单独的方法中,在断言中使用,这将轻松消除生产代码中的性能损失。 - Stijn de Witt
6
Java编译器或JIT编译器会不会认为这段代码的结果没有被使用,并因此“优化”它而不编译它? - RenniePet
1
如果代码可能会抛出异常,它就不是真正的死代码。我不太了解当前使用的JIT编译器,不能保证它们都不会出错,但我相当有信心地说,它们不应该出错。 - GrandOpener
3
这仍然不能保证类型安全,因为仍然使用了相同的映射。它可能最初被定义为Map<Object,Object>,只是碰巧包含了字符串和数字,如果后来添加布尔值,则这段代码的用户将遇到令人困惑且难以追踪的意外。唯一保证类型安全的方法是将其复制到一个新的映射中,并使用所请求的类型来保证可以放入其中的内容。 - user2219808

114

哇,我想我找到了自己问题的答案。只是我不确定是否值得这么做!:)

问题在于类型转换没有被检查。所以,你必须自己进行检查。你不能仅仅用 instanceof 来检查参数化类型,因为在运行时参数化类型的信息不可用,在编译时已经被擦除了。

但是,你可以对哈希表中的每个元素都使用 instanceof 进行检查,并在此过程中构建一个类型安全的新哈希表。这样,你就不会引发任何警告。

感谢 mmyers 和 Esko Luontola,我将最初编写的代码进行了参数化,这样它就可以被包装在某个实用类中,并用于任何参数化 HashMap。如果你想更好地理解它,并且对泛型不是很熟悉,我鼓励你查看这个答案的编辑历史记录。

public static <K, V> HashMap<K, V> castHash(HashMap input,
                                            Class<K> keyClass,
                                            Class<V> valueClass) {
  HashMap<K, V> output = new HashMap<K, V>();
  if (input == null)
      return output;
  for (Object key: input.keySet().toArray()) {
    if ((key == null) || (keyClass.isAssignableFrom(key.getClass()))) {
        Object value = input.get(key);
        if ((value == null) || (valueClass.isAssignableFrom(value.getClass()))) {
            K k = keyClass.cast(key);
            V v = valueClass.cast(value);
            output.put(k, v);
        } else {
            throw new AssertionError(
                "Cannot cast to HashMap<"+ keyClass.getSimpleName()
                +", "+ valueClass.getSimpleName() +">"
                +", value "+ value +" is not a "+ valueClass.getSimpleName()
            );
        }
    } else {
        throw new AssertionError(
            "Cannot cast to HashMap<"+ keyClass.getSimpleName()
            +", "+ valueClass.getSimpleName() +">"
            +", key "+ key +" is not a " + keyClass.getSimpleName()
        );
    }
  }
  return output;
}

那是很多工作,可能只有微不足道的回报...我不确定我是否会使用它。我很感谢任何关于是否值得做的评论。另外,我也需要改进建议:除了抛出AssertionErrors之外,是否有更好的方法?我可以抛出更好的异常吗?应该将其设置为已检查的异常吗?


71
这些东西很难懂,但我认为你所做的只是将ClassCastExceptions换成了AssertionErrors。 - Dustin Getz
62
这绝对不值得啊!想象一下,那个可怜的家伙需要回来修改这样一团糟的代码。虽然我不喜欢压制警告,但在这种情况下我认为那是较小的恶。 - Craig B
74
它不仅丑陋难懂(当你无法避免时,详尽的注释可以帮助维护程序员理解它),而且在对集合中的每个元素进行迭代时,类型转换操作从O(1)变为O(n)。 这是意料之外的,很容易变成一个可怕的速度下降之谜。 - Dan Is Fiddling By Firelight
23
@DanNeely你是正确的。总的来说,任何人都不应该这样做。 - skiphoppy
4
一些评论...这个方法签名是错误的,因为它没有 "转换" 任何东西,它只是将现有的映射复制到一个新的映射中。此外,它可能可以重构以接受任何映射,而不依赖于 HashMap 本身(即在方法签名中使用 Map 并返回 Map,即使内部类型是 HashMap)。你实际上不需要进行强制类型转换或将其存储到新映射中——如果您不抛出断言错误,则给定的映射现在已经具有正确的类型。创建具有通用类型的新映射是无意义的,因为您仍然可以使其成为原始映射并放入任何内容。 - MetroidFan2002
显示剩余11条评论

52
在Eclipse首选项中,转到Java-> Compiler-> Errors/Warnings-> Generic types,并勾选“忽略不可避免的泛型类型问题”复选框。 这样做满足了问题的意图,即避免Eclipse的警告,虽然可能不符合其精神。

1
啊,谢谢你 :) 我在 javac 中遇到了一个 "uses unchecked or unsafe operations." 的错误,但是添加 @SuppressWarnings("unchecked") 会让 Eclipse 不高兴,声称这种抑制是不必要的。取消勾选此框使 Eclipse 和 javac 表现一致,这正是我想要的。在代码中明确地抑制警告比在 Eclipse 中随处抑制要清晰得多。 - dimo414

29
您可以创建一个类似下面这样的实用类,并使用它来抑制未经检查的警告。
public class Objects {

    /**
     * Helps to avoid using {@code @SuppressWarnings({"unchecked"})} when casting to a generic type.
     */
    @SuppressWarnings({"unchecked"})
    public static <T> T uncheckedCast(Object obj) {
        return (T) obj;
    }
}

您可以按照以下方式使用它:
import static Objects.uncheckedCast;
...

HashMap<String, String> getItems(javax.servlet.http.HttpSession session) {
      return uncheckedCast(session.getAttribute("attributeKey"));
}

这里还有更多关于此的讨论: http://cleveralias.blogs.com/thought_spearmints/2006/01/suppresswarning.html


22
不要点踩,但这个包装对于仅仅抑制警告来说并没有任何意义。 - Dustin Getz
4
由于这种解决方案不浪费宝贵的代码行,因此+1。 - Tino
1
@ErikE 太多了。更昂贵、更大、分辨率更高的显示器,为了给所有这些浪费的行留出空间,需要更大的桌子来放置所有这些更大的显示器,需要更大的房间来容纳更大的桌子,还需要一个有远见的老板... - Tino
1
@ErikE 滚动条,对于 vi你在开玩笑吧? - Tino

23

这东西很难,但以下是我的想法:

如果你的API返回Object,那么无论如何,你都会盲目地将对象转换。你可以让Java抛出ClassCastExceptions,或者自己检查每个元素并抛出Assertions或IllegalArgumentExceptions或类似的异常,但这些运行时检查都是等效的。无论你在运行时做什么,你都必须抑制编译时未经检查的转换。

我更喜欢盲目转换,让JVM为我执行运行时检查,因为我们“知道”API应该返回什么,并且通常愿意假设API有效。在强制类型转换之上到处使用泛型,如果需要的话。在那里,你并没有真正获得任何东西,因为你仍然有一个单一的盲目转换,但至少你可以从那里开始使用泛型,这样JVM可以帮助你避免在代码的其他部分进行盲目转换。

在这种情况下,你可以看到对SetAttribute的调用并看到输入的类型,所以把类型盲目转换为相同的类型也不算不道德。添加一个引用SetAttribute的注释就行了。


18
以下是一个缩短的示例,通过使用其他答案中提到的两种策略避免了“未经检查的强制转换”警告。
1. 在运行时传递感兴趣类型的类作为参数(Class inputElementClazz)。然后可以使用:inputElementClazz.cast(anyObject); 2. 对于集合的类型转换,请使用通配符?而不是泛型类型T来承认您确实不知道从旧代码中可以期望哪些对象(Collection unknownTypeCollection)。毕竟,“未经检查的强制转换”警告要告诉我们的是:我们不能确定我们获得一个Collection ,所以诚实的做法是使用Collection 。如果绝对需要,仍然可以构建已知类型的集合(Collection knownTypeCollection)。
下面的示例中接口化的旧代码在StructuredViewer中具有属性“input”(StructuredViewer是树形或表格小部件,“input”是其背后的数据模型)。这个“input”可以是任何一种Java Collection。
public void dragFinished(StructuredViewer structuredViewer, Class<T> inputElementClazz) {
    IStructuredSelection selection = (IStructuredSelection) structuredViewer.getSelection();
    // legacy code returns an Object from getFirstElement,
    // the developer knows/hopes it is of type inputElementClazz, but the compiler cannot know
    T firstElement = inputElementClazz.cast(selection.getFirstElement());

    // legacy code returns an object from getInput, so we deal with it as a Collection<?>
    Collection<?> unknownTypeCollection = (Collection<?>) structuredViewer.getInput();

    // for some operations we do not even need a collection with known types
    unknownTypeCollection.remove(firstElement);

    // nothing prevents us from building a Collection of a known type, should we really need one
    Collection<T> knownTypeCollection = new ArrayList<T>();
    for (Object object : unknownTypeCollection) {
        T aT = inputElementClazz.cast(object);
        knownTypeCollection.add(aT);
        System.out.println(aT.getClass());
    }

    structuredViewer.refresh();
}

自然地,如果我们使用错误的数据类型(例如将数组作为StructuredViewer的“输入”而不是Java Collection),上述代码可能会导致运行时错误。

调用该方法的示例:

dragFinishedStrategy.dragFinished(viewer, Product.class);

13

在HTTP Session世界中,你无法避免类型转换,因为API是以这种方式编写的(仅接受和返回Object)。

通过一点努力,你可以很容易地避免未经检查的类型转换。这意味着它将转变为传统的转换,在出现错误的事件中会立即给出一个ClassCastException。未经检查的异常可能会在以后的任何时间点上转换为CCE,而不是在转换点上(这就是为什么它是单独的警告的原因)。

用专用类替换HashMap:

import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class Attributes extends AbstractMap<String, String> {
    final Map<String, String> content = new HashMap<String, String>();

    @Override
    public Set<Map.Entry<String, String>> entrySet() {
        return content.entrySet();
    }

    @Override
    public Set<String> keySet() {
        return content.keySet();
    }

    @Override
    public Collection<String> values() {
        return content.values();
    }

    @Override
    public String put(final String key, final String value) {
        return content.put(key, value);
    }
}

那么将其转换为该类而不是 Map<String,String>,一切都将在您编写代码的确切位置进行检查。以后不会出现意外的 ClassCastExceptions


这是非常有帮助的答案。 - GPrathap

10

如果你想在Android Studio中禁用检查,可以使用:

//noinspection unchecked
Map<String, String> myMap = (Map<String, String>) deserializeMap();

8
在Esko Luontola的上述答案中,Objects.Unchecked实用程序函数是避免程序混乱的好方法。
如果您不想在整个方法上使用SuppressWarnings,Java会强制您将其放在本地变量上。如果需要对成员进行转换,则可能会导致以下代码:
@SuppressWarnings("unchecked")
Vector<String> watchedSymbolsClone = (Vector<String>) watchedSymbols.clone();
this.watchedSymbols = watchedSymbolsClone;

使用该实用程序更加简洁,而且你所做的事情仍然很明显:
this.watchedSymbols = Objects.uncheckedCast(watchedSymbols.clone());

注意:我认为有必要补充一点,有时候警告确实意味着你正在做一些错误的事情,比如:

ArrayList<Integer> intList = new ArrayList<Integer>();
intList.add(1);
Object intListObject = intList; 

 // this line gives an unchecked warning - but no runtime error
ArrayList<String> stringList  = (ArrayList<String>) intListObject;
System.out.println(stringList.get(0)); // cast exception will be given here

编译器告诉你的是,这个强制转换将不会在运行时进行检查,因此在尝试访问泛型容器中的数据之前不会引发任何运行时错误。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接