1、泛型是什么?
1.1 泛型的定义
泛型(Generic)是 Java 5 引入的特性,本质是一种「类型参数化」技术—— 简单说,就是允许我们在定义类、接口、方法时,不直接指定具体的数据类型,而是用一个「类型占位符」(如 T、E、K、V)来代替,等到实际使用(创建对象、调用方法)时,再传入具体的类型(如 String、Integer)进行约束。
泛型的核心目标是让代码具备通用性,同时在编译期保证类型安全,避免类型转换异常。
1.2 为什么需要泛型
在泛型出现之前,java用Object来实现通用代码,但是存在两个致命的问题:类型不安全、需要频繁强制转换类型。
(1)无泛型示例
public class NoGenericDemo {
public static void main(String[] args) {
//无泛型list
List list = new ArrayList();
//可以添加任意类型数据
list.add("1");
list.add(true);
list.add(10);
//取数据时必须强制转换
String s = (String) list.get(0); //正常转换
Integer i = (Integer) list.get(2); //正常转换
String error = (String) list.get(1); //ClassCastException
}
}
代码存在的问题:
编译期间无法发现类型错误问题,只有运行时才会抛出ClassCastException(类型转换异常);
必须手动强制转换,代码繁琐,效率低
(2)有泛型的解决方案
public class GenericDemo {
public static void main(String[] args) {
//定义泛型List,执行类型为String(编译期间约束,只能存储String类型数据)
List<String> list = new ArrayList<>();
// 只能添加String类型数据,其他类型编译会报错(类型安全)
list.add("1");
list.add("2");
//list.add(3); //编译会报错,不兼容的类型
/// 获取数据时,会自动进行类型转换
String s = list.get(0);
String s1 = list.get(1);
System.out.println(s);
}
}
泛型带来的核心价值:
编译期类型安全:限制容器 / 方法只能操作指定类型的数据,非法类型编译直接报错,避免运行时类型转换异常;
消除冗余的强制类型转换:编译器自动完成类型转换,代码更简洁、高效;
提升代码通用性:一套泛型代码可以适配多种具体类型,无需为每种类型编写重复代码。
2、泛型通配符
泛型通配符用 ? 表示,称为「未知类型通配符」,它的核心作用是:表示一个未知的泛型类型,用于灵活约束泛型的类型范围,解决泛型类型之间不兼容(无法向上向下转型)的问题。
先看一个泛型不兼容的痛点示例,理解通配符的必要性:
import java.util.ArrayList;
import java.util.List;
public class WildcardNeedDemo {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
// 错误:List<String> 不能赋值给 List<Object>,泛型不支持直接向上转型
// List<Object> objectList = stringList;
// 需求:编写一个通用方法,能打印任意类型的 List 集合(String、Integer 等)
// 若无通配符,需要为每种类型编写重复方法,无法实现通用
printList(stringList);
printList(integerList);
}
// 使用通配符 ?,表示接收任意类型的 List 集合
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.print(obj + " ");
}
System.out.println();
}
}
3、泛型限定符
泛型支持使用通配符 ? 表示 “任意类型”,并可以通过 extends 指定上限、super 指定下限,用于灵活约束泛型类型的范围。
<? extends T>:通配符上限,只能接受 T 及其子类类型,可以缩小范围;
<? super T>:通配符下限,只能接受 T 及其父类类型。
2.1限定符的使用
假设有如下三个类
class Animal {
public String toString(){
return "I am an animal";
}
}
class Dog extends Animal {
public String toString(){
return "I am a dog";
}
}
class Cat extends Animal {
public String toString(){
return "I am a cat";
}
}
class WhiteCat extends Cat{
public String toString(){
return "I am a white cat";
}
}
使用限定符extend实现一个MyArrayList类
class MyArrayList<E extends Cat>{
public void add(E e){
System.out.println(e);
}
}
当我们向MyArrayList中添加Dog元素时编译器会提示参数类型错误。
MyArrayList<Cat> list = new MyArrayList<>();
list.add(new Cat());
list.add(new WhiteCat());
//list.add(new Dog()); //类型不匹配
4、类型擦除(泛型的底层实现)
Java 泛型是编译期特性,在运行时会发生「类型擦除」—— 编译器在编译时会将泛型占位符(如 T、E)替换为其上限类型(默认是 Object,有指定上限则替换为上限类型),运行时 JVM 无法感知泛型类型的存在。
示例:编译后的泛型代码会被擦除
// 编译前的泛型代码
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0);
// 编译后的字节码(类型擦除后)
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0); // 编译器自动添加强制类型转换
✏️Note
1、泛型中不同字母的使用场景?
1)语法功能层面:E 和 T 无区别,都是泛型占位符,编译器处理逻辑一致,可互相替换且不影响程序运行;
2)命名规范层面:E 专用于集合 / 容器的元素类型,T 是通用任意类型,遵循惯例可提升代码可读性;
❗注意
通配符只能用于「声明类型、方法参数」,不能用于「创建对象、类定义、方法返回值」
正确:List<? extends Number> list = new ArrayList<Integer>();
错误:new ArrayList<?>(无法创建通配符类型的对象)
错误:public class GenericClass<?>(类定义不能使用通配符,只能使用泛型占位符 T/E 等)
上界通配符「只读」,下界通配符「可写」,无界通配符「仅通用读取」
记住口诀:extends 取上限,只读不写;super 取下限,可写只读(Object)。
泛型占位符(T/E/K/V)与通配符(?)的区别
泛型占位符:是「类型参数」,用于定义泛型类 / 接口 / 方法,代表一个「确定的未知类型」,使用时需要指定具体类型;
通配符:是「类型通配」,用于使用泛型时,代表一个「不确定的未知类型」,直接用于约束类型范围,无需指定具体类型。
通配符不能嵌套使用
错误:List<? extends List<? extends Number>>(虽然编译可能通过,但可读性极差,不推荐)
解决方案:使用泛型占位符封装,提升可读性。