Java泛型(generics) 是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK 5中的新集合类框架中。
泛型的好处有两个:
- 方便 :消除强制类型转换,提高代码复用性。以 List 为例,使用泛型后取出元素时无需手动向下转型;如果你希望某个 List 只能存放 String,直接在声明时指定
List<String>即可,编译器会在编译期保证类型匹配,无需为每种类型编写独立的数据结构实现。 - 安全:在泛型出现之前,通过 Object 实现的集合在取出元素时必须手动向下转型,转型错误要到运行时才能发现(ClassCastException)。泛型将类型检查提前到编译期,在编译时就能发现类型不匹配的问题,极大增强了程序的安全性。
知识扩展
泛型是如何实现的
Java中的泛型通过类型擦除 的方式来实现,通俗点理解,就是通过语法糖的形式,在 .java -> .class 转换的阶段,将 List<String> 擦除掉转为 List 的手段。换句话说,Java 的泛型只在编译期有效,JVM 在运行期是感知不到泛型的。
类型擦除的缺点有哪些?
- 不能以类型参数差异实现方法重载 :
void foo(List<String>)和void foo(List<Integer>)擦除后签名相同,编译报错。 - 泛型类不能在多个 catch 子句中捕获不同类型的异常:因为异常类型在字节码中必须是具体且可区分的。
- 泛型类的静态变量只有一份 :由于
Foo<String>和Foo<Integer>经过擦除后对应同一个Foo类,其静态字段只有一份,不会因类型参数的不同而有多份副本。
对泛型通配符的理解
✅泛型中上下界限定符extends 和 super有什么区别?
List<?>, List<Object>, List 之间的区别
List<?>是通配符类型,表示持有某种未知类型的 List;List<Object>则是明确可以存放 Object 及其子类的 List。可以将List<String>、List<Integer>赋值给List<?>,却不能将List<String>赋值给List<Object>(泛型不支持协变)。- 原始类型
List可以接收任何带类型参数的 List(为了与遗留代码兼容),但将一个List<String>赋值给List<Object>会编译报错(泛型不变性)。 List<?>由于不确定元素的具体类型,只能从列表中读取元素(读出的元素类型为?的边界,即 Object),不能向其中添加除了 null 以外的任何元素。

在泛型为 Integer 的 ArrayList 中存放一个 String 类型的对象
通过反射可以绕过编译期的泛型检查:
java
public void test() throws Exception {
ArrayList<Integer> list = new ArrayList<Integer>();
Method method = list.getClass().getMethod("add", Object.class);
method.invoke(list, "Java反射机制实例");
System.out.println(list.get(0));
}
对数组协变和泛型非协变的理解
所谓协变,可以简单理解为因为 Object 是 String 的父类,所以 Object[] 同样是 String[] 的父类,这种情况 Java 是允许的;但是对于泛型来说,List<Object> 和 List<String> 之间没有继承关系。
为什么要这样设计呢?如果泛型允许协变,考虑如下代码:
java
List<Object> a = new ArrayList<String>(); // 编译错误:泛型不允许协变
// 若能执行到下行,允许协变就可以把一个 Integer 加进去
// a.add(1);
// String s = a.get(0); // 编译报错:取出的是 Object 而非 String
既然泛型不允许协变,为什么数组允许协变?原因有二:
- 数组在设计之初(Java 1.0)还没有泛型的概念,为了兼容性保留了协变特性,例如
Arrays.equals(Object[], Object[])等方法可以同时处理String[]和Integer[],是历史遗留的设计选择。 - 数组在运行时携带自身的组件类型信息,在尝试存入不兼容类型时会抛出
ArrayStoreException(运行时安全检查),而泛型在运行时类型信息已被擦除,无法做类似检查。数组的运行时类型检查在一定程度上弥补了协变带来的风险。