1. 泛型简介
泛型(Generics) 是 JDK 5 引入的特性,它允许在定义类、接口和方法时使用类型参数 ,从而在编译时提供类型安全检查,并避免强制类型转换。
1.1 为什么需要泛型?
泛型的出现不是为了增加语言复杂性,而是为了解决真实存在的工程问题:
- 提高程序的健壮性(减少运行时异常)
- 提升开发效率(减少样板代码,更好的工具支持)
- 增强代码可读性(类型信息显式表达)
- 支持类型安全的通用编程(编写一次,多处安全使用)
1.1.1. 泛型出现前的世界
在JDK 5 之前,没有泛型的时代,Java 集合类只能存储 Object 类型:
java
// JDK 5 之前的代码(没有泛型)
List list = new ArrayList();
list.add("Hello");
list.add(new Integer(42));
list.add(new Date());
// 取出元素时需要强制转换
String str = (String) list.get(0); // OK
Integer num = (Integer) list.get(1); // OK
String wrong = (String) list.get(2); // 运行时错误!ClassCastException
**痛点1.类型不安全:**编译器无法检查类型错误,类型错误只能在运行时发现
java
public void processNumbers(List numbers) {
// 开发者期望传入 List<Integer>
// 但实际可能传入任何类型的 List
for (int i = 0; i < numbers.size(); i++) {
Integer num = (Integer) numbers.get(i); // 可能抛出 ClassCastException
System.out.println(num * 2);
}
}
// 调用者可能犯错
List strings = Arrays.asList("a", "b", "c");
processNumbers(strings); // 编译通过,但运行时崩溃!
痛点2. 强制转换繁琐:每次取值都要强制转换,代码冗余和可读性差
java
Map employeeMap = new HashMap();
employeeMap.put("001", new Employee("Alice"));
employeeMap.put("002", new Employee("Bob"));
// 每次使用都要转换
Employee emp1 = (Employee) employeeMap.get("001");
Employee emp2 = (Employee) employeeMap.get("002");
Employee emp3 = (Employee) employeeMap.get("003"); // 如果 key 不存在,返回 null,转换没问题
// 但如果 map 中混入了其他类型...
employeeMap.put("004", "Not an employee");
Employee emp4 = (Employee) employeeMap.get("004"); // 运行时错误!
**痛点3. API 设计困难:**没有泛型时,工具方法很难设计
java
public static Object max(Object[] array) {
// 如何比较?需要额外的 Comparator 参数
// 返回 Object,调用者需要转换
}
// 调用时
Integer[] numbers = {1, 2, 3};
Object result = max(numbers);
Integer maxNum = (Integer) result; // 必须转换
1.1.2 没有泛型的解决方案
使用 **[继承]**解决类型安全和强制转换问题,但:
- 需要为每种类型都创建类,代码爆炸!
- 而且无法处理自定义类型(每个用户都要创建自己的 XxxList)
java
// 方案:为每种类型创建专门的集合类
class StringList extends ArrayList {
public void add(String s) { /* ... */ }
public String get(int i) { /* ... */ }
}
class IntegerList extends ArrayList {
public void add(Integer i) { /* ... */ }
public Integer get(int i) { /* ... */ }
}
使用 **[接口标记]**方式来解决:仍然无法避免强制转换,类型安全问题依然存在
java
// 方案:使用标记接口
interface StringContainer {}
class MyList extends ArrayList implements StringContainer {}
1.1.3. 泛型如何解决这些问题?
解决方案一:编译时类型检查。类型错误在编译时就被发现,而不是等到运行时崩溃。
java
// 有了泛型后
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");
// stringList.add(42); // 编译错误!类型不匹配
String first = stringList.get(0); // 无需强制转换!
**解决方案二:消除强制转换。**代码更简洁,可读性更好,减少样板代码。
java
// 泛型集合
Map<String, Employee> employeeMap = new HashMap<>();
employeeMap.put("001", new Employee("Alice"));
Employee emp = employeeMap.get("001"); // 直接获得正确类型,无需转换
解决方案三:类型安全的 API。
java
// 泛型工具方法
public static <T extends Comparable<T>> T max(T[] array) {
if (array.length == 0) return null;
T max = array[0];
for (T item : array) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
// 使用
Integer[] numbers = {1, 2, 3};
Integer maxNum = max(numbers); // 类型安全,无需转换
String[] words = {"apple", "banana", "cherry"};
String maxWord = max(words); // 同样的方法,不同类型
1.1.4 泛型的核心价值总结
| 维度 | 没有泛型 | 有泛型 |
|---|---|---|
| 安全性 | 运行时才发现类型错误 | 编译时检查类型安全 |
| 代码质量 | 大量强制转换,容易出错 | 无强制转换,类型明确 |
| 开发效率 | 需要小心处理类型转换 | IDE 提供准确的类型推断和补全 |
| API 设计 | 接口模糊,文档依赖注释 | 接口自文档化,类型约束明确 |
| 维护成本 | 类型错误难以追踪 | 类型错误在编译时暴露 |
1.2. 泛型的基本语法
1.2.1 泛型类
java
// 定义泛型类
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
// 使用泛型类
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String str = stringBox.get(); // 无需强制转换
Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer num = intBox.get();
1.2.2 泛型接口
java
// 定义泛型接口
public interface Comparable<T> {
int compareTo(T other);
}
// 实现泛型接口
public class Person implements Comparable<Person> {
private String name;
private int age;
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
1.2.3 泛型方法
java
// 泛型方法(与类是否泛型无关)
public class GenericUtils {
// 静态泛型方法
public static <T> void printArray(T[] array) {
for (T item : array) {
System.out.println(item);
}
}
// 实例泛型方法
public <T extends Number> double sum(T[] numbers) {
double sum = 0;
for (T num : numbers) {
sum += num.doubleValue();
}
return sum;
}
}
// 使用泛型方法
String[] names = {"Alice", "Bob", "Charlie"};
GenericUtils.printArray(names);
Integer[] nums = {1, 2, 3, 4, 5};
double total = new GenericUtils().sum(nums);
对比泛型类中的方法,我们可以看到这个例子里的泛型方法返回值前多了泛型参数 <T> 或 <T extends Number>。那什么时候需要加泛型参数呢?
1.2.3.1 需加泛型参数的情况
case1.静态方法 需要声明自己的泛型参数,因为静态方法不能使用类的泛型参数(最常见需要 <T> 的情况)。
java
// 非泛型类中的静态泛型方法 - 要加泛型参数
public class Collections {
// ✅ 必须有 <T> - 这是方法自己的泛型参数
public static <T> void sort(List<T> list) {}
// ✅ 必须有 <T> - 带约束的泛型参数
public static <T extends Comparable<T>> T max(List<T> list) {}
}
java
// 泛型类中的静态泛型方法 - 要加泛型参数
public class Box<T> {
// ✅ 必须有 <E> - 静态方法不能使用类的泛型参数 T
// 静态方法属于类本身,而不是类的实例
public static <E> Box<E> createEmptyBox() {
return new Box<E>();
}
// ❌ 错误!静态方法不能使用类的泛型参数
// public static Box<T> createEmptyBox() { } // 编译错误
}
case2. 泛型类的实例方法 声明新的泛型参数,新的泛型参数必须声明
java
public class MyList<T> { // 类声明了泛型参数 T
// 不需要 <T> - 使用类的泛型参数
public void add(T item) {}
// 不需要 <T> - 使用类的泛型参数
public T get(int index) { return null; }
// ❌ 错误!不能重复声明 T
// public <T> void badMethod(T item) {} // 编译错误:重复的类型参数
// ✅ 声明新的泛型参数 E(用不同名字)
public <E> void copyTo(MyList<E> other) {}
}
case2.非泛型类的方法声明泛型参数(较少见但合法)
java
public class Processor {
// ✅ 非泛型类中的实例泛型方法
public <T> void process(List<T> items) {
// T 是这个方法特有的泛型参数
}
// ✅ 可以同时使用多个泛型参数
public <T, U> Map<T, U> zip(List<T> keys, List<U> values) {
return null;
}
}
1.2.3.1 不需加泛型参数的情况
泛型类的实例方法使用类的泛型参数,泛型参数已在类级别声明过了,无需再次声明。
java
public class Box<T> { // ← T 是在类定义时声明的
public void set(T value) {} // 使用类的泛型参数 T
public T get() { return value; } // 使用类的泛型参数 T
}
当方法使用的是类上定义的泛型参数 时,不需要在方法返回值前加 <T>。
T已经在类定义Box<T>中声明过了- 这些方法直接使用类的泛型参数
- 编译器知道
T指的是什么
1.3. 类型参数命名约定
| 字母 | 含义 | 示例 |
|---|---|---|
| T | Type | List<T> |
| E | Element | ArrayList<E> |
| K | Key | Map<K, V> |
| V | Value | Map<K, V> |
| N | Number | <N extends Number> |
| S, U, V | 第二、第三、第四类型 | <T, S extends T> |
2. 泛型的类型约束
2.1 上界通配符extends
上界通配符(Upper Bounded Wildcards):使用 extends 关键字限制类型参数的上限。
2.1.1 用法
java
// 只能接受 Number 及其子类
public static void processNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num.doubleValue());
}
// list.add(10); // 编译错误!不能添加元素; 写入是不安全操作,故编译器禁止
}
// 使用示例
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.0, 2.0, 3.0);
processNumbers(integers); // OK
processNumbers(doubles); // OK
2.1.2 上界通配符:可读不可写
方法参数类型使用上界通配符后**,** 可读不可写。 例子中,方法参数类型为List<? extends Number>,参数 list 可以获取元素,不能添加元素。记忆:extends --> 出口,用于输出。
List<? extends Number> list,编译器只知道 list 中的元素是 Number 的某个子类型,但不知道具体是哪个子类型:
java
List<? extends Number> list = new ArrayList<Integer>();
// 或者
List<? extends Number> list = new ArrayList<Double>();
// 或者
List<? extends Number> list = new ArrayList<Float>();
为什么可以读取?
- 无论
list实际存储的是Integer、Double还是Float,它们都是Number的子类,所以赋值给Number类型变量总是安全的。
java
Number num = list.get(0); // ✅ 安全!
为什么不能写入?
- 编译器不知道这个列表到底是什么类型的,可能是
Integer列表,也可能是Double列表。如果允许你添加任何Number的子类,就可能破坏列表的类型一致性。为了安全起见,什么都不让你添加。 - 假设
list实际是ArrayList<Integer>,如果允许list.add(new Double(3.14)),就会把Double放进Integer列表中,这违反了类型安全。
java
list.add(new Integer(42)); // ❌ 编译错误!
list.add(new Double(3.14)); // ❌ 编译错误!
list.add(new Number()); // ❌ 编译错误!(这个例子中Number是抽象类,但即使不是抽象类也会报错)
2.2 下界通配符super
下界通配符(Lower Bounded Wildcards):使用 super 关键字限制类型参数的下限。
2.2.1 用法
java
// 只能接受 Integer 及其父类
public static void addNumbers(List<? super Integer> list) {
list.add(10); // OK - 可以添加 Integer
list.add(20); // OK
// Integer num = list.get(0); // 编译错误!只能获取 Object
}
// 使用示例
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(integers); // OK
addNumbers(numbers); // OK
addNumbers(objects); // OK
2.2.1 下界通配符:可写不可读
List<? super Integer> list,编译器只知道 list 中的元素类型是 Integer 的某个父类型,但不知道具体是哪个父类型。 记忆:super -->入口,用于输入。
java
List<? super Integer> list = new ArrayList<Integer>();
// 或者
List<? super Integer> list = new ArrayList<Number>();
// 或者
List<? super Integer> list = new ArrayList<Object>();
为什么可以写入?
- 无论
list实际是Integer列表、Number列表还是Object列表,Integer都可以安全地赋值给这些类型(因为Integer是它们的子类)。
java
list.add(new Integer(42)); // ✅ 安全!
list.add(100); // ✅ 自动装箱,也是 Integer
为什么不能安全读取?
- 编译器知道这个列表可以接受
Integer,但它可能是一个Object列表,里面可能有各种类型的对象。只能保证取出的对象是Object类型,更具体的类型无法确定 - 假设
list实际是ArrayList<Object>,里面可能存储了String、Date等任意对象。如果允许Integer num = list.get(0),而实际取出的是String,就会导致类型错误。编译器无法保证取出的元素一定是Integer或Number
java
Integer num = list.get(0); // ❌ 编译错误!
Number num = list.get(0); // ❌ 编译错误!
Object obj = list.get(0); // ✅ 只能作为 Object 读取
2.3 无界通配符?
无界通配符(Unbounded Wildcards):使用 <?> 表示未知类型。
java
// 接受任何类型的 List
public static void printListSize(List<?> list) {
System.out.println("Size: " + list.size());
// Object obj = list.get(0); // OK,但只能作为 Object 处理
// list.add("hello"); // 编译错误!
}
// 使用示例
List<String> strings = Arrays.asList("a", "b");
List<Integer> integers = Arrays.asList(1, 2);
printListSize(strings); // OK
printListSize(integers); // OK
2.4 PECS 原则
PECS 原则(Producer Extends, Consumer Super)是理解和使用通配符的关键原则:
- Producer Extends :如果你需要从集合中读取 数据(生产者),使用
<? extends T> - Consumer Super :如果你需要向集合中写入 数据(消费者),使用
<? super T>
java
// 经典例子:Collections.copy()
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
// dest 是消费者(写入),所以用 super
// src 是生产者(读取),所以用 extends
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}
// 使用示例
List<Integer> src = Arrays.asList(1, 2, 3);
List<Number> dest = new ArrayList<>(Arrays.asList(0.0, 0.0, 0.0));
Collections.copy(dest, src); // 完美工作!
为什么这样设计是安全的?
src.get(i)返回? extends T,可以安全赋值给Tdest.set(i, T)接受T,而dest是? super T,可以安全接受T
总结: 这种设计看似限制了灵活性,但实际上保证了类型安全,这正是泛型存在的根本目的
| 通配符类型 | 可以做什么 | 为什么 |
|---|---|---|
<? extends T> |
读取 为 T 类型 |
所有元素都是 T 的子类,向上转型安全 |
<? extends T> |
不能写入 | 不知道具体类型,写入可能破坏类型安全 |
<? super T> |
写入 T 类型 |
T 可以向上转型为任何父类型 |
<? super T> |
只能读取 为 Object |
不知道具体父类型,无法确定更具体的类型 |
3.泛型的类型擦除
3.1 什么是类型擦除?
Java 泛型是通过类型擦除实现的,这意味着:
- 泛型信息只在编译时存在
- 运行时所有的泛型类型都被擦除为原始类型(Raw Type)
java
// 编译后,这两个 List 在运行时是相同的类型,均为原始类型List
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
// 运行时类型检查
System.out.println(stringList.getClass() == integerList.getClass()); // true
3.2 类型擦除的影响
3.2.1 影响1:无法进行 instanceof 检查
java
// 编译错误!
if (obj instanceof List<String>) { }
// 只能检查原始类型
if (obj instanceof List) { }
3.2.2 影响2:无法创建泛型数组
java
// 编译错误!
T[] array = new T[size]; // 不允许
// 解决方案:使用反射或 Object 数组
@SuppressWarnings("unchecked")
T[] array = (T[]) new Object[size];
3.2.3 影响3:无法抛出或捕获泛型异常
java
// 编译错误!
class GenericException<T> extends Exception { }
// 但可以这样:
class MyException extends Exception { }
4. 泛型的实际应用示例
4.1 自定义泛型容器
java
public class Stack<T> {
private List<T> elements = new ArrayList<>();
public void push(T item) {
elements.add(item);
}
public T pop() {
if (elements.isEmpty()) {
throw new IllegalStateException("Stack is empty");
}
return elements.remove(elements.size() - 1);
}
public boolean isEmpty() {
return elements.isEmpty();
}
}
// 使用
Stack<String> stringStack = new Stack<>();
stringStack.push("Hello");
stringStack.push("World");
String top = stringStack.pop(); // "World"
4.2 泛型工具方法
java
public class CollectionUtils {
// 查找列表中的最大值
public static <T extends Comparable<T>> T max(List<T> list) {
if (list.isEmpty()) {
throw new IllegalArgumentException("List is empty");
}
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
// 合并两个列表
public static <T> List<T> merge(List<T> list1, List<T> list2) {
List<T> result = new ArrayList<>(list1);
result.addAll(list2);
return result;
}
}
// 使用
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5);
Integer maxNum = CollectionUtils.max(numbers);
List<String> list1 = Arrays.asList("A", "B");
List<String> list2 = Arrays.asList("C", "D");
List<String> merged = CollectionUtils.merge(list1, list2);
4.3 泛型与继承
java
// 泛型类的继承
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
// 注意:List<Dog> 不是 List<Animal> 的子类型!
public void processAnimals(List<Animal> animals) {
// 不能传入 List<Dog>
}
// 正确的方式:使用通配符
public void processAnimals(List<? extends Animal> animals) {
// 现在可以传入 List<Dog> 或 List<Cat>
for (Animal animal : animals) {
// 处理动物
}
}
4.4 最佳实践
**原始类型 vs 泛型类型:**尽可能使用泛型而不是原始类型
java
// 危险!使用原始类型
List rawList = new ArrayList();
rawList.add("Hello");
rawList.add(42); // 编译器不会检查!
// 安全!使用泛型
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// stringList.add(42); // 编译错误!
泛型数组创建
java
// 错误方式
public static <T> T[] toArray(List<T> list) {
// T[] array = new T[list.size()]; // 编译错误
return array;
}
// 正确方式
public static <T> T[] toArray(List<T> list, T[] a) {
return list.toArray(a);
}