提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
1. 泛型是什么,为什么需要(#一泛型是什么为什么需要)
2. 基本语法:类、接口、方法、构造方法(#二基本语法类接口方法构造方法)
3. 边界与通配符:extends、super 与 PECS(#三边界与通配符extendssuper-与-pecs)
4. 泛型与继承:不变性、协变、逆变(#四泛型与继承不变性协变逆变)
5. 类型擦除:编译期的戏法(#五类型擦除编译期的戏法)
6. 常见限制与易错点(#六常见限制与易错点)
7. 实战示例(#七实战示例)
8. 练习题(#八练习题)
9. 总结与实践建议(#九总结与实践建议) 2. 基本语法:类、接口、方法、构造方法 3. 边界与通配符:extends、super 与 PECS 4. 泛型与继承:不变性、协变、逆变 5. 类型擦除:编译期的戏法 6. 常见限制与易错点 7. 实战示例 8. 练习题 9. 总结与实践建议)
前言
先假装你要讲给「会写 `ArrayList`、但一看到 `<T>` 就发懵」的朋友听------把泛型拆成生活画面:贴了标签的收纳盒、快递单上的「易碎」限制、仓库里「只进不出 / 只出不进」的规则。你合上文章,能用自己的话说清楚「为什么要用泛型、`extends` 和 `super` 差在哪、类型擦除擦掉了什么」,就算真懂了。
泛型(Generics)是 Java 5 引入的核心特性:让类、接口、方法在**编译期**用「类型参数」表达「我装的是什么」,从而获得类型安全、少写强制转换、代码可复用。本文面向初学者与有一定基础的读者,从「为什么需要」讲到通配符、PECS、类型擦除与常见陷阱,并配可运行示例与示意图。文中代码基于 **Java 17**(语法兼容 Java 8+)。
一、泛型是什么,为什么需要?
1.1 生活画面:贴标签的收纳盒
没有泛型时,集合像【什么都能塞】的杂物箱;有了泛型,就像给盒子贴上标签:【此盒只装书】。
生活中的画面 | 编程里的对应 |
|-------------|-------------|
| 杂物箱,什么都往里扔 | `List` 原始类型,装 `Object` |
| 取出来还要自己认:「这是书还是鞋子?」 | 强制类型转换 `(String) list.get(0)` |
| 认错了 → 穿鞋当书用 → 出事 | `ClassCastException` |
| 贴标签:只装书 | `List<String>`,编译期就拦住错误 |
一句话 :泛型 = 把【装什么类型】从运行时猜测,提前到编译期检查的参数化类型机制。

1.2使用前后对比
java
package Exercise.generic;
import java.util.ArrayList;
import java.util.List;
public class BeforeAfterDemo {
public static void main(String[] args) {
//没有泛型(原始类型,不推荐)
List raw = new ArrayList();
raw.add("hello");
raw.add(123);//编译能过!埋雷
// String s = (String) raw.get(1);//运行时ClassCastException
//有泛型
List<String> safe = new ArrayList<>();
safe.add("hello");
String ss = safe.get(0);//无需强转
//safe.add(123);//编译错误:不兼容的类型
// System.out.println(s);
System.out.println(ss);//输出hello
}
}
1.3泛型带来的三大好处
1.类型安全:错误尽量在编译期发现,而不是上线后炸。
2.代码复用:一套Box<T>服务多种类型,不必为每种类型抄一份。
3.减少类型转换:取出元素时类型已知,代码更干净。
【泛型解决的根本问题是什么?】------答:让【容器/算法】与【具体元素类型】解耦,同时不牺牲类型安全。
二、基本语法:类、接口、方法、构造方法
2.1.命名约定(先认脸)
代码如下(示例):
2.读入数据
| 常见参数 | 含义 | 典型场景 |
|---------|------|----------|
| `T` | Type | 通用类型 |
| `E` | Element | 集合元素 |
| `K` | Key | Map 的键 |
| `V` | Value | Map 的值 |
| `N` | Number | 数字相关 |
| `R` | Return | 返回类型 |
名字本身没有魔法,约定是为了读代码时【一眼知道角色】。
2.2泛型类
java
package Exercise.generic;
// 一个「万能盒子」:装什么,由创建时决定
public class Box <T>{
private T value;
public void set(T value){
this.value = value;
}
public T get(){
return value;
}
public static void main(String[] args) {
Box<String >box = new Box<>();
box.set("passion");
System.out.println(box.get());//passion
Box<Integer> numBox = new Box<>();
numBox.set(60);
System.out.println(numBox.get());//60
}
}
Box<T>
┌─────────────┐
│ value : T │ ← T 在编译期被「替换」成具体类型的检查规则
│ set(T) │
│ get() → T │
└─────────────┘
│
┌────┴────┐
Box<String> Box<Integer>
2.3泛型接口
java
public interface Repository<T, ID> {
T findById(ID id);
void save(T entity);
}
// 实现时固定类型参数
public class UserRepository implements Repository<User, Long> {
@Override
public User findById(Long id) { /* ... */ return null; }
@Override
public void save(User entity) { /* ... */ }
}
2.4 泛型方法与泛型构造方法
java
package Exercise.generic;
import Exercise.blog.PartTimeEmployee;
public class GenericMethodDemo {
// 泛型方法:从数组里取第一个元素
public static <T> T first(T[] arr){
return arr == null || arr.length == 0 ? null : arr[0];
}
//泛型构造方法(较少见):构造时推断类型
static class Pair{
private final Object left;
private final Object right;
public <L,R> Pair(L left,R right){
this.left = left;
this.right = right;
}
}
public static void main(String[] args) {
String s =first(new String[]{"a","b"});//推断T=String
Integer n = first(new Integer[]{1,2});
System.out.println(s+", "+n);//a, 1
new Pair("x",1);
}
}
| 对比项 | 泛型类 | 泛型方法 |
|--------|--------|----------|
| 类型参数作用域 | 整个类实例生命周期 | 单次方法调用 |
| 声明位置 | `class Foo<T>` | `<T> 返回类型 方法名(...)` |
| 典型用途 | 容器、实体包装 | 工具方法、算法 |
三、边界与通配符:extends、super 与 PECS
3.1 有界类型参数:限制【T 必须是某种东西】
java
public static <T extends Number>double sum(T a,T b){
return a.doubleValue() + b.doubleValue();
}
public static void main(String[] args) {
sum(1,2);// OK,Integer
sum(1.2,1.6);// OK,Double
sum("a","b")// 编译错误
}
这里的extends 不是 【只能继承类】------在泛型里它表示上界:T是Number 或Number的子类型(类或接口都行,多个边界用 &:<T extends A& B>)。
3.2 三种通配符对照
写法 | 含义(人话) | 能读出什么 | 能写入什么 |
|------|-------------|-----------|-----------|
| `<?>` | 未知类型的盒子 | `Object`(信息很少) | 基本不能写(除 `null`) |
| `<? extends T>` | 盒子里装的是 T 或 T 的子类 | 当作 `T` 读 | 不能写(除 `null`) |
| `<? super T>` | 盒子能装 T 或 T 的父类 | 只能当 `Object` 读 | 可以写入 `T` |

3.3为什么 `List<Object>` 不能接收 `List<String>`?
因为泛型在 Java 里是**不变的(invariant)**:`List<String>` 不是 `List<Object>` 的子类型。
java
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
//List<Object> objects = strings;//编译错误
// 假如允许,就会捅出大娄子:
// objects.add(123); // 往「号称装 String 的列表」塞了 Integer
// String s = strings.get(0); // 运行时爆炸
}
需要【只读一堆Number 子类】时,用通配符:
java
void printALL(List<? extends Number>list){
for (Number n : list){
System.out.println(n);
}
printALL(List.of(1,2,3));//List<Integer> Ok
printALL(List.of(1.1,2.2));//List<Double>
}
3.4 PECS 原则(务必记住)
PECS = Producer Extends, Consumer Super
**- Producer(往外拿数据)用 `extends`:你从集合里读,用 `<? extends T>`。
- Consumer(往里塞数据)用 `super`:你向集合里写,用 `<? super T>`。**
java
package Exercise.generic;
import java.util.ArrayList;
import java.util.List;
public class PecDemo {
//src是生成者->extends;dst是消费者->super
public static <T> void copy(List<? extends T> src,List<? super T>dst){
for (T item: src){
dst.add(item);
}
}
public static void main(String[] args) {
List<Integer> src = List.of(1,2,3);
List<Number> dst = new ArrayList<>();
copy(src,dst);
System.out.println(dst);//[1, 2, 3]
}
}
口诀:【从哪读用extends,往哪写用super:又读又写就别用通配符,直接用T。】
四、泛型与继承:不变性、协变、逆变
4.1 三个词的人话版
| 概念 | 人话 | Java 中的例子 |
|------|------|----------------|
| 不变性 | 父子类型关系**不**传递到泛型参数上 | `List<String>` ≠ `List<Object>` |
| 协变 | 子类型关系「顺着」保留 | 数组:`String\[\]` 可赋给 `Object\[\]`(但不安全) |
| 逆变 | 子类型关系「反过来」 | 通配符 `? super T` 表达一种受控的逆变写能力 |
继承关系: Integer → Number → Object
(子) (父)
泛型不变: List<Integer> 不能当成 List<Number>
数组协变: Integer\[\] 能赋给 Number\[\] (运行时可能 ArrayStoreException)
通配符: List<? extends Number> 可读 Number
List<? super Integer> 可写 Integer
4.2 泛型类之间的继承
java
class Animal{}
class Dog extends ArrayList{}
class Cage<T>{
private T animal;
void put(T t){
animal = t;
}
T get(){
return animal;
}
// Cage<Dog> 并不是 Cage<Animal> 的子类
// Cage<Animal> cage = new Cage<Dog>(); // 编译错误
// 正确:让通配符表达「只读动物」
void listen(Cage<? extends Animal> cage){
Animal a= cage.get();//OK
//cage.put(new Dog());//不行:不知道具体是哪种Cage
}
常见错误与正确写法:
java
// 错误:以为子类泛型可以赋给父类泛型
// List<Animal> animals = new ArrayList<Dog>();
// 正确 1:用通配符
List<? extends Animal> animals = new ArrayList<>();
//正确2:直接用具体类型,自己保证脂肪Dog
List<Dog> dogs = new ArrayList<>();
五、类型擦除:编译期的戏法
5.1 什么是类型擦除?
编译器检查完泛型后,字节码里几乎不保留具体类型参数------List<String>在运行时大题就是List。
这叫类型擦除Type Erasure)。

java
public static void main(String[] args) {
List<String> a = new ArrayList<>();
List<String> b =new ArrayList<>();
System.out.println(a.getClass() == b.getClass());//true
//运行时两者都是class java.util.ArrayList
}
5.2为什么采用擦除?
历史原因:Java5引入泛型时,要兼容已有的、不带泛型的字节码与库(迁移兼容),选择了擦除而不是像部分语言那样在运行时保留完整泛型信息(具体化)。
5.3v 擦除带来的直接后果
1.不能 new T():运行时不知道 T 是谁。
2.不能 直接 new T【】:泛型数组创建受限。
3.不能 用基本类型:`List<int>` 非法,要用 `List<Integer>`。
4.反射拿到的往往是擦除后的原始信息,细节依赖`ParameterizedType` 等才能抠出「声明时」的泛型签名。
java
// 为什么不能直接创建泛型数组?
// T[] arr = new T[10]; // 编译错误
// 常见变通:用反射或 Collection
@SuppressWarnings("unchecked")
static <T> T[] newArray(Class<T> clazz, int size) {
return (T[]) java.lang.reflect.Array.newInstance(clazz, size);
}
java
// 泛型与反射:运行时擦除,但类文件里保留「签名」
import java.lang.reflect.*;
public class ReflectGeneric {
public List<String> names;
public static void main(String[] args) throws Exception {
Field f = ReflectGeneric.class.getField("names");
Type t = f.getGenericType();
if (t instanceof ParameterizedType pt) {
System.out.println(pt.getActualTypeArguments()[0]); // class java.lang.String
}
}
}
六、常见限制与易错点
6.1速查表
限制 | 原因 | 怎么办 |
|------|----------------|--------|
| `new T()` | 擦除后不知道造谁 | 传入 `Class<T>` / `Supplier<T>` |
| `new T\[\]` | 数组要记真实组件类型 | 用 `List<T>` 或 `Array.newInstance` |
| 静态成员用类上的 `T` | 静态属于类,不属某个 `T` 实例 | 静态方法自己声明 `<T>` |
| 原始类型 `List` | 关掉泛型检查 | 逐步改成 `List<?>` / `List<T>` |
| 擦除导致重载冲突 | `foo(List<String>)` 与 `foo(List<Integer>)` 擦除后签名相同 | 改名或换参数结构 |
java
package Exercise.generic;
public class LimitsDemo<T> {
/* private static T cache;//错误:静态不能用类型T
public T create(){ //错误
return new T();
}*/
public static <T> T create(java.util.function.Supplier<T> factory){
return factory.get();//正确:把【怎么造】交给调用方
}
}
6.2 原始类型的风险
java
public static void main(String[] args) {
List list = new ArrayList<String>();
list.add(1);//警告,但能编译
// 取出时可能 ClassCastException
}
6.3 重载冲突示例
java
// 以下两个方法不能同时存在------擦除后都是 foo(List)
// void foo(List<String> list) {}
// void foo(List<Integer> list) {}
6.4 不当强转
泛型减少了强转,但不等于可用乱转:List 原始类型 + `(String) get(i)` 仍是雷区。原则是:能让编译器证明类型,就不要自己骗编译器。
七、实战示例
7.1统一API响应:Response<T>
业务接口经常要返回【状态 + 数据】。用泛型让data 类型随接口变化:
java
package Exercise.generic;
public class Response <T>{
private final int code;
private final String message;
private final T data;
private Response(int code,String message,T data){
this.code = code;
this.message = message;
this.data = data;
}
public static <T> Response<T> ok(T data){
return new Response<>(200,"OK",data);
}
public static <T> Response<T> fail(String message){
return new Response<>(500,message,null);
}
public int getCode(){
return code;
}
public String getMessage() {
return message;
}
public T getData() {
return data;
}
public static void main(String[] args) {
Response<String > r1 = Response.ok("登录成功");
Response<Integer > r2 = Response.ok(42);
System.out.println(r1.getData());//登录成功
System.out.println(r2.getData());//42
}
}
API 设计价值* 调用方 `Response<User>` 与 `Response<List<Order>>` 共享同一套成功/失败协议,又不必牺牲 data 的类型信息。
7.2 泛型工具方法:安全取值
java
package Exercise.generic;
import java.util.HashMap;
import java.util.Map;
public final class MapUtils {
private MapUtils() {}
/** 从 Map 取 V;缺失时用默认值(消费者侧用 super 更灵活时可再扩展) */
public static <K, V> V getOrDefault(Map<K, V> map, K key, V defaultValue) {
V v = map.get(key);
return v != null ? v : defaultValue;
}
public static void main(String[] args) {
Map<String,Integer> scores =Map.of("Alice",95);
System.out.println(getOrDefault(scores,"Alice",0));//95
System.out.println(getOrDefault(scores,"Bob",0));//0
}
}
7.3 集合场景串联
java
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Java");
Map<String,List<String>> groups = new HashMap<>();
groups.put("lang",names);
// 编译器帮你保证:键是 String,值是 List<String>
}
九、总结与实践建议
核心思想
-
泛型把「装什么」变成编译期契约,换类型安全与复用。
-
Java 泛型不变 + 类型擦除 :通配符(PECS)用来安全地表达只读/只写。
-
`extends` 在泛型里是「上界 / 生产者」,`super` 是「下界 / 消费者」。
-
限制大多来自擦除:别碰 `new T()`、基本类型参数、冲突重载、原始类型。
开发建议
-
API 入参:只读集合用 `List<? extends T>`,只写用 `List<? super T>`,读写都要就用 `List<T>`。
-
优先 `List`/`Map` 等带泛型的集合,避免原始类型。
-
工具方法优先写成泛型方法,类型交给调用处推断。
-
需要运行时类型时,显式传 `Class<T>`,不要幻想擦除后还能 `instanceof T`。
容易混淆的点
易混点 | 记住 |
|--------|------|
| `List<Object>` vs `List<?>` | 前者只要 Object;后者是「某种未知类型」,更灵活于只读场景 |
| `T extends Number` vs `? extends Number` | 前者给类型参数起名还可在方法体使用 `T`;后者是通配,不能当「可写的 T」 |
| 数组协变 vs 泛型不变 | 数组看起来方便但不安全;泛型更严但更稳 |