当前位置: 首页 > ai >正文

【Java】泛型

文章目录

  • 【Java】泛型
    • 泛型概述
      • 什么是泛型?
      • 为什么需要泛型?
    • 泛型的使用
      • 泛型类
      • 泛型方法
      • 泛型接口
    • 泛型通配符
      • 什么是泛型通配符?
      • `<?>` —— 通配任何类型
      • `<? extends T>` —— 上界通配符(子类)
      • `<? super T>` —— 下界通配符(父类)
      • T 和 ? 的区别
        • 协变和逆变
          • 为什么数组支持协变而泛型集合不支持?
    • 类型擦除
      • 什么是类型擦除?
      • 核心概念
      • 类型擦除的局限性
        • 无法创建泛型类型数组
        • 无法使用 `instanceof` 检查泛型类型
        • 无法创建类型参数的实例
        • 无法创建类型参数的数组
        • 静态上下文(静态变量、静态方法)不能引用类型参数
        • 方法重载冲突
        • 异常处理
      • 类型擦除的必要性:兼容性
      • 总结

【Java】泛型

泛型概述

什么是泛型?

泛型,也就是 “参数化类型”

在编程中,我们常常希望编写通用的代码,比如容器类(如 ArrayList)可以存放任意类型的对象,而不需要为每种类型分别写一个类。泛型就提供了一种方式,让你在定义类、接口或方法时将“类型”也当作参数传入

为什么需要泛型?

想象一下,在泛型出现前(Java 5之前),我们操作集合时常这样写:

List list = new ArrayList();
list.add("Hello");
list.add(100); // 不小心混入整数
String str = (String) list.get(1); // 运行时抛出ClassCastException!

问题显而易见:

  1. 类型不安全:任何类型都能放入集合,错误在运行时才暴露。
  2. 繁琐的强制类型转换:每次取出元素都需要显式转换。
  3. 代码可读性差:无法直接看出集合存储的类型。

泛型正是为解决这些问题而生!

泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。

泛型类

类型参数用于类的定义中,则该类被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map等。

基本语法

class 类名称 <泛型标识> {...
}
  • 尖括号 <> 中的 泛型标识被称作是类型参数,用于指代任何数据类型。

  • 泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下:

    T	Type 类型(代表一般的任何类)
    E	Element 元素(常用于集合)
    K	KeyV	Value?	通配符
    

示例

public class Box<T> {private T content;public void setContent(T content) {this.content = content;}public T getContent() {return content;}
}Box<String> stringBox = new Box<>();
stringBox.setContent("Java");
String value = stringBox.getContent();

泛型方法

当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数

基本语法

public <类型参数> 返回类型 方法名(类型参数 变量名) {...
}
  • 只有在方法签名中声明了< T >的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法。
  • 方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。

示例

public <T> void printArray(T[] array) {for (T element : array) {System.out.println(element);}
}// 调用时编译器自动推断类型
printArray(new String[]{"A", "B"});

泛型接口

泛型接口和泛型类的定义差不多,基本语法如下:

public interface 接口名<类型参数> {...
}
  • 泛型接口中的类型参数,在该接口被继承或者被实现时确定。

示例

public interface Repository<T> {T findById(int id);void save(T entity);
}public class UserRepository implements Repository<E> {// 实现中T被替换为E
}

泛型通配符

什么是泛型通配符?

在现实编码中,确实有这样的需求,希望泛型能够处理某一类型范围内的类型参数,比如某个泛型类和它的子类,为此 Java 引入了泛型通配符这个概念。

泛型通配符有三种形式:

  1. <?>:表示未知类型(无界通配符)
  2. <? extends T>:表示某个 T 的子类型(上界通配符)
  3. <? super T>:表示某个 T 的父类型(下界通配符)

<?> —— 通配任何类型

<?> 表示未知类型,可以接收任何类型的泛型实例。

示例代码

public void printList(List<?> list) {for (Object obj : list) {System.out.println(obj);}
}

使用场景

只读取集合中的数据,不进行写入(除了添加 null)。

注意

  • 不能添加除 null 以外的任何元素。
  • 通常用于只读操作。

