【Java 进阶】泛型深度解析|泛型类、泛型方法、通配符、类型擦除、PECS 原则

一、前言

泛型(Generics)是 JDK 1.5 引入的重要特性,核心思想:类型参数化。 简单理解:把数据类型当作参数传递,定义类 / 接口 / 方法时不固定类型,在使用的时候再指定具体类型。

泛型带来的 3 大好处

  1. 编译期类型检查 :类型错误在编译阶段就发现,而不是运行时抛出ClassCastException;
  2. 消除强制类型转换,代码更简洁;
  3. 代码复用:一套逻辑支持多种引用类型,不用重复编写相似类。

注意:Java 泛型是编译期泛型 ,依靠类型擦除实现,运行期 JVM 不保留泛型信息。这是 Java 泛型和 C++ 模板最大区别。

常用泛型标识符(约定俗成)

  • T Type:任意类型
  • E Element:集合元素(Collection/List)
  • K Key:Map 键
  • V Value:Map 值
  • R Return:返回值类型
  • S,U:多个额外类型参数

二、基础语法:泛型类、泛型接口、泛型方法

1. 泛型类

定义类的时候,在类名后<T>声明类型参数。

java 复制代码
public class Box<T> {
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

使用:

java 复制代码
Box<String> strBox = new Box<>();
strBox.setData("hello");
Box<Integer> intBox = new Box<>();
intBox.setData(100);

2. 泛型接口

java 复制代码
public interface IGenerator<T> {
    T next();
}

实现方式:

  1. 实现类确定类型:
java 复制代码
public class StringGenerator implements IGenerator<String>{
    @Override
    public String next() { return "test"; }
}
  1. 实现类继续保留泛型参数:
java 复制代码
public class ObjectGenerator<T> implements IGenerator<T>{
    @Override
    public T next() { return null; }
}

3. 泛型方法⭐重点

泛型方法的类型参数写在方法返回值前面,和泛型类无关。

即使类不是泛型类,也可以单独定义泛型方法。

java 复制代码
public class GenericUtil {
    // <T> 声明泛型参数
    public static <T> T getFirst(T[] arr){
        if(arr == null || arr.length == 0){
            return null;
        }
        return arr[0];
    }
}

调用时编译器自动类型推断:

java 复制代码
String s = GenericUtil.getFirst(new String[]{"A","B"});

区分坑:泛型类上的 T 和 泛型方法上的 T,不是同一个类型参数,名字相同也互相独立。

三、有界类型(限定泛型参数范围)

可以限制泛型参数必须是某个类或者它的子类,使用extends上界限定。

注意:extends在这里代表上限,不仅可以是类,也可以是接口。

java 复制代码
// T只能是Number或者Number的子类:Integer、Double等
public <T extends Number> double sum(T a, T b){
    return a.doubleValue() + b.doubleValue();
}
  • 有界之后,编译器知道 T 是 Number 子类,可以直接调用 Number 里面的方法。
  • 没有super用来限定类型参数 ,super只用于通配符。

四、通配符?(Wildcard)

通配符?代表未知类型,主要用于方法参数,提升 API 灵活性。 分三类:无界通配符、上界通配符、下界通配符。

1. 无界通配符 <?>

代表任意未知类型。

适用:只读取,不写入,不关心元素具体类型。

java 复制代码
public static void printList(List<?> list){
    for(Object obj : list){
        System.out.println(obj);
    }
}

⚠️ 限制:不能 add,除了 null。因为不知道集合真实类型,无法保证类型安全。

2. 上界通配符 <? extends T>

代表T 或者 T 的子类,协变。

java 复制代码
// 可以接收 List<Integer>、List<Double>、List<Number>
public static double sumNumberList(List<? extends Number> list){
    double sum = 0;
    for (Number num : list) {
        sum += num.doubleValue();
    }
    return sum;
}

✅ 可读:取出元素一定是 Number; ❌ 不可写入:编译器不知道集合真实子类型,禁止 add,无法保证类型安全。

3. 下界通配符 <? super T>

代表T 或者 T 的父类,逆变。

java 复制代码
// 接收 List<Integer>、List<Number>、List<Object>
public static void addInt(List<? super Integer> list){
    list.add(100);
    list.add(200);
}

✅ 可以写入Integer 及其子类; ❌ 读取的时候只能拿到 Object 类型。

五、PECS 原则(面试高频,出自 Effective Java)

PECS:Producer Extends,Consumer Super

