一、泛型核心本质

泛型的本质,用一句话就能说懂:类型参数化。简单来讲,以前我们写代码,变量的类型是固定的(比如String、Integer),写一个存String的容器,就不能存Integer;而泛型相当于把“固定类型”改成了“可变参数”,定义类、方法时,不明确指定具体类型,只先用一个字母(比如T、E)代替,等我们实际用的时候(创建对象、调用方法),再告诉程序这个字母对应的具体类型。这里要注意:泛型不支持int、char这种基本类型,得用它们的包装类(Integer、Character)才行。

核心优势:

  1. 类型安全:编译期检查类型,避免运行时出现ClassCastException;

  2. 代码复用:一套代码适配多种引用类型,无需重复编写;

  3. 简化开发:无需手动进行强制类型转换,编译器自动完成。

前提:Java泛型是编译期特性,运行时JVM不感知泛型类型信息(结合下一篇类型擦除详细说明)。

二、泛型类(最基础的泛型使用)

1. 定义

类定义时,在类名后添加尖括号<>,里面填写类型参数(通常用单个大写字母表示,如T、E、K、V,无特殊含义,仅为标识),类内部可使用该类型参数定义属性、方法的参数和返回值。

2. 语法格式

// T:类型参数,代表任意引用类型(不能是基本类型)
public class GenericClass<T> {
    // 使用类型参数定义属性
    private T data;

    // 使用类型参数定义方法的参数和返回值
    public T getData() {
        return data;
    }

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

3. 使用示例

创建泛型类对象时,需在尖括号中指定具体的引用类型,编译器会根据指定的类型,进行编译期检查和类型适配。

public class GenericTest {
    public static void main(String[] args) {
        // 1. 指定T为String类型,只能存储String数据
        GenericClass<String> strObj = new GenericClass<>();
        strObj.setData("Java泛型基础");
        // 无需强制转换,直接返回String类型
        String strData = strObj.getData();
        System.out.println("String类型数据:" + strData);

        // 2. 指定T为Integer类型,只能存储Integer数据
        GenericClass<Integer> intObj = new GenericClass<>();
        intObj.setData(100);
        Integer intData = intObj.getData();
        System.out.println("Integer类型数据:" + intData);

        // 错误示例:不能存储与指定类型不一致的数据(编译报错)
        // strObj.setData(123);
    }
}

4. 注意事项

泛型类的类型参数,仅在类内部有效;创建对象时,必须指定具体类型。

三、泛型方法(独立于泛型类的泛型使用)

泛型方法与泛型类无关,普通类、泛型类中都可以定义泛型方法,核心是方法级别的类型参数化——类型参数仅作用于当前方法。

1. 定义与语法

泛型方法的关键标识:在返回值类型前添加尖括号<>,填写类型参数,这是区分泛型方法和普通方法的核心。

public class GenericMethodDemo {
    // 泛型方法:<T> 是类型参数声明,必须在返回值前
    public <T> T getFirstElement(T[] array) {
        // 处理逻辑:获取数组的第一个元素
        if (array == null || array.length == 0) {
            return null;
        }
        return array[0];
    }

    // 普通类中的泛型方法(重载示例)
    public <E> void printElement(E element) {
        System.out.println("元素内容:" + element);
    }
}

2. 使用示例

调用泛型方法时,编译器会自动根据传入的参数类型,推断出类型参数的具体值,无需手动指定(也可手动指定,不推荐)。

public class GenericMethodTest {
    public static void main(String[] args) {
        GenericMethodDemo demo = new GenericMethodDemo();

        // 1. 调用泛型方法getFirstElement,传入String数组,自动推断T为String
        String[] strArray = {"Java", "泛型", "方法"};
        String firstStr = demo.getFirstElement(strArray);
        System.out.println("String数组第一个元素:" + firstStr);

        // 2. 传入Integer数组,自动推断T为Integer
        Integer[] intArray = {1, 2, 3, 4};
        Integer firstInt = demo.getFirstElement(intArray);
        System.out.println("Integer数组第一个元素:" + firstInt);

        // 3. 调用printElement方法,传入不同类型参数
        demo.printElement("测试字符串");
        demo.printElement(12345);
        demo.printElement(3.14);
    }
}

3.区别(泛型方法 vs 泛型类)

  • 泛型类:类型参数作用于整个类,创建类对象时指定类型;

  • 泛型方法:类型参数仅作用于当前方法,调用方法时自动推断类型,无需依赖类的类型参数。

四、泛型约束(边界限定,限制类型参数范围)

默认情况下,泛型的类型参数可以是任意引用类型,但实际开发中,我们可能需要限制类型参数的范围(比如只能是某个类的子类、某个接口的实现类),这就是泛型约束(也叫边界限定)。

泛型约束分为上界约束下界约束,其中下界约束仅支持通配符,不支持泛型类/方法的类型参数。

1. 上界约束(extends关键字)

(1)定义

表示类型参数必须是“指定类的子类”或“指定接口的实现类”,语法:<T extends 边界类型>。

注意:如果边界是类+接口,类必须放在最前面,接口后续排列(可多个接口)。

(2)代码示例
// 示例1:上界为类(T必须是Number的子类,如Integer、Double)
public class NumberContainer<T extends Number> {
    private T number;

    public T getNumber() {
        return number;
    }