<? extends T> —— 上界通配符(子类)

<? extends T> 表示某种类型是 TT 的子类。

示例代码

public void printNumbers(List<? extends Number> list) {for (Number num : list) {System.out.println(num);}
}

可以传入 List<Integer>List<Double>List<Number>

使用场景

  • 从集合中读取元素,适用于生产者 Producer
  • 不能添加任何非 null 值。

你不知道传入的是 List<Integer> 还是 List<Double>,如果你尝试加入 DoubleList<Integer> 中会类型错误。

“extends 只能读,不能写”

<? super T> —— 下界通配符(父类)

<? super T> 表示某种类型是 TT 的父类。

示例代码

public void addNumbers(List<? super Integer> list) {list.add(1);list.add(2);
}

可以传入 List<Integer>List<Number>List<Object>

使用场景

  • 向集合中添加元素,适用于消费者 Consumer
  • 取出元素时只能当作 Object 类型。

“super 只能写,不能读(读出来是 Object)”

T 和 ? 的区别

两者的核心角色不同:

  • T 是类型参数:在声明泛型类、接口或方法时使用,代表一个具体的类型占位符(如 class Box<T>),可在代码中作为实际类型操作变量。
  • ? 是通配符:在使用泛型时(如方法参数)表示未知类型(如 List<?>),主要用于放宽类型限制,但无法直接引用该类型。

具体区别有三点:

  1. 可操作性
    • T 可定义变量、返回值、参数(T item;
    • ? 仅用于类型约束,不能声明变量? item; ❌)
  2. 类型关联性
    • T 可关联多个位置(如 <T> void add(T a, List<T> b) 要求a和b同类型)
    • ? 每个通配符独立(List<?> a, List<?> b 可接受不同类型集合)
  3. 写入能力
    • T 允许写入对象(list.add(item) ✅)
    • ? 禁止写入(除 null 外),如 List<?> list.add("A") ❌”

T 的定位:精确控制类型,实现类型安全的操作(如 Collections.sort(List<T>))。

? 的定位:解决泛型的协变/逆变问题(通过 ? extends/? super),遵循 PECS 原则
例如 Collections.copy(List<? super T> dest, List<? extends T> src) 中:

  • srcextends 保证安全读取
  • destsuper 保证安全写入

代码示例

// T 的典型用例:类型安全的容器
public class Container<T> {private T value;public T getValue() { return value; } // T作为返回类型
}// ? 的典型用例:灵活处理未知集合
public void printSize(List<?> list) {System.out.println(list.size()); // 不依赖具体类型
}

补充

  • T 只能定义上界(<T extends Number>),而 ? 支持上下界(<? extends Number><? super Integer>),这是实现API灵活性的关键。
  • 编译后两者都会被擦除,但 T 擦除到边界类型(如 Number),? extends T 擦除到 T,而 ? 直接擦除到 Object
协变和逆变

协变和逆变是泛型中处理类型继承关系的两种方式:

  • 协变<? extends T> 实现:允许将子类型的集合视为父类型的集合(例如 List<Integer> 当作 List<? extends Number>),但只能读不能写
  • 逆变<? super T> 实现:允许将父类型的集合视为子类型的集合(例如 List<Number> 当作 List<? super Integer>),但只能写不能安全读
  1. 数据流向
    • 协变支持数据产出(如从集合读取元素)
    • 逆变支持数据消费(如向集合添加元素)
  2. 类型安全
    • 协变读取时编译器保证返回 T 类型
    • 逆变写入时编译器检查元素必须是 T 的子类
  3. 设计原则
    二者共同遵循 PECSProducer(生产者)使用 extendsConsumer(消费者)使用 super),例如:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {// src 是生产者(协变) → 读取安全// dest 是消费者(逆变) → 写入安全for (int i = 0; i < src.size(); i++) {dest.set(i, src.get(i)); // 安全读 + 安全写}
}
  • 协变子类型替换的延伸(里氏替换原则),但 Java 泛型默认是不变的,因此需显式声明 extends 实现协变。
  • 逆变父类型能力覆盖的体现(如回调函数 Consumer<? super T> 可处理所有 T 的子类)。
  • 类型擦除后,JVM 无法在运行时检查泛型类型,因此编译器需在编译期通过规则保证安全。
