三十五、Java 泛型——类型安全的「万能模板」

什么是泛型?为什么要泛型?泛型的好处是什么?泛型的类、方法、接口怎么定义?泛型的通配符怎么用?什么是PECS原则?什么是类型擦除?

看这一篇就够啦

😫 痛点引入 :每次写集合都要强转?ObjectString 转到你怀疑人生?

代码里一堆 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 

本篇总结 📝

  1. 泛型基础 🧩:定义时用符号代替类型,使用时确定具体类型
  2. 三大好处 🏆:类型安全、消除强转、代码简洁
  3. 泛型类 🏗️:类上声明泛型,整个类都能用(储物箱案例)
  4. 泛型方法 🔧:方法上声明泛型,静态方法必须自己声明泛型
  5. 泛型接口 📋:两种实现方式(明确类型 / 继续泛型)
  6. 泛型通配符 🃏:? 任意、extends 上限、super 下限
  7. PECS 原则 💡:生产者用 extends,消费者用 super
  8. 类型擦除 🔍:编译时检查,运行时擦除成 Object(面试必问!)

作者 :书源丶
发布平台 :CSDN
系列:「Java基础」

相关推荐
京师20万禁军教头1 小时前
37面向对象(高级)-main方法
java
EF@蛐蛐堂2 小时前
【js】浏览器滚动条优化组件OverlayScrollbars
开发语言·javascript·ecmascript
dovens2 小时前
SpringBoot集成MQTT客户端
java·spring boot·后端
❀͜͡傀儡师2 小时前
Spring Boot 集成 RocksDB 实战:打造高性能 KV 存储加速层
java·spring boot·后端·rocksdb
代码中介商2 小时前
C++ 仿函数(Functor)深度解析:从基础到应用
开发语言·c++
BENA ceic2 小时前
Spring 的三种注入方式?
java·数据库·spring
小雅痞2 小时前
[Java][Leetcode middle] 209. 长度最小的子数组
java·算法·leetcode
小杍随笔2 小时前
Rust桌面GUI框架:性能优化与实战避坑指南
开发语言·性能优化·rust
二哈赛车手2 小时前
新人笔记---项目中简易版的RAG检索后评测指标(@Recall ,Mrr..)实现
java·开发语言·笔记·spring·ai