第39篇:Java泛型方法的定义和使用
📌 系列导航 :《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第38篇:Java是动态语言吗? |
➡️ 下一篇:第40篇:浮点数转成十进制问题
一、核心知识点
- 泛型类、泛型接口、泛型方法的定义
- 类型参数:
<T>、<T extends Bound>(有界类型) - 类型擦除原理(编译后转为原始类型)
- 通配符:
?、? extends T、? super T(PECS 原则) - 泛型方法和普通方法的区别
二、通俗讲解(1分钟开心学)
1. 为什么需要泛型?
在没有泛型之前,我们从集合中取出的元素都是 Object,需要强制转换,而且类型错误只能在运行时发现。泛型让"类型"成为参数,可以在编译时检查类型安全,代码也更清晰。
2. 泛型方法
泛型方法可以在普通类、泛型类中定义,不要求整个类是泛型。类型参数放在方法修饰符和返回类型之间,如 public static <T> T getMiddle(T... a)。
3. 有界类型参数
如果你想限制泛型参数的范围,比如只能接受 Number 的子类,可以写 <T extends Number>。这样 T 就可以调用 Number 的方法(如 doubleValue())。
4. 类型擦除
泛型信息只在编译时存在,运行时会擦除为原始类型(比如 List<String> 变成 List),并插入强制类型转换。这样做是为了兼容旧代码。
5. 通配符
?:表示未知类型,常用于方法参数。? extends T:表示 T 或 T 的子类(上界),可读不可写(除了null)。? super T:表示 T 或 T 的父类(下界),可写不可读(但可读为Object)。
PECS 原则 :Producer Extends, Consumer Super。如果你要生产 元素(读取),用 extends;如果你要消费 元素(写入),用 super。
生活类比 :
泛型就像一个"万能模具"。你只要告诉模具"今天做苹果派"(指定类型 Integer),它就能产出苹果派,不会给你混进香蕉(类型安全)。类型擦除就像模具工作完后,上面的标签被擦掉了,只剩下"派"这个原始形态。
三、实操代码案例 + 场景说明
场景:写一个通用的数组元素交换方法,以及一个安全地复制列表的方法。
java
import java.util.*;
public class GenericMethodDemo {
// 1. 泛型方法:交换数组中两个位置
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// 2. 泛型方法:返回两个值中的较大者(有界类型)
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// 3. 通配符:打印任意类型的列表
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.print(obj + " ");
}
System.out.println();
}
// 4. 上界通配符:读取数字列表求和(Producer Extends)
public static double sumList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
// 5. 下界通配符:向列表添加整数(Consumer Super)
public static void addIntegers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i);
}
}
public static void main(String[] args) {
// 交换测试
String[] fruits = {"apple", "banana", "cherry"};
swap(fruits, 0, 2);
System.out.println(Arrays.toString(fruits)); // [cherry, banana, apple]
// 最大值
System.out.println(max(10, 20)); // 20
System.out.println(max("cat", "dog")); // dog (词典序)
// 通配符打印
List<Integer> ints = Arrays.asList(1, 2, 3);
List<String> strs = Arrays.asList("A", "B");
printList(ints);
printList(strs);
// 上界:只能读,不能写
List<Integer> nums = Arrays.asList(1, 2, 3);
double sum = sumList(nums);
System.out.println("Sum: " + sum);
// sumList(nums).add(4); // 编译错误:不能添加
// 下界:可以写,读只能读为 Object
List<Object> objects = new ArrayList<>();
addIntegers(objects);
System.out.println(objects); // [1,2,3,4,5]
}
}
四、避坑要点
| 错误/误区 | 后果 | 正确做法 |
|---|---|---|
| 在静态上下文中使用泛型类的类型参数 | 编译错误 | 静态方法需要独立声明 <T> |
创建泛型数组:new T[10] |
编译错误 | 使用 ArrayList<T> 或 Array.newInstance |
泛型方法返回 T 但实参类型推断为 Object |
需要强制转换 | 调用时显式指定类型参数 |
? extends T 试图添加非 null 元素 |
编译错误 | 只读场景使用上界通配符 |
五、面试高频考点
Q1:什么是类型擦除?
泛型在编译后,类型参数会被替换为它们的边界(如
<T>变成Object,<T extends Number>变成Number),并插入强制类型转换。这是为了向后兼容旧代码。
Q2:List<?> 和 List<Object> 的区别?
List<Object>可以添加任何对象;List<?>表示未知类型的列表,只能读(除null外不能添加)。前者是具体类型,后者是通配符类型,更安全。
Q3:PECS 原则是什么?
Producer Extends, Consumer Super。如果需要从集合读取元素(Producer),使用
? extends T;如果需要向集合写入元素(Consumer),使用? super T。
六、练习题
- 代码填空 :编写一个泛型方法
getMiddle,返回任意数组的中间元素(如果长度为偶数,返回中间两个中的第一个)。 - 判断 :
List<Integer>是List<Number>的子类型吗?为什么? - 动手 :使用有界类型参数实现一个方法
findMax,找出List<T extends Comparable<T>>中的最大值。
📊 你的学习进度
- 当前:第39篇 / 共44篇 · 第六阶段:NIO、泛型、JVM内幕、字节码(第36~44篇)
- ✅ 已完成:第1~38篇
- 📖 正在学:第39篇
- ⏳ 待学习:第40~44篇
👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇
💡 本专栏每篇都包含:避坑表 + 面试高频考点 + 练习题。每天30分钟,100天拿offer!
👉 下一篇文章预告
《第40篇:浮点数转成十进制问题》
内容简介:浮点数精度丢失原因、0.1+0.2!=0.3、BigDecimal正确使用、金额计算最佳实践。
💡 学完这篇,你将彻底搞懂浮点数陷阱,写出精确的金额计算代码。
📌 《Java 100 天进阶之路 | 从入门到上岗就业》 每天一篇,建议收藏 + 关注 ,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!
📌 除了Java,我也在深挖智能物流实战(出版社WMS、托盘调度、机器学习落地)。如果你对技术在不同领域的实战感兴趣,欢迎点击我的头像,看看专栏《出版社物流WMS智能调度实战》。技术相通,思路可鉴。