Java 泛型

2025年4月21日 | 阅读 10 分钟

Java 泛型,在 J2SE 5.0 中引入,用于处理类型安全的类对象,提供了一种使用占位符来定义类、接口和方法的方式。此功能允许创建灵活且可重用的代码组件,这些组件可以在保持编译时类型安全的同时,与不同的数据类型配合使用。它通过在编译时检测错误来使代码更稳定。

在泛型出现之前,我们可以在集合中存储任何类型的对象,即非泛型。现在,泛型强制 Java 程序员存储特定类型的对象。

为什么要使用泛型?

泛型解决了 Java 编程中的几个关键问题

  • 类型安全: 类型安全是 Java 编程的一个关键方面。在泛型出现之前,Java 中的集合可以存储任何类型的对象,这意味着类型错误只能在运行时捕获,而不能在编译时捕获。缺乏类型安全性可能导致在将对象转换为错误类型时发生 ClassCastException 错误。
  • 代码可重用性: 泛型允许创建更通用、更可重用的代码。通过参数化类型,我们可以编写能够操作任何类型的类和方法,从而减少代码重复并提高灵活性。
  • 消除类型转换: 泛型减少了显式类型转换的需要,使代码更简洁,不易出错。没有泛型,开发人员经常需要将对象转换为所需的类型,这会导致代码混乱并可能出现运行时错误。
  • 启用泛型算法: 泛型可以创建能够与任何类型配合使用的泛型算法,从而增强了代码的通用性。这在集合和其他数据结构中特别有用。
  • 增强 Java 集合框架: 泛型的引入极大地增强了 Java 集合框架,使其功能更强大、类型更安全。集合现在可以根据特定类型进行参数化,从而确保类型安全并降低运行时错误的风险。
  • 支持更具可读性和可维护性的代码: 泛型通过明确说明正在使用的类型,使代码更具可读性和可维护性。这种清晰度有助于其他开发人员理解集合和方法的预期用途,从而降低出错的可能性。
  • 实现类型推断: Java 的类型推断机制与泛型配合良好,允许编译器在许多情况下推断出类型参数,从而简化了开发人员的代码。
  • 向后兼容性: Java 中的泛型通过类型擦除设计,以确保与旧版本 Java 的向后兼容性。类型擦除意味着泛型类型信息仅在编译时可用,并在运行时被删除。它允许泛型代码与不使用泛型的旧代码进行互操作。

Java 泛型的优势

泛型主要有以下三个优势:

1) 类型安全: 泛型只能存储单一类型的对象。不允许存储其他对象。

没有泛型,我们可以存储任何类型的对象。

2) 无需类型转换: 对象无需进行类型转换。

在没有泛型的情况下,我们需要进行类型转换。

3) 编译时检查: 在编译时进行检查,因此不会在运行时出现问题。良好的编程策略认为,在编译时处理问题远胜于在运行时处理。

使用泛型集合的语法

使用 Java 泛型的示例

Java 泛型的完整示例

在这里,我们使用了 ArrayList 类,但我们可以使用任何集合类,例如 ArrayList、LinkedList、HashSet、TreeSet、HashMap、Comparator 等。

示例

编译并运行

输出

element is: jai
rahul
jai 

使用 Map 的 Java 泛型示例

现在我们将使用泛型来使用 Map 元素。在这里,我们需要传递键和值。让我们通过一个简单的例子来理解它。

示例

编译并运行

输出

1 vijay
2 ankit 
4 umesh

泛型类

可以引用任何类型的类称为泛型类。在这里,我们使用 T 类型参数来创建特定类型的泛型类。

让我们看一个创建和使用泛型类的简单示例。

创建泛型类

T 类型表示它可以引用任何类型(如 String、Integer 和 Employee)。您为类指定的类型将用于存储和检索数据。

使用泛型类

让我们看看使用泛型类的代码。

示例

输出

2

类型参数

类型参数命名约定对于彻底学习泛型非常重要。常见的类型参数如下:

  1. T - 类型
  2. E - 元素
  3. K - 键
  4. N - 数字
  5. V - 值

泛型方法

与泛型类一样,我们可以创建泛型方法,该方法可以接受任何类型的参数。在这里,参数的范围仅限于声明它的方法。它允许静态方法和非静态方法。

让我们看一个打印数组元素的 Java 泛型方法的简单示例。我们在这里使用 E 来表示元素。

