问题

日常在写 Java 代码时对警告 Type safety: Unchecked cast from XXX to YYY 一定不会陌生,例如 Type safety: Unchecked cast from Object to Map<String,String>。如果仔细观察的话,可以注意到,YYY 从来不会是一个非泛型的类型。

原因

产生这个警告的原因是在强制类型转换时目标类型是一个非无边界通配符的泛型类型,而 Java 的半残泛型又无法在运行时判断一个泛型类型是否匹配目标类型。举个例子来说就是:

Object source = Collections.singletonMap("a", 2);  // #1
var target = (Map<String, String>) source;         // #2
var value = target.get("a");                       // #3

对于上面的代码,无法在 #2 行代码执行时就确认 target 是一个完全通过类型检查的值,当 #3 行代码执行时,还是会产生 java.lang.ClassCastException 异常;而从直觉上,如果 #2 行没有编译期错误或者运行时 java.lang.ClassCastException异常,那 target 就是 Map<String, String> 类型的。在这个矛盾之下,在 #2 处产生 Type safety: Unchecked cast 警告提醒该行可能有问题,也是一种没有办法的办法。

前面提到了非无边界通配符的泛型类型,如果目标类型是无边界通配符的泛型类型,例如:

Object source = Collections.singletonMap("a", 2);  // #1
var target = (Map<?, ?>) source;                   // #2

上面代码中的 #2 行不会产生警告,因为这将指定具体的泛型参数类型这件事推迟到了后面的代码中,在这里只需要确定 source 是 Map 类型即可。

解决

1. 解决提出问题的人

最容易想到的,也是最普遍采用的方法自然是——当解决问题有困难时,解决提出问题的人。当然,我这是开个玩笑,虽然大家都知道编码实践中这才是主流。

例如在 Eclipse 中,这个警告受 Preferences > Java > Compiler > Errors/Warnings > Generic types > Unchecked generic type operation 选项控制,将其关闭即可。

对这个警告来说,由于它非常特定于泛型类型的转换,并且在这个问题上是整体没有好的解决方案,因此关掉也不会有什么实质性的影响。但是因为没有通用的关掉警告的方法,这种不可移植的方案难以让人接受。

另外,@SuppressWarnings("unchecked") 也可以让提出问题的人闭嘴。我希望它是被用在变量上面而不是方法上甚至模块上。使用这个注解的问题是,当对代码质量要求很高时,这个注解通常是被配置为忽略的,而试图消除警告的场景又往往与之高度重合。

2. 严谨的处理办法

前面提到了,无边界通配符类型因为延迟了类型指定而彻底符合了要求,我们可以把类型转换拆成多步,保证每一步都只转换确定的类型。当然,在这个场景下这叫没有困难创造困难也要上。

Object source = Collections.singletonMap("a", 2);  // #1
var target = (Map<?, ?>) source;                   // #2
Object t = target.get("a");                        // #3
                                                   // #4
if (t instanceof String) {                         // #5
    String value = (String) t;                     // #6
}                                                  // #7

其中的繁琐和局限性一看便知。需要特别注意的是 #3 处 = 的右边失去了强类型的好处,似乎有些得不偿失。

3. 一个还算不错的处理方式

从前面给出的原因可以看到,在眼下以及可预见的未来,都没有简单而彻底的解决方法。好在这个警告主要是给有代码洁癖人带来麻烦,可以用一种代码级的方法几乎完整解决问题。只需要写一个工具类

public final class CastUtils {

    @SuppressWarnings("unchecked")
    public static <T> T cast(Object obj) {
        return (T) obj;
    }

    private CastUtils() {
        throw new UnsupportedOperationException();
    }

}

原来有警告的代码改为

Object source = Collections.singletonMap("a", 2);           // #1
Map<String, String> target1= CastUtils.cast(source);        // #2
var target2 = CastUtils.<Map<String, String>>cast(source);  // #3
var value1 = target1.get("a");                              // #4
var value2 = target2.get("a");                              // #5

注意 #2 行、#3 行的不同写法,它们都能消除警告,但是 #2 写法更短,在绝大多数场景下更优一点。

CastUtils 类中依然使用到了 @SuppressWarnings("unchecked") 注解,如果该注解没有被配置为忽略,就能完全消除该警告,如果被配置为忽略,那也可以保持整个代码中仅有 1 处该警告,也就是几乎完整解决了问题。当然,还有另外一种选择,将 CastUtils 置于某个工具类库 jar 文件中,再把 jar 文件引用到项目中,无论 @SuppressWarnings("unchecked") 是否被配置为忽略,都可以彻底消除该警告。

总结

Type safety: Unchecked cast 是个说严重不严重,但是对整洁代码来说却不能无视的警告。如果自己有工具类库,在工具类库里增加 CastUtils 工具类是最好的选择。次优的选择则是将 CastUtils 工具类直接置于项目代码中。