深入理解 Java 泛型:把「万能盒子」用对、用稳

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

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>
    }

九、总结与实践建议

核心思想

  1. 泛型把「装什么」变成编译期契约,换类型安全与复用。

  2. Java 泛型不变 + 类型擦除 :通配符(PECS)用来安全地表达只读/只写。

  3. `extends` 在泛型里是「上界 / 生产者」,`super` 是「下界 / 消费者」。

  4. 限制大多来自擦除:别碰 `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 泛型不变 | 数组看起来方便但不安全;泛型更严但更稳 |

相关推荐
大猫和小黄1 小时前
深入理解 Java Optional:从设计哲学到工程实战
java
数据库百宝箱1 小时前
rum&gin索引对比
java·数据库·gin
架构技术专栏1 小时前
Skill 有效性与稳定性评估:用对照、重复与证据判断价值
后端
2601_962218611 小时前
C语言中三种库的编译和使用方法
开发语言·c++
传奇开心果编程2 小时前
中庸、有容、破执:仓颉编程语言的中华智慧结晶——三重哲学境界思辨
开发语言·华为·开源·软件工程
yychen_java2 小时前
第六篇:Spring AI 实战:将 Java 业务接口封装成企业级 MCP Server
java·人工智能·spring
jimy12 小时前
派生类重写基类虚函数(二):override的作用
开发语言·c++
小雪崩2 小时前
嵌入式学习 day60:ARM汇编——触摸屏与SPI
汇编·arm开发·学习
拔剑纵狂歌2 小时前
ClickHouse system.query_log 字段详解与排障实践
后端·clickhouse·1024程序员节