    public void setNumber(T number) {
        this.number = number;
    }
}

// 示例2:上界为类+接口(T必须继承Animal,且实现Runnable、Serializable接口)
class Animal {}
public class AnimalContainer<T extends Animal & Runnable & Serializable> {
    private T animal;
    // 方法省略...
}
(3)使用注意
// 合法:Integer是Number的子类
NumberContainer<Integer> intContainer = new NumberContainer<>();
intContainer.setNumber(100);

// 合法:Double是Number的子类
NumberContainer<Double> doubleContainer = new NumberContainer<>();
doubleContainer.setNumber(3.14);

// 错误:String不是Number的子类,编译报错
// NumberContainer<String> strContainer = new NumberContainer<>();

2. 下界约束(super关键字,仅支持通配符)

表示类型参数必须是“指定类的父类”或“指定接口的父接口”,语法:<? super 边界类型>(注意:这里用的是通配符?,不是普通类型参数T)。

具体用法将在“通配符”部分详细说明,此处先记住核心:下界约束仅用于通配符,目的是限制“写入”的数据类型。

五、通配符(?,解决泛型协变/逆变问题)

Java泛型默认不支持协变(比如:String[]是Object[]的子类,但List<String>不是List<Object>的子类),为了解决这个问题,引入了通配符?,表示“未知类型”。

通配符分为三种:无界通配符、上界通配符、下界通配符,核心作用是灵活适配不同的泛型类型,同时保证类型安全。

1. 无界通配符(?)

(1)定义

? 表示任意引用类型,相当于<? extends Object>,适用于“只关心是泛型类型,不关心具体类型”的场景(比如仅读取数据,不写入数据)。

(2)代码示例
// 无界通配符方法:仅读取数据,不写入
public void printData(GenericClass<?> container) {
    // 合法:读取数据,返回Object类型(因为未知具体类型)
    Object data = container.getData();
    System.out.println("数据内容:" + data);

    // 错误:无法写入数据(除了null),因为不知道具体类型,无法保证安全
    // container.setData("test");

    // 唯一合法的写入:null(null是所有引用类型的默认值)
    container.setData(null);
}

2. 上界通配符(? extends T)

(1)定义

表示未知类型是“T或T的子类”,适用于“消费数据”的场景(仅读取,不写入,除了null),核心是“只能取,不能存”。

(2)代码示例
// 上界通配符:未知类型是Number或其子类
public void printNumber(GenericClass<? extends Number> container) {
    // 合法:读取数据,返回Number类型(子类对象可赋值给父类引用)
    Number num = container.getData();
    System.out.println("数字值(转为double):" + num.doubleValue());

    // 错误:无法写入数据,不知道具体是Integer还是Double
    // container.setData(100); // 可能是Integer,也可能是Double,不安全
}

3. 下界通配符(? super T)

(1)定义

表示未知类型是“T或T的父类”,适用于“生产数据”的场景(仅写入T或T的子类,读取返回Object),核心是“只能存,不能取(除非强转)”。

(2)代码示例
// 下界通配符:未知类型是Integer或其父类(如Number、Object)
public void addInteger(GenericClass<? super Integer> container) {
    // 合法:写入Integer或其子类(假设存在MyInteger extends Integer)
    container.setData(100); // Integer类型,符合要求
    // container.setData(new MyInteger()); // 子类也合法

    // 读取:返回Object类型,需强转才能使用具体类型的方法
    Object data = container.getData();
    Integer num = (Integer) data; // 强转,有风险(需确保实际类型是Integer)
}

4. 通配符核心总结

通配符类型

语法

核心场景

读写规则

无界通配符

?

仅读取,不关心具体类型

可读(返回Object),可写null

上界通配符

? extends T

消费数据(仅读取)

可读(返回T),可写null

下界通配符

? super T

生产数据(仅写入)

可读(返回Object),可写T/子类

六、类型擦除(Java泛型的底层实现)

结合前文提到的所有知识点,这里重点补充类型擦除——Java泛型之所以存在诸多限制(如不能用基本类型、强转有风险),本质是因为类型擦除。

简单来说,类型擦除就是编译器在编译时,会移除所有泛型类型参数信息,将泛型代码转为普通非泛型代码(无边界泛型擦除为Object,有上界泛型擦除为对应上界类型),JVM运行时不感知泛型存在。无需深入展开(详细可参考上篇专门讲解类型擦除的博客),只需记住:类型擦除是Java泛型的底层特性,也是泛型相关限制的根源即可。

博客链接:

Java类型擦除详解:从原理到避坑,一文吃透-CSDN博客https://blog.csdn.net/2201_75940978/article/details/157218569?spm=1001.2014.3001.5501

结合前文提到的所有知识点,这里重点补充类型擦除——Java泛型之所以存在诸多限制(如不能用基本类型、强转有风险),本质是因为类型擦除。

类型擦除的关键影响(结合泛型知识点)

  • 基本类型不能作为泛型参数:擦除后是Object(引用类型),无法存储基本类型,需用包装类;

  • 运行时无法获取泛型类型:instanceof无法判断Box<String>和Box<Integer>,因为擦除后都是Box;

  • 泛型强转有风险:运行时无泛型信息,无法校验强转的正确性,容易抛出ClassCastException;

  • 不能创建泛型数组:new T[10]擦除后是new Object[10],强制转换有类型安全风险。

七、泛型核心知识点总结

1. 泛型本质:类型参数化,编译期类型检查,运行时类型擦除;

2. 泛型类:类级别的类型参数,创建对象时指定具体类型;

3. 泛型方法:方法级别的类型参数,独立于泛型类,调用时自动推断类型;

4. 泛型约束:上界(extends)限制类型为子类/实现类,下界(super)仅支持通配符;

5. 通配符:解决协变问题,无界(?)、上界(? extends T)、下界(? super T),牢记读写规则;

6. 泛型强转:谨慎使用,明确类型可强转,否则有运行时风险;

7. 类型擦除:底层实现,解释泛型的诸多限制,核心是编译期擦除类型参数。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