吃透Java泛型:从类/方法到通配符,干货无冗余
一、泛型核心本质
泛型的本质,用一句话就能说懂:类型参数化。简单来讲,以前我们写代码,变量的类型是固定的(比如String、Integer),写一个存String的容器,就不能存Integer;而泛型相当于把“固定类型”改成了“可变参数”,定义类、方法时,不明确指定具体类型,只先用一个字母(比如T、E)代替,等我们实际用的时候(创建对象、调用方法),再告诉程序这个字母对应的具体类型。这里要注意:泛型不支持int、char这种基本类型,得用它们的包装类(Integer、Character)才行。
核心优势:
-
类型安全:编译期检查类型,避免运行时出现ClassCastException;
-
代码复用:一套代码适配多种引用类型,无需重复编写;
-
简化开发:无需手动进行强制类型转换,编译器自动完成。
前提:Java泛型是编译期特性,运行时JVM不感知泛型类型信息(结合下一篇类型擦除详细说明)。
二、泛型类(最基础的泛型使用)
1. 定义
类定义时,在类名后添加尖括号<>,里面填写类型参数(通常用单个大写字母表示,如T、E、K、V,无特殊含义,仅为标识),类内部可使用该类型参数定义属性、方法的参数和返回值。
2. 语法格式
// T:类型参数,代表任意引用类型(不能是基本类型)
public class GenericClass<T> {
// 使用类型参数定义属性
private T data;
// 使用类型参数定义方法的参数和返回值
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
3. 使用示例
创建泛型类对象时,需在尖括号中指定具体的引用类型,编译器会根据指定的类型,进行编译期检查和类型适配。
public class GenericTest {
public static void main(String[] args) {
// 1. 指定T为String类型,只能存储String数据
GenericClass<String> strObj = new GenericClass<>();
strObj.setData("Java泛型基础");
// 无需强制转换,直接返回String类型
String strData = strObj.getData();
System.out.println("String类型数据:" + strData);
// 2. 指定T为Integer类型,只能存储Integer数据
GenericClass<Integer> intObj = new GenericClass<>();
intObj.setData(100);
Integer intData = intObj.getData();
System.out.println("Integer类型数据:" + intData);
// 错误示例:不能存储与指定类型不一致的数据(编译报错)
// strObj.setData(123);
}
}
4. 注意事项
泛型类的类型参数,仅在类内部有效;创建对象时,必须指定具体类型。
三、泛型方法(独立于泛型类的泛型使用)
泛型方法与泛型类无关,普通类、泛型类中都可以定义泛型方法,核心是方法级别的类型参数化——类型参数仅作用于当前方法。
1. 定义与语法
泛型方法的关键标识:在返回值类型前添加尖括号<>,填写类型参数,这是区分泛型方法和普通方法的核心。
public class GenericMethodDemo {
// 泛型方法:<T> 是类型参数声明,必须在返回值前
public <T> T getFirstElement(T[] array) {
// 处理逻辑:获取数组的第一个元素
if (array == null || array.length == 0) {
return null;
}
return array[0];
}
// 普通类中的泛型方法(重载示例)
public <E> void printElement(E element) {
System.out.println("元素内容:" + element);
}
}
2. 使用示例
调用泛型方法时,编译器会自动根据传入的参数类型,推断出类型参数的具体值,无需手动指定(也可手动指定,不推荐)。
public class GenericMethodTest {
public static void main(String[] args) {
GenericMethodDemo demo = new GenericMethodDemo();
// 1. 调用泛型方法getFirstElement,传入String数组,自动推断T为String
String[] strArray = {"Java", "泛型", "方法"};
String firstStr = demo.getFirstElement(strArray);
System.out.println("String数组第一个元素:" + firstStr);
// 2. 传入Integer数组,自动推断T为Integer
Integer[] intArray = {1, 2, 3, 4};
Integer firstInt = demo.getFirstElement(intArray);
System.out.println("Integer数组第一个元素:" + firstInt);
// 3. 调用printElement方法,传入不同类型参数
demo.printElement("测试字符串");
demo.printElement(12345);
demo.printElement(3.14);
}
}
3.区别(泛型方法 vs 泛型类)
泛型类:类型参数作用于整个类,创建类对象时指定类型;
泛型方法:类型参数仅作用于当前方法,调用方法时自动推断类型,无需依赖类的类型参数。
四、泛型约束(边界限定,限制类型参数范围)
默认情况下,泛型的类型参数可以是任意引用类型,但实际开发中,我们可能需要限制类型参数的范围(比如只能是某个类的子类、某个接口的实现类),这就是泛型约束(也叫边界限定)。
泛型约束分为上界约束和下界约束,其中下界约束仅支持通配符,不支持泛型类/方法的类型参数。
1. 上界约束(extends关键字)
(1)定义
表示类型参数必须是“指定类的子类”或“指定接口的实现类”,语法:<T extends 边界类型>。
注意:如果边界是类+接口,类必须放在最前面,接口后续排列(可多个接口)。
(2)代码示例
// 示例1:上界为类(T必须是Number的子类,如Integer、Double)
public class NumberContainer<T extends Number> {
private T number;
public T getNumber() {
return number;
}
public void setNumber(T number) {
this.number = number;
}
}
// 示例2:上界为类+接口(T必须继承Animal,且实现Runnable、Serializable接口)
class Animal {}
public class AnimalContainer<T extends Animal & Runnable & Serializable> {
private T animal;
// 方法省略...
}
(3)使用注意
// 合法:Integer是Number的子类
NumberContainer<Integer> intContainer = new NumberContainer<>();
intContainer.setNumber(100);
// 合法:Double是Number的子类
NumberContainer<Double> doubleContainer = new NumberContainer<>();
doubleContainer.setNumber(3.14);
// 错误:String不是Number的子类,编译报错
// NumberContainer<String> strContainer = new NumberContainer<>();
2. 下界约束(super关键字,仅支持通配符)
表示类型参数必须是“指定类的父类”或“指定接口的父接口”,语法:<? super 边界类型>(注意:这里用的是通配符?,不是普通类型参数T)。
具体用法将在“通配符”部分详细说明,此处先记住核心:下界约束仅用于通配符,目的是限制“写入”的数据类型。
五、通配符(?,解决泛型协变/逆变问题)
Java泛型默认不支持协变(比如:String[]是Object[]的子类,但List<String>不是List<Object>的子类),为了解决这个问题,引入了通配符?,表示“未知类型”。
通配符分为三种:无界通配符、上界通配符、下界通配符,核心作用是灵活适配不同的泛型类型,同时保证类型安全。
1. 无界通配符(?)
(1)定义
? 表示任意引用类型,相当于<? extends Object>,适用于“只关心是泛型类型,不关心具体类型”的场景(比如仅读取数据,不写入数据)。
(2)代码示例
// 无界通配符方法:仅读取数据,不写入
public void printData(GenericClass<?> container) {
// 合法:读取数据,返回Object类型(因为未知具体类型)
Object data = container.getData();
System.out.println("数据内容:" + data);
// 错误:无法写入数据(除了null),因为不知道具体类型,无法保证安全
// container.setData("test");
// 唯一合法的写入:null(null是所有引用类型的默认值)
container.setData(null);
}
2. 上界通配符(? extends T)
(1)定义
表示未知类型是“T或T的子类”,适用于“消费数据”的场景(仅读取,不写入,除了null),核心是“只能取,不能存”。
(2)代码示例
// 上界通配符:未知类型是Number或其子类
public void printNumber(GenericClass<? extends Number> container) {
// 合法:读取数据,返回Number类型(子类对象可赋值给父类引用)
Number num = container.getData();
System.out.println("数字值(转为double):" + num.doubleValue());
// 错误:无法写入数据,不知道具体是Integer还是Double
// container.setData(100); // 可能是Integer,也可能是Double,不安全
}
3. 下界通配符(? super T)
(1)定义
表示未知类型是“T或T的父类”,适用于“生产数据”的场景(仅写入T或T的子类,读取返回Object),核心是“只能存,不能取(除非强转)”。
(2)代码示例
// 下界通配符:未知类型是Integer或其父类(如Number、Object)
public void addInteger(GenericClass<? super Integer> container) {
// 合法:写入Integer或其子类(假设存在MyInteger extends Integer)
container.setData(100); // Integer类型,符合要求
// container.setData(new MyInteger()); // 子类也合法
// 读取:返回Object类型,需强转才能使用具体类型的方法
Object data = container.getData();
Integer num = (Integer) data; // 强转,有风险(需确保实际类型是Integer)
}
4. 通配符核心总结
|
通配符类型 |
语法 |
核心场景 |
读写规则 |
|---|---|---|---|
|
无界通配符 |
? |
仅读取,不关心具体类型 |
可读(返回Object),可写null |
|
上界通配符 |
? extends T |
消费数据(仅读取) |
可读(返回T),可写null |
|
下界通配符 |
? super T |
生产数据(仅写入) |
可读(返回Object),可写T/子类 |
六、类型擦除(Java泛型的底层实现)
结合前文提到的所有知识点,这里重点补充类型擦除——Java泛型之所以存在诸多限制(如不能用基本类型、强转有风险),本质是因为类型擦除。
简单来说,类型擦除就是编译器在编译时,会移除所有泛型类型参数信息,将泛型代码转为普通非泛型代码(无边界泛型擦除为Object,有上界泛型擦除为对应上界类型),JVM运行时不感知泛型存在。无需深入展开(详细可参考上篇专门讲解类型擦除的博客),只需记住:类型擦除是Java泛型的底层特性,也是泛型相关限制的根源即可。
博客链接:
结合前文提到的所有知识点,这里重点补充类型擦除——Java泛型之所以存在诸多限制(如不能用基本类型、强转有风险),本质是因为类型擦除。
类型擦除的关键影响(结合泛型知识点)
基本类型不能作为泛型参数:擦除后是Object(引用类型),无法存储基本类型,需用包装类;
运行时无法获取泛型类型:instanceof无法判断Box<String>和Box<Integer>,因为擦除后都是Box;
泛型强转有风险:运行时无泛型信息,无法校验强转的正确性,容易抛出ClassCastException;
不能创建泛型数组:new T[10]擦除后是new Object[10],强制转换有类型安全风险。
七、泛型核心知识点总结
1. 泛型本质:类型参数化,编译期类型检查,运行时类型擦除;
2. 泛型类:类级别的类型参数,创建对象时指定具体类型;
3. 泛型方法:方法级别的类型参数,独立于泛型类,调用时自动推断类型;
4. 泛型约束:上界(extends)限制类型为子类/实现类,下界(super)仅支持通配符;
5. 通配符:解决协变问题,无界(?)、上界(? extends T)、下界(? super T),牢记读写规则;
6. 泛型强转:谨慎使用,明确类型可强转,否则有运行时风险;
7. 类型擦除:底层实现,解释泛型的诸多限制,核心是编译期擦除类型参数。
更多推荐


所有评论(0)