如何在 Java 程序中使用泛型

如何在 Java 程序中使用泛型
泛型可以使你的代码更灵活、更易读,并能帮助你在运行时避免 ClassCastExceptions。让我们通过这篇结合 Java 集合框架的泛型入门指南,开启你的泛型之旅。

Java 5 引入的泛型增强了代码的类型安全性并提升了可读性。它能帮助你避免诸如 ClassCastException(当尝试将对象强制转换为不兼容类型时引发的异常)这类运行时错误。
本教程将解析泛型概念,通过三个结合 Java 集合框架的实例演示其应用。同时我们将介绍原始类型(raw types),探讨选择使用原始类型而非泛型的场景及其潜在风险。
Java 编程中的泛型
为何使用泛型?
如何利用泛型保障类型安全
Java 集合框架中的泛型应用
Java 泛型类型示例
原始类型与泛型对比
为何使用泛型?
泛型在 Java 集合框架中被广泛用于 java.util.List、java.util.Set 和 java.util.Map 等接口。它们也存在于 Java 其他领域,如 java.lang.Class、java.lang.Comparable 和 java.lang.ThreadLocal。
在泛型出现前,Java 代码常缺乏类型安全保障。以下是非泛型时代 Java 代码的典型示例:
这段代码意图存储 Integer 对象,但没有任何机制阻止你添加其他类型(如字符串):
当尝试将 String 强制转换为 Integer 时,这段代码会在运行时抛出 ClassCastException。
利用泛型保障类型安全
为解决上述问题并避免 ClassCastExceptions,我们可以使用泛型指定列表允许存储的对象类型。此时无需手动类型转换,代码更安全且更易理解:
List<Integer>表示"存储 Integer 对象的列表"。基于此声明,编译器确保只有 Integer 对象能被添加至列表,既消除了类型转换需求,也预防了类型错误。
Java 集合框架中的泛型
泛型深度集成于 Java 集合框架,提供编译时类型检查并消除显式类型转换需求。当使用带泛型的集合时,你需指定集合可容纳的元素类型。Java 编译器基于此规范确保你不会意外插入不兼容对象,从而减少错误并提升代码可读性。
为演示泛型在 Java 集合框架中的使用,让我们观察几个实例。
List 和 ArrayList 的泛型应用
前例已简要展示 ArrayList 的基本用法。现在让我们通过 List 接口的声明深入理解这一概念:
此处声明泛型变量为"E",该变量可被任何对象类型替代。注意变量 E 代表元素(Element)。
接下来演示如何用具体类型替换<E>变量。下例中将<E>替换为<String>:
List<String>声明该列表仅能存储 String 对象。如代码最后一行所示,尝试添加 Integer 将引发编译错误。
Set 和 HashSet 的泛型应用
Set 接口与 List 类似:
我们将用<Double>替换<E>,使集合只能存储 Double 值:
Set<Double>确保只有 Double 值能被添加至集合,防止因错误类型转换引发的运行时错误。
Map 和 HashMap 的泛型应用
我们可以声明任意数量的泛型类型。以键值数据结构 Map 为例,K 代表键(Key),V 代表值(Value):
现在用 String 替换 K 作为键类型,用 Integer 替换 V 作为值类型:
此例展示将 String 键映射到 Integer 值的 HashMap。添加 Integer 类型的键将不被允许并导致编译错误。
泛型命名规范
我们可以在任何类中声明泛型类型。虽然可以使用任意名称,但建议遵循命名规范:
E 代表元素(Element)
K 代表键(Key)
V 代表值(Value)
T 代表类型(Type)
应避免使用无意义的"X"、"Y"或"Z"等名称。
Java 泛型类型使用示例
现在通过更多示例深入演示 Java 中泛型类型的声明与使用。
创建通用对象容器
我们可以在自定义类中声明泛型类型,不必局限于集合类型。下例中,Box 类通过声明泛型类型 E 来操作任意元素类型。注意泛型类型 E 声明于类名之后,随后即可作为属性、构造器、方法参数和返回类型使用:
输出结果:
代码要点:
Box 类使用类型参数 E 作为容器存储对象的占位符,允许 Box 处理任意对象类型
构造器初始化 Box 实例时接受指定类型对象,确保类型安全
getContent 返回与实例创建时指定的泛型类型匹配的对象,无需类型转换
setContent 通过类型参数 E 确保只能设置正确类型的对象
main 方法创建了存储 Integer 和 String 的 Box 实例
每个 Box 实例操作特定数据类型,展现泛型在类型安全方面的优势
此例展示了 Java 泛型的基础实现,演示了如何以类型安全方式创建和操作任意类型对象。
处理多数据类型
我们可以声明多个泛型类型。以下 Pair 类包含<K, V>泛型值。如需更多泛型参数,可扩展为<K, V, V1, V2, V3>等,代码仍可正常编译。
Pair 类示例:
输出结果:
代码要点:
Pair<K, V>类包含两个类型参数,适用于任意数据类型组合
构造器与方法使用类型参数实现严格类型检查
创建存储 String(姓名)和 Integer(年龄)的 Pair 对象
访问器和修改器方法操作 Pair 数据
Pair 类可存储管理关联信息而不受特定类型限制,展现泛型的灵活性与强大功能
此例展示泛型如何创建支持多数据类型的可复用类型安全组件,提升代码复用性和可维护性。
让我们再看一个示例。
方法级泛型声明
泛型类型可直接在方法中声明,无需在类级别定义。若某个泛型类型仅用于特定方法,可在方法签名返回类型前声明:
输出结果:
原始类型与泛型对比
原始类型指未指定类型参数的泛型类或接口名称。在 Java 5 引入泛型前,原始类型被广泛使用。现今开发者通常仅在与遗留代码兼容或与非泛型 API 交互时使用原始类型。即使使用泛型,仍需了解如何识别和处理原始类型。
典型原始类型示例——未指定类型参数的 List 声明:
此处 List rawList 声明了一个未指定泛型参数的列表。rawList 可存储任意类型对象(Integer、String、Double 等)。由于未指定类型,编译器不会对添加至列表的对象类型进行检查。
使用原始类型的编译警告
Java 编译器会对原始类型使用发出警告,提醒开发者可能存在的类型安全隐患。当使用泛型时,编译器会检查集合(如 List、Set)中存储的对象类型、方法返回类型和参数是否匹配声明类型,从而预防如 ClassCastException 的常见错误。
使用原始类型时,由于未指定存储对象类型,编译器无法进行类型检查,因此会发出警告提示你绕过了泛型提供的类型安全机制。
编译警告示例
以下代码演示编译器如何对原始类型发出警告:
编译时通常会显示:
使用-Xlint:unchecked 参数编译将显示更详细警告:
若确信使用原始类型不会引入风险,或处理无法重构的遗留代码,可使用 @SuppressWarnings("unchecked")注解抑制警告。但需谨慎使用,避免掩盖真实问题。
使用原始类型的后果
尽管原始类型有助于向后兼容,但存在两大缺陷:类型安全性缺失和维护成本增加。
类型安全性缺失:泛型的核心优势是类型安全,使用原始类型将丧失这一优势。编译器不进行类型正确性检查,可能导致运行时 ClassCastException。
维护成本增加:使用原始类型的代码缺乏泛型提供的明确类型信息,维护难度加大,易产生仅在运行时暴露的错误。
类型安全问题示例:使用原始类型 List 而非泛型 List<String>时,编译器允许添加任意类型对象。当从列表检索元素并尝试强制转换为 String 时,若实际为其他类型将导致运行时错误。
泛型知识要点回顾
泛型以高度灵活性提供类型安全保障。以下回顾关键要点:
泛型是什么?为何使用?
code.Java 5 引入泛型以提升代码类型安全性和灵活性
主要优势在于帮助避免 ClassCastException 等运行时错误
泛型广泛应用于 Java 集合框架,也见于 Class、Comparable、ThreadLocal 等组件
通过阻止不兼容类型插入实现类型安全
Java 集合中的泛型
List 和 ArrayList:List<E>允许指定元素类型 E,确保列表类型专一
Set 和 HashSet:Set<E>限定元素为类型 E,保持一致性
Map 和 HashMap:Map<K,V>定义键值类型,提升类型安全性和代码清晰度
泛型使用优势
通过阻止不兼容类型插入减少错误
明确类型关联提升代码可读性和可维护性
便于以类型安全方式创建和管理集合等数据结构
版权声明: 本文为 InfoQ 作者【码语者】的原创文章。
原文链接:【http://xie.infoq.cn/article/bef51eb25bd88a30bf49f5cc3】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论