什么是泛型?为什么要泛型?泛型的好处是什么?泛型的类、方法、接口怎么定义?泛型的通配符怎么用?什么是PECS原则?什么是类型擦除?
看这一篇就够啦
😫 痛点引入 :每次写集合都要强转?
Object转String转到你怀疑人生?代码里一堆
instanceof判断类型?泛型来救你了!✨泛型就像带模具的储物盒📦------装什么类型你自己定,取出来不用转型,直接就是目标类型!
一、泛型到底是什么?🔍
1.1 官方定义
泛型 :广泛的类型。在定义类/接口/方法时,某个类型不确定,用一个符号来代替,这个符号就是泛型。
1.2 生活类比
普通盒子(无泛型):
装进去什么都有,取出来还得自己判断类型
盒子里有:苹果、手机、书...
取出来 → 你得先问:"这是啥?" → instanceof判断
泛型盒子<T>(有泛型):
装之前就定好类型,取出来直接用
ArrayList<String> → 只能装字符串,取出来就是String
ArrayList<Integer> → 只能装整数,取出来就是Integer
1.3 泛型的书写格式
java
// 格式:在类名/接口名后面加尖括号,里面写泛型符号
ArrayList<String> list = new ArrayList<>();
HashMap<String, Integer> map = new HashMap<>();
1.4 常见泛型命名约定
| 符号 | 含义 | 使用场景 |
|---|---|---|
T |
Type(类型) | 最常用,任意类型 |
E |
Element(元素) | 集合元素 |
K |
Key(键) | Map的键 |
V |
Value(值) | Map的值 |
R |
Return(返回值) | 返回类型 |
二、泛型的三大好处 🏆
2.1 好处一览表
| 好处 | 说明 | 效果 |
|---|---|---|
| 类型安全 ✅ | 编译期检查类型,错误提前暴露 | 不用等运行时崩溃 |
| 消除强转 ✅ | 取出的数据直接是目标类型 | 不再写 (String) |
| 代码简洁 ✅ | 逻辑更清晰,代码更干净 | 可读性翻倍 |
2.2 代码对比:无泛型 vs 有泛型
java
import java.util.ArrayList;
public class Demo01 {
public static void main(String[] args) {
// ❌ 以前:没有泛型,默认都是Object
ArrayList list1 = new ArrayList();
list1.add("书源");
list1.add(123); // int数字居然也能加进去!
// 取出来必须强转,而且容易出错
Object o = list1.get(0);
String name = (String) o; // 得强转
// Integer num = (Integer) list1.get(1); // 运行时才知道类型错了!
System.out.println("无泛型演示完毕");
// ✅ 现在:有泛型,类型安全
ArrayList<String> list2 = new ArrayList<>();
list2.add("书源");
// list2.add(123); // 编译直接报错!还没运行就告诉你错了!
// 编译错误:The method add(int, String) of type ArrayList<String> is not applicable
// for the arguments (int)
// 取出来直接就是String,不用强转!
String s = list2.get(0); // 完美
System.out.println("有泛型:" + s);
}
}
2.3 ⚠️ 注意事项
前后一致原则:创建对象时,尖括号里前后的泛型类型必须一致!
java
// ✅ 正确:前后一致
ArrayList<String> list = new ArrayList<String>();
// ✅ Java 7 新特性:菱形语法,后面的<>可以不写类型
ArrayList<String> list2 = new ArrayList<>();
// ❌ 错误:前后不一致
// ArrayList<String> list3 = new ArrayList<Integer>(); // 编译错误!
钻石语法(Java 7+):
<>像不像钻石(狗头)
java
// 完整写法
HashMap<String, Integer> map1 = new HashMap<String, Integer>();
// 钻石语法(推荐)
HashMap<String, Integer> map2 = new HashMap<>(); // 后面不用写,编译器自动推断
三、泛型类 🏗️
3.1 什么是泛型类
泛型类:带着泛型定义的类。在类声明时用泛型符号代替某些属性的类型。
3.2 定义格式
java
class 类名<泛型符号1, 泛型符号2...> {
// 泛型符号可以在整个类中使用
泛型符号1 变量名;
泛型符号2 变量名;
}
3.3 经典案例:写一个「储物箱」类
java
import java.util.ArrayList;
public class Demo02 {
public static void main(String[] args) {
// 创建String类型的储物箱
Box<String> box1 = new Box<>();
box1.put("我的宝贝书籍");
box1.print();
// 创建Integer类型的储物箱
Box<Integer> box2 = new Box<>();
box2.put(666);
box2.print();
// 创建自定义类型的储物箱
Box<Person> box3 = new Box<>();
box3.put(new Person("书源", 18));
box3.print();
}
}
// 定义一个泛型类:储物箱
class Box<T> {
private T content;
private ArrayList<T> list = new ArrayList<>();
// 放东西
public void put(T item) {
list.add(item);
}
// 打印所有内容
public void print() {
System.out.println(list);
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
运行结果:
[我的宝贝书籍]
[666]
[Person{name='书源', age=18}]
四、泛型方法 🔧
4.1 什么是泛型方法
在方法的声明上声明了泛型,这个方法就是泛型方法。
4.2 定义格式
java
修饰符 <泛型声明> 返回值类型 方法名(参数列表) {
// 方法体
}
4.3 经典案例:打印任意类型数组
java
public class Demo03 {
public static void main(String[] args) {
// 测试Integer数组
Integer[] nums = {1, 2, 3, 4, 5};
printArray(nums);
// 测试String数组
String[] names = {"书源", "苏奥", "陈毅"};
printArray(names);
// 测试Character数组
Character[] chars = {'A', 'B', 'C'};
printArray(chars);
}
// 泛型方法:打印任意类型数组
public static <T> void printArray(T[] arr) {
for (T item : arr) {
System.out.print(item + " ");
}
System.out.println();
}
}
4.4 普通方法中使用类声明的泛型 vs 泛型方法单独声明泛型
java
public class Demo04 {
public static void main(String[] args) {
MyClass<String> obj = new MyClass<>();
// 普通方法:使用类声明的泛型T
obj.show("书源是个好学生");
// 泛型方法:方法自己声明了泛型Y,和类的T互不影响
String result = obj.say("Hello", 666);
System.out.println(result);
}
}
class MyClass<T> {
private T data;
// 普通方法:使用类上声明的泛型T
public void show(T t) {
System.out.println("普通方法:" + t);
}
// 泛型方法:方法自己声明了泛型Y
// 注意:泛型方法一般用在参数上,这样调用时能推断类型
public <Y> Y say(Y y, T t) {
System.out.println("泛型方法参数 T = " + t);
System.out.println("泛型方法参数 Y = " + y);
return y;
}
}
4.5 ⚠️ 静态方法的泛型注意
java
class StaticDemo<T> {
T instance; // 实例变量,可以用类上的泛型
// ❌ 错误:静态方法不能直接用类的泛型
// public static void print(T t) {}
// ✅ 正确:静态方法如果想用泛型,必须自己声明static <T>
public static <T> void staticPrint(T t) {
System.out.println("静态方法中的泛型:" + t);
}
}
原因:类的泛型在创建对象时确定,而静态方法不需要对象就可以调用,所以不能依赖类的泛型。
五、泛型接口 📋
5.1 什么是泛型接口
接口也可以声明泛型,让实现类决定具体的类型。
5.2 定义格式
java
interface 接口名<泛型符号> {
泛型符号 方法名();
}
5.3 两种实现方式
java
public class Demo05 {
public static void main(String[] args) {
// 方式1:实现类明确指定泛型类型
StringResult result1 = new StringResult();
result1.setData("书源最棒");
System.out.println(result1.getData());
// 方式2:实现类也定义为泛型类
GenericResult<String> result2 = new GenericResult<>();
result2.setData("苏奥也棒");
System.out.println(result2.getData());
GenericResult<Integer> result3 = new GenericResult<>();
result3.setData(12345);
System.out.println(result3.getData());
}
}
// 定义一个泛型接口
interface Result<T> {
T getData();
void setData(T data);
}
// 方式1:实现类明确指定类型
class StringResult implements Result<String> {
private String data;
@Override
public String getData() {
return data;
}
@Override
public void setData(String data) {
this.data = data;
}
}
// 方式2:实现类也是泛型类
class GenericResult<T> implements Result<T> {
private T data;
@Override
public T getData() {
return data;
}
@Override
public void setData(T data) {
this.data = data;
}
}
六、泛型通配符 🃏
6.1 三种通配符一览
| 通配符 | 含义 | 特点 |
|---|---|---|
<?> |
任意类型 | 不知道、不关心是什么类型 |
<? extends T> |
T 或 T 的子类(上限) | 我要取出来用,只能往小类型靠 |
<? super T> |
T 或 T 的父类(下限) | 我要放进去存,只能往大类型靠 |
6.2 生活类比
类比:装水果的盒子 🧺
<? extends Fruit> → 装苹果、装香蕉、装橘子都行
但我只取出来当 Fruit 用(不知道具体是啥)
<? super Apple> → 装苹果、装水果、装Object都行
但装进去的东西,最小也是 Apple 级别
6.3 代码演示
java
import java.util.ArrayList;
public class Demo06 {
public static void main(String[] args) {
// 准备测试数据
ArrayList<Food> foods = new ArrayList<>();
foods.add(new Food());
ArrayList<Fruit> fruits = new ArrayList<>();
fruits.add(new Fruit());
fruits.add(new Apple());
ArrayList<Apple> apples = new ArrayList<>();
apples.add(new Apple());
ArrayList<RedApple> redApples = new ArrayList<>();
redApples.add(new RedApple());
// printAll 任意类型都可以
printAll(foods);
printAll(fruits);
printAll(apples);
// printFruits 只能传 Fruit 或其子类
printFruits(fruits); // ✅
printFruits(apples); // ✅ Apple是Fruit的子类
// printFruits(foods); // ❌ Food是Fruit的父类,不行!
// printFoods 只能传 Food 或其父类
printFoods(foods); // ✅
printFoods(fruits); // ✅ Fruit的父类是Food
// printFoods(apples); // ❌ Apple是Food的子类,不行!
}
// <?>: 任意类型
public static void printAll(ArrayList<?> list) {
System.out.println("printAll: " + list);
}
// <? extends Fruit>: 只能传 Fruit 及其子类(上限)
public static void printFruits(ArrayList<? extends Fruit> list) {
System.out.println("printFruits: " + list);
// 注意:这里取出来的元素,只能当 Fruit 用,不能当具体子类用
// Fruit f = list.get(0); // ✅
// Apple a = list.get(0); // ❌ 不确定是Apple还是Banana
}
// <? super Apple>: 只能传 Apple 及其父类(下限)
public static void printFoods(ArrayList<? super Apple> list) {
System.out.println("printFoods: " + list);
// 注意:这里可以放 Apple 或其子类进去
list.add(new Apple()); // ✅
list.add(new RedApple()); // ✅ RedApple也是Apple的子类
// Apple a = list.get(0); // ❌ 取出来是Object,还得强转
}
}
// 类层次关系:Food > Fruit > Apple > RedApple
class Food {}
class Fruit extends Food {}
class Apple extends Fruit {}
class RedApple extends Apple {}
6.4 PECS 原则(面试加分项!)💡
PECS = Producer Extends, Consumer Super
| 场景 | 用什么 | 原因 |
|---|---|---|
| 只读取(生产者) | <? extends T> |
取出来是 T,安全 |
| 只写入(消费者) | <? super T> |
写进去要求最小是 T |
| 又读又写 | 不用通配符 | 用具体类型更安全 |
java
// 生产者:用 extends(往外取数据)
public static double sumOfList(ArrayList<? extends Number> list) {
double s = 0.0;
for (Number n : list) {
s += n.doubleValue(); // 往外取,当Number用
}
return s;
}
// 消费者:用 super(往里存数据)
public static void addNumbers(ArrayList<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
// 取出来?不知道是啥,只能当Object用
}
七、类型擦除🔍
7.1 什么是类型擦除
泛型是编译阶段的概念! 编译成字节码后,泛型信息会被擦除,变成原始类型(通常是 Object)。
7.2 擦除过程图解
源代码阶段:
ArrayList<String> list = new ArrayList<>();
list.add("书源");
String s = list.get(0);
编译后(字节码阶段):
ArrayList list = new ArrayList(); // 泛型信息消失,变成ArrayList(无泛型)
list.add("书源");
String s = (String) list.get(0); // 编译器自动加上强转!
运行时:
没有任何泛型信息,全是 Object
7.3 类型擦除的两条规则
| 泛型类型 | 擦除后变成 | 说明 |
|---|---|---|
无边界 <T> |
Object |
编译后变成 Object |
有边界 <T extends Fruit> |
Fruit |
编译后变成边界类型 |
java
class MyClass<T> {
T data; // 擦除后:Object data
}
class MyClass2<T extends Number> {
T data; // 擦除后:Number data
}
7.4 ⚠️ 类型擦除带来的「坑」
java
// 场景:泛型方法重载
class Test {
// ❌ 编译错误!这两个方法在擦除后是一样的!
public void method(List<String> list) {}
public void method(List<Integer> list) {}
// 原因:擦除后都变成 List,参数列表相同
// public void method(List list) {}
// public void method(List list) {} ← 冲突!
}
7.5 为什么需要类型擦除?
历史兼容 :泛型是 Java 1.5 才加入的,之前没有泛型。
为了兼容老代码(ArrayList 不带泛型的代码也能跑),Java 选择了「编译时检查,运行时擦除」的方案。
java
// 老代码(Java 1.4)
ArrayList list = new ArrayList();
list.add("hello");
// 新代码(Java 1.5+)
ArrayList<String> list2 = new ArrayList<>();
list2.add("hello");
// 两者可以混用!因为运行时都是同一个 ArrayList.class
八、综合练习:数据管理器 💪
8.1 需求
定义一个数据管理员类:
- 维护一个私有的 List 集合
- 不知道集合中存储什么类型
- 提供添加、删除、打印的方法
8.2 代码实现
java
import java.util.ArrayList;
import java.util.Iterator;
public class Demo07 {
public static void main(String[] args) {
// 测试 String 类型
DataManager<String> dm1 = new DataManager<>();
dm1.add("书源");
dm1.add("苏奥");
dm1.add("陈毅");
System.out.print("添加后 → ");
dm1.print();
dm1.remove("苏奥");
System.out.print("删除后 → ");
dm1.print();
// 测试 Integer 类型
DataManager<Integer> dm2 = new DataManager<>();
dm2.add(100);
dm2.add(200);
dm2.add(300);
System.out.print("Integer添加后 → ");
dm2.print();
}
}
// 数据管理器:泛型类
class DataManager<T> {
// 维护一个私有的List集合,类型由创建对象时决定
private ArrayList<T> list = new ArrayList<>();
// 添加元素
public void add(T item) {
list.add(item);
}
// 删除元素
public void remove(T item) {
list.remove(item);
}
// 打印所有元素
public void print() {
// 方式1:Lambda(推荐)
// list.forEach(item -> System.out.print(item + " "));
// 方式2:迭代器
Iterator<T> it = list.iterator();
while (it.hasNext()) {
T item = it.next();
System.out.print(item + " ");
}
System.out.println();
}
}
运行结果:
添加后 → 书源 苏奥 陈毅
删除后 → 书源 陈毅
Integer添加后 → 100 200 300
本篇总结 📝
- 泛型基础 🧩:定义时用符号代替类型,使用时确定具体类型
- 三大好处 🏆:类型安全、消除强转、代码简洁
- 泛型类 🏗️:类上声明泛型,整个类都能用(储物箱案例)
- 泛型方法 🔧:方法上声明泛型,静态方法必须自己声明泛型
- 泛型接口 📋:两种实现方式(明确类型 / 继续泛型)
- 泛型通配符 🃏:
?任意、extends上限、super下限 - PECS 原则 💡:生产者用 extends,消费者用 super
- 类型擦除 🔍:编译时检查,运行时擦除成 Object(面试必问!)
作者 :书源丶
发布平台 :CSDN
系列:「Java基础」