1. 引言

本文将深入探讨 Java 中的 Collections.checkedXXX() 方法,展示它们如何帮助及早捕获类型不匹配问题、防止 bug 并提升代码可维护性。

在 Java 开发中,类型安全是避免运行时错误和确保代码可靠性的关键。这些方法为集合提供了运行时类型安全机制。我们将详细解析各种 Collections.checkedXXX() 方法及其在 Java 应用中的有效使用方式。

2. 理解 Java 集合的类型安全

Java 集合的类型安全对于防止运行时错误至关重要,它能确保集合只包含特定类型的元素。Java 泛型(Java 5 引入)提供了编译时类型检查,使我们能定义特定类型的集合。例如 List<String> 确保只能向列表添加字符串。

然而,当处理原始类型未检查操作或未使用泛型的遗留代码时,类型安全会被破坏。这时 Collections.checkedXXX() 方法就派上用场了。这些方法通过动态类型检查包装集合,在运行时强制类型安全。

例如,Collections.checkedList(new ArrayList(), String.class) 返回一个列表,当尝试添加非字符串元素时会抛出 ClassCastException这种额外的运行时检查层补充了编译时检查,能及早捕获类型不匹配问题,使代码更健壮。

在通过 API 暴露集合或处理由外部源填充的集合时,这些方法尤其有用。 它们帮助确保集合中的元素符合预期类型,降低 bug 风险,简化调试和维护工作。

下面我们来学习这些方法。

3. 理解 Collections.checkedCollection() 方法

首先查看该方法签名:

public static <E> Collection<E> checkedCollection(Collection<E> c, Class<E> type)

该方法返回指定集合的动态类型安全视图。 如果尝试插入错误类型的元素,会立即抛出 ClassCastException。假设在生成动态类型安全视图前集合不包含错误类型元素,且所有后续访问都通过该视图进行,则能保证集合不会包含错误类型元素。

Java 语言的泛型机制提供编译时(静态)类型检查,但通过未检查转换绕过此机制是可能的。通常这不是问题,因为编译器会对未检查操作发出警告。

但有些场景下我们需要超越静态类型检查。 例如,当我们将集合传递给第三方库时,需要确保库代码不会通过插入错误类型元素破坏集合。

如果用动态类型安全视图包装集合,可以快速定位问题源头。

例如有如下声明:

Collection<String> c = new Collection<>();

可替换为以下表达式(将原始集合包装为受检集合):

Collection<String> c = Collections.checkedCollection(new Collection(), String.class);

重新运行程序时,当尝试向集合插入错误类型元素会立即失败。这清晰地展示了问题位置。

使用动态类型安全视图对调试也有帮助。 例如,当程序遇到 ClassCastException 时,必然是向参数化集合添加了错误类型元素。但异常可能在插入不当元素后的任何时间点发生,这几乎无法提供关于问题实际来源的信息。

4. 使用 Collections.checkedCollection() 方法

现在了解如何使用该方法。

假设有一个数据验证工具类:

class DataProcessor {

    public boolean checkPrefix(Collection<?> data) {
        boolean result = true;
        if (data != null) {
            for (Object item : data) {
                if (item != null && !((String) item).startsWith("DATA_")) {
                    result = false;
                    break;
                }
            }
        }
        return result;
    }
}

checkPrefix() 方法检查集合元素是否以 "DATA_" 前缀开头。它期望元素非空且为 String 类型。

测试如下:

@Test
void givenGenericCollection_whenInvalidTypeDataAdded_thenFailsAfterInvocation() {
    Collection data = new ArrayList<>();
    data.add("DATA_ONE");
    data.add("DATA_TWO");
    data.add(3); // 应该在这里失败

    DataProcessor dataProcessor = new DataProcessor();

    assertThrows(ClassCastException.class, () -> dataProcessor.checkPrefix(data)); // 但实际在这里失败
}

测试向泛型集合添加了 StringInteger,期望处理时抛出 ClassCastException。但由于集合未进行类型检查,错误发生在 checkPrefix() 方法而非添加时。

现在看 checkedCollection() 如何在尝试添加错误类型元素时及早捕获错误:

@Test
void givenGenericCollection_whenInvalidTypeDataAdded_thenFailsAfterAdding() {
    Collection data = Collections.checkedCollection(new ArrayList<>(), String.class);
    data.add("DATA_ONE");
    data.add("DATA_TWO");

    assertThrows(ClassCastException.class, () -> {
      data.add(3); // 在这里失败
    });

    DataProcessor dataProcessor = new DataProcessor();
    boolean result = dataProcessor.checkPrefix(data);
    assertTrue(result);
}

测试使用 Collections.checkedCollection() 确保只添加字符串。当尝试添加整数时立即抛出 ClassCastException,在到达 checkPrefix() 前就强制了类型安全。

⚠️ 注意:不能为此集合指定类型参数,否则会破坏契约并导致 IDE 或语法检查器报错。

Collections 类提供了多个 checkedXXX 方法,如 checkedList()checkedMap()checkedSet()checkedQueue()checkedNavigableMap()checkedNavigableSet()checkedSortedMap()checkedSortedSet()。这些方法为不同集合类型在运行时强制类型安全。它们通过类型检查包装集合,确保只添加指定类型元素,帮助防止 ClassCastException 并维护类型完整性。

5. 关于返回集合的注意事项

返回的集合不会将 hashCode()equals() 操作委托给底层集合,而是依赖 Object 类的 equals()hashCode() 方法。这种方式确保了这些操作的契约得以保留,尤其当底层集合是 Set 或 List 时。

此外,如果指定集合可序列化,方法返回的集合也可序列化

需要特别注意:由于 null 被视为任何引用类型的值,只要底层集合允许,返回的集合也允许插入 null 元素

6. 结论

本文探讨了 Collections.checkedXXX 方法,展示了它们如何在 Java 集合中强制运行时类型安全。我们看到了 checkedCollection() 如何通过确保只添加指定类型元素来防止类型错误。

使用这些方法能提升代码可靠性,及早捕获类型不匹配问题。通过善用这些工具,我们可以编写更安全、更健壮的代码,并获得更好的运行时类型检查。

完整源代码可在 GitHub 获取。


原始标题:Ensuring Type Safety With Collections.checkedXXX() in Java | Baeldung