1. 简介

在编译 Java 源代码时,有时会看到编译器输出类似这样的警告信息:

“unchecked conversion” 或 “The expression of type List needs unchecked conversion”

这类警告提示我们,编译器在类型转换过程中没有进行完整的类型检查。本文将深入探讨这个警告的含义、可能引发的问题,以及如何正确处理它。

2. 开启 unchecked 警告选项

在深入分析这个警告前,我们先确认是否已经开启了相关的编译器警告选项。

✅ 如果你使用的是 Eclipse JDT 编译器,这个警告默认是开启的。

❌ 如果你使用的是 Oracle 或 OpenJDK 的 javac 编译器,则需要手动添加 -Xlint:unchecked 选项来启用该警告。

在 IDE 中配置

大多数开发者使用 IDE 编写代码,可以在 IDE 的编译器设置中添加该选项。例如,在 JetBrains IntelliJ IDEA 中的配置如下图所示:

screenshot_2021-01-21_22-27-48

在 Maven 中配置

如果你使用 Apache Maven 构建项目,可以通过配置 maven-compiler-plugin 插件的 compilerArguments 来启用该选项:

<build>
...
    <plugins>
    ...
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            ...
            <configuration>
                ...
                <compilerArguments>
                    <Xlint:unchecked/>
                </compilerArguments>
            </configuration>
        </plugin>
    </plugins>
</build>

现在我们已经确认开启了相关警告,接下来我们看看这个警告会在什么情况下出现。

3. 什么时候会出现 “unchecked conversion” 警告?

这个警告是一个 编译时警告,通常出现在你将一个 原始类型(raw type)赋值给一个泛型类型(parameterized type) 的时候,而没有进行类型检查。

编译器之所以允许这种赋值,是为了 保持与不支持泛型的老版本 Java 的兼容性

来看一个例子。假设我们有一个返回原始类型 List 的方法:

public class UncheckedConversion {
    public static List getRawList() {
        List result = new ArrayList();
        result.add("I am the 1st String.");
        result.add("I am the 2nd String.");
        result.add("I am the 3rd String.");
        return result;
    }
...
}

然后我们调用这个方法,并将其赋值给一个泛型类型 List<String>

@Test
public void givenRawList_whenAssignToTypedList_shouldHaveCompilerWarning() {
    List<String> fromRawList = UncheckedConversion.getRawList();
    Assert.assertEquals(3, fromRawList.size());
    Assert.assertEquals("I am the 1st String.", fromRawList.get(0));
}

编译这段代码时,就会看到如下警告:

[WARNING] .../UncheckedConversionDemoUnitTest.java:[12,66] unchecked conversion
  required: java.util.List<java.lang.String>
  found:    java.util.List

一个常见的实际场景是使用 Java Persistence APIQuery.getResultList() 方法。该方法返回的是原始类型 List,当我们试图将其赋值给泛型类型时,就会触发这个警告:

List<MyEntity> results = entityManager.createNativeQuery("... SQL ...", MyEntity.class).getResultList();

虽然程序可以正常运行,但潜在风险依然存在。我们接着看。

4. 为什么编译器会发出警告?

虽然上一节的测试方法能正常运行,但这只是因为 getRawList() 方法只添加了 String 类型元素。

如果我们修改一下方法,加入一个 Date 对象:

public static List getRawListWithMixedTypes() {
    List result = new ArrayList();
    result.add("I am the 1st String.");
    result.add("I am the 2nd String.");
    result.add("I am the 3rd String.");
    result.add(new Date());
    return result;
}

然后测试:

@Test(expected = ClassCastException.class)
public void givenRawList_whenListHasMixedType_shouldThrowClassCastException() {
    List<String> fromRawList = UncheckedConversion.getRawListWithMixedTypes();
    Assert.assertEquals(4, fromRawList.size());
    Assert.assertFalse(fromRawList.get(3).endsWith("String."));
}

运行测试时,会再次看到 “unchecked conversion” 警告,并且最终会抛出 ClassCastException

⚠️ 在实际项目中,这种异常可能在程序运行到一半才抛出,导致已经处理了部分数据,造成脏数据或事务回滚失败等严重后果。

5. 如何处理这个警告?

5.1. 修改原始方法为泛型方法(推荐)

如果可以修改原始方法,应该将其改造成泛型方法,这样可以从根本上保证类型安全。

5.2. 如果无法修改(如第三方库)

如果你无法修改原始方法,只能在调用时处理这个警告。

✅ 使用 @SuppressWarnings("unchecked") 抑制警告

只有在你 完全确定类型转换是安全的 情况下,才使用这个注解:

Query query = entityManager.createQuery("SELECT e.field1, e.field2, e.field3 FROM SomeEntity e");
@SuppressWarnings("unchecked")
List<Object[]> list = query.list();

✅ 手动进行类型检查

可以通过遍历原始集合并手动进行类型转换,来避免潜在的 ClassCastException

下面是一个通用的类型转换方法,会过滤掉类型不匹配的元素:

public static <T> List<T> castList(Class<? extends T> clazz, Collection<?> rawCollection) {
    List<T> result = new ArrayList<>(rawCollection.size());
    for (Object o : rawCollection) {
        try {
            result.add(clazz.cast(o));
        } catch (ClassCastException e) {
            // log the exception or other error handling
        }
    }
    return result;
}

测试方法如下:

@Test
public void givenRawList_whenAssignToTypedListAfterCallingCastList_shouldOnlyHaveElementsWithExpectedType() {
    List rawList = UncheckedConversion.getRawListWithMixedTypes();
    List<String> strList = UncheckedConversion.castList(String.class, rawList);
    Assert.assertEquals(4, rawList.size());
    Assert.assertEquals("One element with the wrong type has been filtered out.", 3, strList.size());
    Assert.assertTrue(strList.stream().allMatch(el -> el.endsWith("String.")));
}

或者,如果你希望在遇到类型错误时立即抛出异常,可以使用这个版本:

public static <T> List<T> castList2(Class<? extends T> clazz, Collection<?> rawCollection) 
  throws ClassCastException {
    List<T> result = new ArrayList<>(rawCollection.size());
    for (Object o : rawCollection) {
        result.add(clazz.cast(o));
    }
    return result;
}

测试方法:

@Test(expected = ClassCastException.class)
public void givenRawListWithWrongType_whenAssignToTypedListAfterCallingCastList2_shouldThrowException() {
    List rawList = UncheckedConversion.getRawListWithMixedTypes();
    UncheckedConversion.castList2(String.class, rawList);
}

6. 总结

  • “unchecked conversion” 是一个编译时警告,表示类型转换未经检查。
  • 它通常出现在将原始类型赋值给泛型类型时。
  • 虽然程序可能正常运行,但存在运行时类型转换失败的风险。
  • 最好的做法是使用泛型方法来避免警告。
  • 如果无法修改源代码,可以使用 @SuppressWarnings("unchecked") 或手动类型检查来规避风险。

一如既往,本文中的所有代码都可以在 GitHub 上找到。


原始标题:Java Warning “unchecked conversion” | Baeldung