为什么数组支持协变而泛型集合不支持?
// 数组协变(运行时可能失败)
Number[] nums = new Integer[10];
nums[0] = 1.5; // 运行时抛出 ArrayStoreException// 泛型集合(编译器阻止错误)
List<Number> numList = new ArrayList<Integer>(); // 编译错误!

数组在运行时保留类型信息,泛型通过编译器类型擦除实现。Java 用 ? extends 提供安全的编译期协变,避免运行时错误。

类型擦除

什么是类型擦除?

Java 的类型擦除是泛型实现的核心机制。它的主要目的是在编译时提供更强的类型检查,同时保持与旧版本Java字节码的兼容性(主要是Java 5之前的非泛型代码)

核心概念

  1. 编译时检查,运行时擦除:
    • 编译时: 编译器会严格检查泛型类型的使用是否安全(例如,确保你向 List<String> 里添加的都是 String 对象)。
    • 运行时: JVM 在运行时并不知道泛型类型参数的具体信息。编译器在生成字节码时,会将泛型类型参数移除或替换掉,这个过程就是“擦除”。
  2. 擦除规则:
    • 无界类型参数 (<T>): 被擦除为 Object
      • class Box<T> { T item; } -> 擦除后 class Box { Object item; }
    • 有界类型参数 (<T extends SomeClass & SomeInterface>): 被擦除为边界中的第一个类型(类或接口)。
      • class Box<T extends Number & Comparable> { T item; } -> 擦除后 class Box { Number item; } (使用第一个边界 Number)
    • 泛型方法中的类型参数: 同样遵循上述规则进行擦除。
    • 类型转换: 编译器在必要的地方插入类型转换指令(称为 checkcast),以保证类型安全。这些转换在字节码中是显式的。
      • 当你从 List<String> list 中获取元素 String s = list.get(0); 时,编译器生成的字节码相当于 String s = (String) list.get(0);
    • 桥接方法: 为了保证多态性在类型擦除后仍然正确工作,编译器会在子类中生成合成的“桥接方法”。这是类型擦除中最复杂且对开发者透明的一部分。

类型擦除的局限性

无法创建泛型类型数组
// 编译错误!
List<String>[] arrayOfLists = new List<String>[10];

原因: 数组在创建时需要在运行时确切知道其元素类型(new List<String>[10] 需要知道 List<String> 类型)。但类型擦除后,List<String>List<Integer> 在运行时都是 List(原始类型),JVM 无法区分它们。如果允许创建,可能会导致将错误的类型存入数组(破坏类型安全)。

无法使用 instanceof 检查泛型类型
List<String> list = new ArrayList<>();
// 编译错误!
if (list instanceof List<String>) { ... }
// 只能检查原始类型
if (list instanceof List) { ... } // 可以,但意义不大

原因: instanceof 是一个运行时操作符,需要检查对象的实际类型信息。但类型擦除后,List<String>List<Integer> 在运行时都是 List,无法区分。

无法创建类型参数的实例
class Box<T> {T createInstance() {// 编译错误!return new T();}
}

原因: new T() 需要在运行时知道 T 的具体类型来调用其构造函数。但类型擦除后,T 被替换为 Object 或边界类型,编译器无法确定调用哪个构造函数,甚至 T 是否有无参构造函数都不确定。

无法创建类型参数的数组
class Box<T> {// 编译错误!T[] createArray(int size) {return new T[size];}
}

原因: 与创建泛型类型数组类似。数组创建 new T[size] 需要在运行时知道 T 的具体类型,但类型擦除后无法得知。

静态上下文(静态变量、静态方法)不能引用类型参数
class Box<T> {// 编译错误!每个Box实例的T可能不同private static T staticField;// 编译错误!public static void staticMethod(T param) { ... }
}

原因: 静态成员属于类本身,而不是类的某个特定实例。类型参数 T 是在创建类的实例时才被具体化的(即使是在编译时具体化)。在静态上下文中,没有具体的 T 类型信息可用。擦除后,静态字段或方法的签名中根本不会有 T 的信息。

方法重载冲突
// 编译错误!方法签名冲突 (Erasure of method print(List<String>) is the same as print(List<Integer>))
public void print(List<String> list) { ... }
public void print(List<Integer> list) { ... }

原因: 类型擦除后,两个方法的签名都变成了 print(List list),它们在字节码层面是完全相同的,违反了方法重载要求签名不同的规则。

异常处理
  • 不能在 catch 子句中使用类型参数(catch (T e) 是非法的)。
  • 在方法声明中,throws T 是允许的,但受限于类型擦除和异常处理的规则。

类型擦除的必要性:兼容性

Java 在 1.5 版本才引入泛型。类型擦除的设计是向后兼容的关键:

