Java的泛型

Java的泛型

泛型是什么

Java 泛型(Generic)是 JDK 5 引入的静态类型参数化机制,其核心是在类、接口、方法的定义阶段,声明抽象的类型参数(Type Parameter)以替代显式的具体引用类型;在泛型代码的使用阶段(实例化泛型类、调用泛型方法、实现泛型接口),将类型参数绑定为具体的引用类型,从而使同一套代码能安全适配多种数据类型,并通过编译期的类型合法性校验,保障静态类型安全的语言特性。

为什么要有泛型

首先我们先写这样一份代码来设计一个栈:

栈中数据均是int类型,若我想将此栈中的数据换成String类型的,代码需要这样写:

若需让栈支持不同数据类型,需为每种类型编写逻辑完全一致、仅元素类型不同的栈类实现。这种方式会导致大量重复代码,维护成本极高,若需修改栈的核心逻辑,需在所有类型的栈类中逐一修改,易出现遗漏或不一致问题。

有开发者可能想到:利用 Object 是所有引用类型的父类这一特性,将栈底层数组声明为 Object[],通过向上转型存储任意引用类型(基本类型需自动装箱为对应包装类),试图用一份代码适配所有类型,代码如下:

但该方案存在核心缺陷:

  • 业务规则违规:Java 数组的核心规则是元素类型与数组声明类型一致(Object[] 数组的元素类型为 Object,所有引用类型实例均可存入),因此语法上未违背数组规则,但从业务角度,栈作为同类型元素的有序集合,混入不同类型元素会导致逻辑混乱;
  • 类型安全缺失:取出元素时需手动强制类型转换,若转换类型与实际存储类型不符,会抛出 ClassCastException,且此异常仅运行时暴露,编译期无法拦截。

因此,Object 方案虽能减少代码冗余,但以牺牲类型安全为代价,并非可靠的实现方式。最好的解决办法是泛型,是因为泛型通过引入类型参数化解决了上述问题。

泛型的使用

泛型的核心是类型参数(Type Parameter),通常用单个大写字母表示:

T:Type(任意类型)

E:Element(集合元素)

K:Key(Map 的键)

V:Value(Map 的值)

N:Number(数字类型)

泛型类

定义类时声明类型参数,实例化时指定具体类型。

泛型类的定义需在类名后声明类型参数,格式为:

java 复制代码
// <T> 是类型参数声明
public class 类名<T> {
    // 类的属性可使用类型参数T
    private T 属性名;
    
    // 方法的参数/返回值可使用类型参数T
    public T 方法名(T 参数) {
        return 参数;
    }
}

需要注意:

静态成员不能使用类型参数,静态成员属于类,而类型参数是实例级的(不同实例的 T 可能不同);

泛型类的基本使用是实例化时绑定类型,泛型类不能直接用类型参数T实例化,必须在new对象时指定具体的引用类型,绑定后类中所有T都会被替换为该类型。

我们将上面的例子使用泛型类的类型绑定,代码如下:

  • 泛型类的类型绑定:限定栈的元素类型
    当实例化泛型类 Stack<E> 时,需显式绑定类型参数 E 为具体引用类型,此时泛型类中所有依赖E的部分,都会被编译期限定为绑定的具体类型;
  • 编译期类型检查:保证数组元素类型的一致性
    由于 Stack<String> 的 push 方法参数类型被限定为 String,若尝试向该栈传入非 String 类型的数据,编译期会直接抛出类型不匹配错误,这是泛型对数组需存储相同类型数据这一规则的编译期保障;
  • 类型安全的便捷性:获取元素无需显式强转
    Stack<String> 的 pop() 方法返回类型被编译期确定为 String,因此调用 stringStack.pop() 时,无需手动强制类型转换(编译器已基于绑定的String类型,在字节码中隐式插入安全的类型转换),既简化了代码又避免了运行时 ClassCastException 的风险。

通用容器泛型类 Box<T>

java 复制代码
// 泛型类定义:T为类型参数,代表容器存储的元素类型
public class Box<T> {
    private T content; // 属性类型为T

    // 方法参数类型为T
    public void setContent(T content) {
        this.content = content;
    }

    // 方法返回值类型为T
    public T getContent() {
        return content;
    }
}

泛型类的继承

泛型类可作为父类被继承,子类有两种处理方式

方式 1:子类指定具体类型(非泛型子类)

java 复制代码
// 子类继承泛型父类,并绑定T为String
public class StringBox extends Box<String> {
    @Override
    public void setContent(String content) {
        super.setContent(content);
    }
}

方式 2:子类保留类型参数(泛型子类)

java 复制代码
// 子类保留类型参数T,同时可添加新的类型参数U
public class PairBox<T, U> extends Box<T> {
    private U extraContent;

    public void setExtraContent(U extraContent) {
        this.extraContent = extraContent;
    }
}

泛型接口