示例

编译并运行

输出

Printing Integer Array
10
20
30
40
50
Printing Character Array
J
A
V
A
T
P
O
I
N
T

Java 泛型中的通配符

?(问号)符号代表通配符元素。它表示任何类型。如果我们写 <? extends Number>,它表示 Number 的任何子类。例如,Integer、Float 和 double。现在我们可以通过任何子类对象调用 Number 类的该方法。

我们可以使用通配符作为参数、字段、返回类型或局部变量的类型。但是,不允许将通配符用作泛型方法调用、泛型类实例创建或超类型的类型参数

让我们通过下面的例子来理解它

示例

输出

drawing rectangle
drawing circle
drawing circle

上界通配符

上界通配符的目的是减少对变量的限制。它限制未知类型为特定类型或该类型的子类型。通过声明通配符字符(“?”)后跟 extends(如果是类)或 implements(如果是接口)关键字,后跟其上界来使用它。

语法

此处,

? 是一个通配符字符。

extends 是一个关键字。

Number 是 `java.lang` 包中存在的一个类。

假设我们想为 Number 及其子类型的列表(如 Integer、Double)编写方法。使用 **List<? extends Number>** 适用于类型为 Number 或其任何子类的列表,而 **List<Number>** 仅适用于类型为 Number 的列表。因此,**List<? extends Number>** 的限制比 **List<Number>** 少。

上界通配符示例

在此示例中,我们使用上界通配符为 List<Integer> 和 List<Double> 编写方法。

示例

编译并运行

输出

displaying the sum= 30.0
displaying the sum= 70.0

无界通配符

无界通配符类型表示未知类型的列表,例如 List<?>。这种方法在以下情况下可能很有用:

  • 当使用 Object 类提供的功能来实现给定方法时。
  • 当泛型类包含不依赖于类型参数的方法时。

无界通配符示例

示例

编译并运行

输出

displaying the Integer values
1
2
3
displaying the String values
One
Two
Three

下界通配符

下界通配符的目的是限制未知类型为特定类型或该类型的超类型。通过声明通配符字符(“?”)后跟 super 关键字,后跟其下界来使用它。

语法

此处,

? 是一个通配符字符。

super 是一个关键字。

Integer 是一个包装类。

假设我们想为 Integer 及其超类型的列表(如 Number、Object)编写方法。使用 **List<? super Integer>** 适用于类型为 Integer 或其任何超类的列表,而 **List<Integer>** 仅适用于类型为 Integer 的列表。因此,**List<? super Integer>** 的限制比 **List<Integer>** 少。

下界通配符示例

在此示例中,我们使用下界通配符为 List<Integer> 和 List<Number> 编写方法。

示例

编译并运行

输出

displaying the Integer values
1
2
3
displaying the Number values
1.0
2.0
3.0

Java 泛型的缺点

  • 类型擦除: Java 泛型的一个基本限制是类型擦除。此设计选择可确保与旧版本 Java 的向后兼容性,但会引入几个问题:
  • 无运行时类型信息: 由于类型信息在运行时被擦除,因此不能使用泛型类型来获取特定类型的运行时信息。此限制意味着您无法在运行时使用反射直接检查泛型实例的类型参数。
  • 类型转换和类型安全: 类型擦除有时需要类型转换,如果类型处理不当,可能会在运行时引入 ClassCastException。

复杂性和学习曲线

泛型为 Java 语言增加了复杂性,这对于开发人员正确学习和使用来说可能是一个挑战。

  • 语法复杂性: 泛型的语法可能很复杂且冗长,尤其是在处理有界类型参数、通配符和嵌套泛型类型时。这种复杂性会使代码更难阅读和理解,特别是对于新开发人员而言。
  • 调试困难: 调试使用泛型的代码可能更具挑战性。由于类型信息在运行时被擦除,与类型问题相关的错误消息可能信息量较少,并且更难追溯到源头。
  • 高级功能: 像有界通配符(<? extends T> 和 <? super T>)、泛型方法和泛型构造函数这样的功能可能很难掌握并在实际场景中正确应用。

限制和局限性

泛型带来了一些限制,这些限制可能会限制其灵活性和可用性。

  • 不能使用基本类型: Java 泛型不直接支持基本类型。您必须使用 Integer 和 Double 等包装类而不是 int 和 double,这可能导致额外的装箱和拆箱开销。

下一主题Java 注解