  • Producer(生产者,读取数据 ):使用 <? extends T>;
  • Consumer(消费者,写入数据 ):使用 <? super T>;
  • 既要读又要写:不要使用通配符,直接使用确定泛型<T>。

经典例子:Collections.copy()

java 复制代码
public static <T> void copy(List<? super T> dest, List<? extends T> src)

src 是源,生产者读取 → ? extends T dest 是目标,消费者写入 → ? super T

六、类型擦除 Type Erasure【泛型底层原理,必考点】

Java 泛型的实现方式:编译期校验类型,编译完成后擦除泛型信息。运行时 JVM 看不到泛型。

擦除规则

  1. 无界类型参数<T>:擦除为 Object
  2. 有界类型参数<T extends Number>:擦除为边界类型Number
  3. 编译器自动插入强制类型转换,保证代码正常运行。

示例:

java 复制代码
List<String> list = new ArrayList<>();
list.add("test");
String s = list.get(0);

编译擦除之后,等价原始代码:

java 复制代码
List list = new ArrayList();
list.add("test");
String s = (String) list.get(0);

重要结论

java 复制代码
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
// true!运行时两者都是ArrayList,泛型信息被擦除
System.out.println(list1.getClass() == list2.getClass());

运行时List<String> 和 List<Integer>是同一个 Class。

桥接方法 Bridge Method

当泛型类继承 / 实现泛型接口,类型擦除会导致方法签名不一致。编译器自动生成桥接方法,保证多态正常执行。

面试追问:桥接方法作用? 答:类型擦除之后,父类原始类型方法和子类泛型方法签名不一致,编译器自动生成桥接方法,保证多态调用正常。

七、泛型的局限性(坑点,开发不能踩)

因为类型擦除,泛型存在很多限制:

  1. 不能使用基本类型 ,List<int>编译报错,只能List<Integer>(装箱)
  2. 不能 new T (),运行时 T 被擦除,不知道真实类型;
  3. 不能创建泛型数组 new T[10];可以创建Object[]或者使用Array.newInstance反射创建;
  4. 不能用 instanceof 判断泛型类型
java 复制代码
if(list instanceof List<String>) {} // 编译报错,泛型运行时擦除
if(list instanceof List<?>) {} // ✅合法
  1. 静态变量不能使用类上的泛型参数;静态方法如果要用泛型,必须单独定义静态泛型方法。
  2. 异常不能是泛型类,class MyException<T> extends Exception编译报错。

八、不变、协变、逆变(数组 vs 泛型集合)

