第一部分:泛型基础概念

理解泛型的核心思想和解决的问题,以及它如何提高代码的安全性和重用性

学习目标

理解泛型解决的问题,掌握泛型的基本语法和优势

1

什么是泛型?

泛型是JDK 5引入的重要特性,允许在定义类、接口和方法时使用类型参数

泛型的核心价值:

  • 类型安全:编译时检查类型错误
  • 代码重用:一套代码处理多种数据类型
  • 消除强制类型转换
  • 更好的代码可读性
2

泛型解决的问题

在引入泛型之前,Java集合只能存储Object类型,使用时需要强制类型转换:

// JDK 5之前的集合使用 List list = new ArrayList(); list.add("Java"); list.add(100); // 编译通过但运行时可能出错 String str = (String) list.get(0); // 需要强制转换 Integer num = (Integer) list.get(1); // 可能抛出ClassCastException

泛型解决了这些问题,使代码更安全、更简洁。

无泛型的问题

  • 运行时的类型转换错误
  • 需要大量类型转换代码
  • 可读性差,难以理解集合元素类型
  • 代码安全性低

使用泛型的优势

  • 编译时类型检查
  • 消除强制类型转换
  • 代码更简洁易读
  • 代码重用性更高
  • API设计更灵活

第二部分:泛型类与接口

学习如何定义和使用泛型类、泛型接口

学习目标

能够创建自定义泛型类和接口,并理解其实现原理

1

定义泛型类

在类名后添加类型参数声明(用尖括号 <><> 包围)

// 泛型类定义示例 public class Box<T> { private T content; public void setContent(T content) { this.content = content; } public T getContent() { return content; } }

这里 T 是类型参数,可以用任何有效标识符(常用T、E、K、V等)。

2

使用泛型类

创建泛型类实例时指定具体类型:

// 使用泛型类 Box<String> stringBox = new Box<>(); stringBox.setContent("Hello Generics"); String str = stringBox.getContent(); // 无需强制转换 Box<Integer> intBox = new Box<>(); intBox.setContent(42); int num = intBox.getContent(); // 自动拆箱 // 编译错误示例 // stringBox.setContent(100); // 错误: 不兼容的类型

注意:从Java 7开始,可以使用"钻石"操作符 <><> 简化泛型实例化

3

泛型接口

接口也可以使用泛型,定义方式类似:

// 泛型接口定义 public interface Repository<T> { void save(T entity); T findById(int id); List<T> findAll(); } // 实现泛型接口 public class UserRepository implements Repository<User> { @Override public void save(User entity) { // 实现保存逻辑 } @Override public User findById(int id) { // 实现查询逻辑 return new User(id); } @Override public List<User> findAll() { // 实现查询所有 return new ArrayList<>(); } }

第三部分:泛型方法

学习如何定义和使用泛型方法

学习目标

掌握泛型方法的定义和使用,理解类型推断原理

1

定义泛型方法

泛型方法在方法返回类型前声明类型参数:

public class ArrayUtils { // 泛型方法:交换数组中的两个元素 public static <T> void swap(T[] array, int i, int j) { T temp = array[i]; array[i] = array[j]; array[j] = temp; } // 泛型方法:查找数组中的最大值 public static <T extends Comparable<T>> T max(T[] array) { if (array == null || array.length == 0) { return null; } T max = array[0]; for (int i = 1; i < array.length; i++) { if (array[i].compareTo(max) > 0) { max = array[i]; } } return max; } }

第一个方法 swap 是简单的泛型方法,第二个方法 max 使用了类型边界(后面会详细讲解)。

2

调用泛型方法

Java编译器通常可以自动推断类型参数:

// 调用泛型方法 String[] words = {"Java", "Generics", "Tutorial"}; ArrayUtils.swap(words, 0, 2); System.out.println(Arrays.toString(words)); // 输出: [Tutorial, Generics, Java] Integer[] numbers = {5, 2, 9, 1, 7}; Integer maxNum = ArrayUtils.max(numbers); System.out.println("Max number: " + maxNum); // 输出: Max number: 9 // 显式指定类型参数(通常不需要) ArrayUtils.<String>swap(words, 1, 2);

注意:静态方法可以是泛型方法,即使类本身不是泛型类

第四部分:类型边界

学习如何限制泛型类型参数的范围

学习目标

掌握类型边界的使用,理解上界和下界的区别

上界通配符 (extends)

使用 <T extends Type> 限制类型参数必须是特定类型或其子类型

上界通配符示例

// 只接受Number及其子类 public class NumericBox<T extends Number> { private T value; public NumericBox(T value) { this.value = value; } public double getSquare() { return value.doubleValue() * value.doubleValue(); } } // 使用示例 NumericBox<Integer> intBox = new NumericBox<>(5); System.out.println(intBox.getSquare()); // 输出: 25.0 NumericBox<Double> doubleBox = new NumericBox<>(2.5); System.out.println(doubleBox.getSquare()); // 输出: 6.25 // 编译错误 // NumericBox<String> stringBox = new NumericBox<>("test");

多边界类型参数

类型参数可以有多个边界,使用 <T extends A & B & C>

多边界示例

interface Loggable { void log(); } // T必须同时是Runnable和Loggable的实现类 public class TaskRunner<T extends Runnable & Loggable> { private T task; public TaskRunner(T task) { this.task = task; } public void execute() { task.log(); new Thread(task).start(); } }

注意:类边界必须放在接口边界之前,且最多只能有一个类边界

第五部分:通配符

学习使用通配符提高API的灵活性

学习目标

掌握通配符的使用场景,理解PECS原则

上界通配符 (?)

使用 <? extends Type> 表示接受Type或其子类型的集合

上界通配符示例

// 打印Number及其子类的列表 public static void printNumbers(List<? extends Number> numbers) { for (Number num : numbers) { System.out.print(num + " "); } System.out.println(); } // 使用示例 List<Integer> integers = Arrays.asList(1, 2, 3); List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3); printNumbers(integers); // 输出: 1 2 3 printNumbers(doubles); // 输出: 1.1 2.2 3.3

下界通配符 (?)

使用 <? super Type> 表示接受Type或其父类型的集合

下界通配符示例

// 向集合中添加Integer及其父类型的元素 public static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 5; i++) { list.add(i); } } // 使用示例 List<Number> numbers = new ArrayList<>(); List<Object> objects = new ArrayList<>(); addNumbers(numbers); addNumbers(objects); System.out.println(numbers); // 输出: [1, 2, 3, 4, 5] System.out.println(objects); // 输出: [1, 2, 3, 4, 5]

PECS原则 (Producer-Extends, Consumer-Super)

使用通配符时的最佳实践:

