Java 泛型(generics)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。下面给一个 Java 泛型的简单例子:
public static void printArray(E[] inputArray){ // 这里的 E 可代表任意类型
for (E element: inputArray){ // 输出数组中的元素,无论是什么类型
System.out.println(element);
}
}
不仅仅在 Java 中有泛型的概念,在 C++ 中也有类似的概念,叫做模板。当然,一般来说只有静态语言才说有类似于泛型的语法,其实不然,对于动态语言也有类似的语法,但用途不太相同,如 Python3.12 中的新类型提示语法,类型形参语法,就有点像仿 Java 泛型语法而成的类型提示语法。
下面给几个例子以进行对比学习:
C++ 模板编程
template // 这里的 T 与 Java 泛型中的 T 类似
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
// C++ 的“泛型”有其独特性,语法与 Java 相比不太一样,毕竟 C++ 中这称之为模板
Python 类型形参语法
def print_array[T](array: list[T]) -> None: # 此处的 T 只是为了类型提示,无实际功能,可认为是特殊的注释
""" Type Parameter Syntax """
print(*array)
# Java 用的是尖括号("<>")表示,而 Python 则是用方括号("[]")表示
所有的泛型声明都是一对尖括号("<>")加上泛型标记符以及范围限定的关键字,多个标记符之间用逗号隔开。
Java 的泛型标记符都是约定俗成的,没有说强制某一种泛型标记符表示什么含义,只是按照约定俗成的来可以让其他人更容易理解你的代码。
下面是一些常见的泛型标记符:
标记符 | 约定全称 | 描述 |
E | Element | 在集合中使用,因为集合中的都是元素(element) |
T | Type | 表示任意 Java 的引用类型 |
K | Key | 在字典中使用,表示键(key) |
V | Value | 在字典中使用,表示值(value) |
N | Number | 表示数字(number)类型 |
U | Unbounded | 无限制类型通配符,常用于泛型方法和泛型类的定义中 |
? | ? | 无限制类型通配符,常用于泛型方法的返回类型声明和方法参数中 |
另外,这里强调一点,上面的常用标记符只是约定俗成的,标记符并没有强制只能是一个字符,多个字符也是可以的,比如 abc,不过标记符还是要满足类型的写法,毕竟其本质也只是表示类型而已,因此一般使用大写开头。
下面是一个简单的示例:
public static void print(K key, V value){ // 这里举的例子是:泛型方法参数
System.out.println(key + " : " + value);
}
当然,System.out.println 本身就可以打印很多类型的对象,这里只是举个例子,来介绍泛型的用法。
在泛型的声明中可以使用 extends 和 super 关键字来限定泛型的范围,使其更符合我们预期的要求,含义与平时使用时略有差异, 下面列出一个表格以体现具体的差异:
关键字(限定词) | 使用方法 | 描述 |
extends | 限定泛型 T 的上界,即 T 必须是类 someType 的子类 | |
super | 限定泛型 T 的下界,即 T 必须是类 someType 的父类 |
特别说明,上表中 someType 可以是类(class),也可以是接口(interface)。
下面给出一些具体的示例来详细地说明它们的用法:
import java.util.ArrayList;
public class Test {
// extends Number 限定数组里面只能是数字
public static void printNumberArray(ArrayList arrayList) {
for (N n: arrayList) System.out.println(n);
}
public static void main(String[] args) {
ArrayList integerArrayList = new ArrayList<>();
ArrayList stringArrayList = new ArrayList<>();
integerArrayList.add(1);
integerArrayList.add(2);
printNumberArray(integerArrayList); // 正常运行
stringArrayList.add("1");
stringArrayList.add("2");
printNumberArray(stringArrayList); // 类型不符合泛型范围,报错!
}
}
泛型用在方法中,可以表示此方法的参数是泛型的,也可以是此方法的返回类型是泛型的。
泛型的参数类型很好写,泛型的声明放在方法的返回类型之前,修饰符之后,声明之后就可以在参数中使用泛型了。
语法:修饰符 泛型声明 返回类型 方法名 参数列表
泛型返回类型和泛型参数类型类似,泛型声明在方法的返回类型之前,修饰符之后,声明之后就可以在返回类型中直接使用泛型了。语法和上述一致。
下面是一个简单的示例:
public static T maxNumber (T a, T b) { // 泛型方法
return a.compareTo(b) > 0 ? a : b; // 返回较大值
}
泛型类中泛型声明与泛型方法非常类似,但又略有不同。其泛型声明在类名的后面。
语法:修饰符 关键字 类(接口)名 泛型声明
下面是一个具体示例:
class TypeBox { // 泛型类
public T type; // 泛型属性
TypeBox (T type){
this.type = type;
}
public void set(T value){
type = value;
}
public T get(){
return type;
}
}
public class Test {
public static void main(String[] args) {
TypeBox typeBox_1 = new TypeBox(666); // 整数:OK
TypeBox typeBox_2 = new TypeBox("Java"); // 字符串:OK
System.out.println(typeBox_1.get()); // Output: 666
System.out.println(typeBox_2.get()); // Output: Java
}
}
类型擦除是 Java 泛型中的一类特殊的机制,它出现的目的是为了兼容 JDK 5 之前的代码。
类型擦除是指,在 Java 运行时,Java 会把所有的泛型都替换为它们最顶级的上界,也就是 Object 对象。比如 ArrayList
在 JDK 5 之前,是没有泛型的,当时的程序员们为了实现类似泛型的功能,是用 Object 对象代替完成的,但 Object 对象实现的“泛型”并不完善,无法真正地做到和 JDK 5 出现的泛型一样,但由于这种做法已经非常普遍了,为了兼容这种早期做法,Java 泛型就有了类型擦除这种机制,为的就是兼容旧代码。这种机制也是 Java 泛型和其他编程语言泛型的区别之一。
顺便提一下,用 Object 对象实现类似泛型的功能:类型都用 Object 来定义,然后在运行时进行检查。
这里有个细节需要注意一下:既然有类型擦除机制,会在运行时将泛型擦除掉,全部用 Object 替换,那为什么泛型机制还可以现在泛型类型变量的值?比如,ArrayList
下面是一个示例:
ArrayList arrayList = new ArrayList();
arrayList.add(666);
arrayList.add("Java"); // 此处无法直接 add,会报错
Class arrayListClass = arrayList.getClass();
Method add = arrayListClass.getDeclaredMethod("add", Object.class);
add.invoke(arrayList, "Java"); // 通过反射机制可以在类型擦除机制之后成功 add