  1. 新代码使用泛型: 新编写的泛型代码(如 List<String>)在编译后,其字节码与非泛型的旧代码(如 List)在表示上是一致的(原始类型 List + Object 转换)。
  2. 旧代码使用泛型集合: 老版本(Java 1.4 及之前)编译的字节码可以直接在支持泛型的 JVM (Java 5+) 上运行。这些旧代码使用的是原始类型(如 List),而 JVM 看到的正是擦除后的原始类型。
  3. 新代码使用旧库: 使用泛型的新代码可以无缝调用和使用那些没有泛型的旧库(传递 List 给期望 List<String> 的方法,编译器会警告但允许,需要 @SuppressWarnings("unchecked"))。

如果没有类型擦除,JVM 就需要修改以支持新的泛型类型表示,这将破坏“一次编写,到处运行”的承诺,导致旧版本 JVM 无法运行新编译的泛型代码。

总结

  • 类型擦除是Java泛型实现的基石,核心是编译时检查类型安全,运行时移除类型参数信息。
  • 擦除规则:无界->Object,有界->第一个边界类型,编译器插入强制转换。
  • 主要限制:无法创建泛型数组/实例/参数数组,无法instanceof检查泛型,静态成员不能使用类型参数,特定方法重载冲突,异常处理限制。
  • 根本原因:确保Java 5+的泛型代码与Java 5之前的非泛型代码之间的二进制兼容性。
http://www.xdnf.cn/news/10549.html

相关文章:

  • 线性代数复习
  • Bootstrap 5学习教程,从入门到精通,Bootstrap 5 安装及使用(2)
  • CNN卷积网络:让计算机拥有“火眼金睛“(superior哥AI系列第4期)
  • Linux——计算机网络基础
  • 分布式锁剖析
  • 微软markitdown PDF/WORD/HTML文档转Markdown格式软件整合包下载
  • React Hooks 与异步数据管理
  • YARN应用日志查看
  • demo_win10配置WSL、DockerDesktop环境,本地部署Dify,ngrok公网测试
  • CppCon 2014 学习:0xBADC0DE
  • FFmpeg移植教程(linux平台)
  • NTP库详解
  • AI矢量软件|Illustrator 2025网盘下载与安装教程指南
  • Java从入门到精通 - 常用API(一)
  • 【Hot 100】70. 爬楼梯
  • 【农资进销存专用软件】佳易王农资进出库管理系统:赋能农业企业高效数字化管理
  • 94. Java 数字和字符串 - 按索引获取字符和子字符串
  • java28
  • 随记 nacos + openfegin 的远程调用找不到服务
  • 【CVE-2025-4123】Grafana完整分析SSRF和从xss到帐户接管
  • 深入探讨redis:缓存
  • AI入门——AI大模型、深度学习、机器学习总结
  • CentOS8.3+Kubernetes1.32.5+Docker28.2.2高可用集群二进制部署
  • 如何把电脑桌面设置在D盘?
  • JDK21深度解密 Day 11:云原生环境中的JDK21应用
  • 【Delphi】实现在多显示器时指定程序运行在某个显示器上
  • 力扣HOT100之动态规划:32. 最长有效括号
  • HTML 等价字符引用:系统化记忆指南
  • Fragment懒加载优化方案总结
  • DAY 43 复习日