定义接口时声明类型参数,实现接口时有两种方式:

方式 1:实现时指定具体类型

java 复制代码
// 泛型接口
interface Generator<T> {
    T generate();
}

// 实现时指定 T 为 String
class StringGenerator implements Generator<String> {
    @Override
    public String generate() {
        return "Random String";
    }
}

方式 2:实现时保留类型参数(泛型类)

java 复制代码
// 实现时保留 T,成为泛型类
class NumberGenerator<T extends Number> implements Generator<T> {
    private T number;

    public NumberGenerator(T number) {
        this.number = number;
    }

    @Override
    public T generate() {
        return number;
    }
}

// 使用
NumberGenerator<Integer> intGenerator = new NumberGenerator<>(123);
System.out.println(intGenerator.generate()); // 123

<T extends 类/接口> 是泛型的类型限定,限制 T 必须是指定类的子类(或自身),或实现指定接口

泛型方法

泛型方法的类型参数是方法级别的(与类是否为泛型类无关),定义时需在返回值前声明类型参数。

格式为:<T> 返回值类型 方法名(T 参数) { ... }

java 复制代码
public class GenericMethodDemo {
    // 泛型方法
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        String[] strArray = {"A", "B", "C"};
        Integer[] intArray = {1, 2, 3};

        printArray(strArray); // A B C
        printArray(intArray); // 1 2 3
    }
}

泛型的类型擦除

泛型类中初始化泛型数组时需显式强制类型转换(如 arr = (E[]) new Object[size];),但普通泛型对象(如泛型类的变量、方法参数)的类型转换由编译器隐式完成,无需开发者手动处理。核心原因在于数组的可具体化类型特性,以及泛型类型擦除机制对两类场景的差异化处理:

一、普通泛型对象(非数组):编译器隐式插入强转

以泛型类 Box<T> 为例:

java 复制代码
public class Box<T> {
    private T content;
    public T getContent() { return content; }
}
java 复制代码
public class Test {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        String content = stringBox.getContent();
    }
}

编译期类型擦除会将 Box<T> 中的无界类型参数 T 替换为其上界Object,因此 Box 类编译后,getContent() 方法的擦除返回类型为 Object。但编译器会在 stringBox.getContent() 的调用点,自动生成 (String) stringBox.getContent() 的字节码,从而避免开发者手动强转。

运行时,Box<String> 会退化为原始类型Box(原始类型并非 Box<Object>Box<Object> 是显式指定类型参数为 Object 的泛型实例,仍受编译期类型校验,而原始类型Box完全丢失泛型信息),编译器正是通过在调用点插入隐式强转,保证了源码层面的类型安全。若直接使用原始类型 Box(不指定泛型),getContent() 会直接返回 Object,需开发者手动强转(String content = (String) rawBox.getContent()),这也反向印证了隐式强转的价值。

二、泛型数组:需显式强转,且编译器禁止直接创建

Java 编译期禁止直接创建泛型数组(如 E[] arr = new E[size];),核心原因如下:

  • 数组是可具体化类型(运行时保留完整的类型信息),JVM 会在运行时检查数组元素类型,若尝试向数组存入与数组声明类型不符的元素,会抛出 ArrayStoreException;
  • 而泛型的类型擦除是编译期行为(运行时无 E 的具体类型信息),编译器无法在编译期保证泛型数组的类型安全,因此直接创建泛型数组的写法会被编译期拒绝。

基于上述原因,开发者通常先创建 Object [] 数组(Object 是所有引用类型的父类),再显式强转为 E [](如 arr = (E[]) new Object[size];)。需注意:该强转为未检查转换(Unchecked Conversion),编译器会报 "Unchecked cast" 警告(因无法在编译期验证 Object [] 强转为 E [] 的类型安全);运行时若数组中存储的元素类型与 E 不符,会抛出 ArrayStoreException,这也是 Java 官方更推荐使用 List<E> 替代泛型数组的核心原因。

相关推荐
沐知全栈开发2 小时前
PostgreSQL 删除数据库指南
开发语言
FPGAI2 小时前
Java学习之计算机存储规则、数据类型、标识符、键盘录入、IDEA
java·学习
!停2 小时前
c语言动态申请内存
c语言·开发语言·数据结构
AC赳赳老秦2 小时前
pbootcms模板后台版权如何修改
java·开发语言·spring boot·postgresql·测试用例·pbootcms·建站
止水编程 water_proof2 小时前
SpringBoot快速上手
java·spring boot·后端
皮卡丘学了没2 小时前
Java基础-HashMap扩容机制(Java8源码)
java·哈希算法·散列表
li.wz2 小时前
ShardingSphere 与 PolarDB-X 选型对比
java·后端·微服务
wanghowie2 小时前
02.02.02 CompletableFuture 组合与异常处理:构建复杂异步流
java·future·并发编程
代码or搬砖2 小时前
Collections和Arrays
java·开发语言