  • 数组是协变 :String[]是Object[]的子类型
java 复制代码
Object[] arr = new String[5]; // 合法,但运行时存Integer会抛ArrayStoreException
  • 泛型集合是不变 :List<String>不是 List<Object>的子类型!
java 复制代码
List<Object> list = new ArrayList<String>(); // ❌编译直接报错

这就是为什么需要通配符? extends / ? super来获得协变 / 逆变能力。

九、泛型开发实战避坑指南

坑 1:原始类型 Raw Type

List list = new ArrayList(),不带泛型参数就是原始类型。 原始类型跳过编译期泛型检查,容易带来ClassCastException,开发尽量不要使用。

坑 2:泛型擦除导致反射拿不到真实类型

List<String>在运行期只能拿到List,拿不到 String。Spring 中TypeReference就是用来解决这个问题,通过匿名内部类保留泛型信息。

java 复制代码
// Spring 泛型捕获
TypeReference<List<String>> typeRef = new TypeReference<List<String>>(){};

坑 3:PECS 用反,导致编译报错

读取数据用 super,写入用 extends,会出现无法读 / 写编译错误。记住生产者 extends,消费者 super。

坑 4:泛型方法和泛型类参数重名混淆

类上<T>和方法<T>是两套独立参数,尽量不要同名,避免阅读混淆。

十、面试高频问答(背诵版)

Q1:Java 泛型底层原理是什么?

A:Java 泛型基于类型擦除实现。编译阶段做类型校验;编译完成后,泛型信息被擦除,无界 T 变成 Object,有界 T 变成边界类型;编译器自动插入类型强转。泛型信息只存在编译期,运行时 JVM 不存在。

Q2:List<String>是不是 List<Object>子类?

A:不是,泛型集合不变。List<String>不是 List<Object>的子类型;数组是协变。

Q3:<? extends T> 和 <? super T>区别?PECS 原则?

A:? extends T上界通配符,代表 T 或子类,生产者,可读不可写 ;? super T下界通配符,代表 T 或父类,消费者,可写,读取只能得到 Object。PECS:Producer Extends,Consumer Super。

Q4:为什么不能 new T ()?

A:类型擦除,运行期 T 变成 Object,无法获取原始类型信息,不能实例化。

Q5:桥接方法是什么?

A:类型擦除之后,父类方法签名和子类方法签名不一致,编译器自动生成桥接方法,保证多态调用正常执行。

Q6:泛型和数组协变的问题?

A:数组协变,编译不报错,运行存入错误类型抛 ArrayStoreException;泛型集合不变,编译期直接拦截,类型更安全。

十一、总结

Java 泛型通过类型参数化 实现编译期类型安全,核心底层机制是类型擦除。 泛型类、泛型接口可以把类型定义在类级别;泛型方法可以独立声明类型参数。 通配符?用来提升 API 灵活性:? extends T用于读取生产者,? super T用于写入消费者,遵循 PECS 原则。 受类型擦除限制,泛型有很多限制,不能使用基本类型、不能直接 new T ()、无法 instanceof 判断泛型参数。开发中合理使用通配符,避免原始类型,写出更安全通用的工具类。泛型大量应用在集合、Spring、MyBatis、RPC 框架中,是后端面试高频考点。

相关推荐
VX_bysjlw9852 小时前
基于PHP的论坛系统设计与实现 35136-计算机毕设原创(免费领源码+带部署教程)
开发语言·php·apache·laravel·thinkphp·phpstudy·wampserver
忆~遂愿2 小时前
电影照片散在硬盘里不好找?我用群晖+Plex整理成可远程访问的家庭媒体库
开发语言·php
这个DBA有点耶2 小时前
MySQL B-tree索引并发控制优化:写密集场景的锁瓶颈与内核级解法
java·数据库·程序人生·算法·database·dba
RuoyiOffice2 小时前
Spring AI+Vue3 企业知识库设计:让每个答案有依据,让每份资料守住权限
java·spring boot·vue3·rag·企业知识库·spring ai·ruoyi office
卢锡荣2 小时前
乐得瑞Type-c接口产品描述
c语言·开发语言
晴天的雨.9922 小时前
【C++】模板进阶
开发语言·c++·算法
倔强的石头1063 小时前
【Linux指南】进程通信系列(二):匿名管道 —— pipe 的本质、fork 继承与四种读写规则
java·linux·运维
调试优选官3 小时前
2026年IoT物联网开发赛道年度榜单深度解析:D-coding物联网系统定制与源码交付能力专项解读
java·物联网·struts·iot·开发经验
木井巳3 小时前
【动态规划】三步问题
java·算法·leetcode·动态规划·推荐算法