  • 当你需要从集合中获取元素(生产者)时,使用 <? extends T>
  • 当你需要向集合中添加元素(消费者)时,使用 <? super T>
  • 当你既需要获取又需要添加时,使用精确类型 <T>

第六部分:类型擦除

理解Java泛型的实现机制和限制

学习目标

掌握类型擦除原理,理解泛型在运行时的行为

1

什么是类型擦除?

Java泛型是通过类型擦除(Type Erasure)实现的,编译器在编译时移除所有类型信息:

  • 所有类型参数被替换为它们的边界(如无边界则替换为Object)
  • 在必要的位置插入类型转换
  • 生成桥接方法以保持多态性
// 编译前 List<String> strings = new ArrayList<>(); strings.add("Hello"); String s = strings.get(0); // 编译后(类型擦除后) List strings = new ArrayList(); strings.add("Hello"); String s = (String) strings.get(0); // 插入类型转换
2

类型擦除的影响

由于类型擦除,泛型在运行时有一些限制:

// 1. 无法创建泛型数组 // 编译错误 // T[] array = new T[10]; // 2. 无法使用instanceof检查泛型类型 List<Integer> list = new ArrayList<>(); // 编译错误 // if (list instanceof List<Integer>) // 3. 无法重载具有相同擦除类型的方法 public class Example { // 编译错误:两个方法有相同的擦除类型 public void print(List<String> list) {} public void print(List<Integer> list) {} }

第七部分:常见错误与陷阱

避免泛型编程中的常见错误

错误1:使用原始类型

// 错误:使用原始类型List而不是List<String> List list = new ArrayList(); list.add("Test"); list.add(100); // 编译通过但运行时可能出错

解决方案:总是使用泛型类型声明集合

错误2:忽略未检查警告

List list = new ArrayList(); list.add("Test"); // 产生未检查警告:未检查的调用add(E)

解决方案:修正代码消除警告,而不是忽略或压制

错误3:混淆类泛型和方法泛型

public class Box<T> { // 错误:这里的T是类泛型参数 public static void print(T item) { System.out.println(item); } }

解决方案:静态方法不能使用类泛型参数,应定义为泛型方法

错误4:误用通配符

// 错误:不能向List<?>添加元素(null除外) List<?> list = new ArrayList<String>(); list.add("Test"); // 编译错误

解决方案:使用下界通配符或精确类型

第八部分:动手练习

通过实践巩固泛型知识

练习1:泛型Pair类

创建一个泛型类 Pair<K, V>,包含两个字段:key(类型为K)和value(类型为V)

  • 实现构造方法接受key和value
  • 实现getKey()和getValue()方法
  • 实现setKey()和setValue()方法
  • 添加一个swap()方法,交换key和value的位置

练习2:统计方法

创建一个泛型方法,计算数组中特定元素出现的次数:

public static <T> int countOccurrences(T[] array, T target) { // 实现计数逻辑 }

练习3:排序工具

创建一个泛型方法,使用冒泡排序对数组进行排序:

public static <T extends Comparable<T>> void bubbleSort(T[] array) { // 实现冒泡排序 }

练习4:列表操作

实现以下泛型方法:

// 1. 合并两个列表 public static <T> List<T> mergeLists(List<? extends T> list1, List<? extends T> list2) // 2. 将列表元素添加到集合 public static <T> void addAllToCollection(List<? extends T> source, Collection<? super T> dest)