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

Java中对泛型的理解

一、泛型是什么?

1. 定义:
泛型允许你在定义类、接口或方法时使用类型参数(Type Parameter)。在使用时(如声明变量、创建实例时),再用具体的类型实参(Type Argument) 替换这个参数。它就像是方法的形参和实参,但操作的对象是类型本身。

2. 核心目的:

  • 类型安全(Type Safety):在编译期就能检查类型是否正确,将运行时错误(ClassCastException)转变为编译期错误。

    // 没有泛型:编译通过,运行时报 ClassCastException
    List list = new ArrayList();
    list.add("Hello");
    Integer num = (Integer) list.get(0); // 运行时错误!// 有泛型:编译期直接报错,无法通过编译
    List<String> list = new ArrayList<>();
    list.add("Hello");
    Integer num = list.get(0); // 编译错误:不兼容的类型
  • 消除强制类型转换(Eliminate Casts):代码更简洁、清晰。

    // 没有泛型
    String str = (String) list.get(0);// 有泛型
    String str = list.get(0); // 自动知道是String,无需强转

二、泛型擦除(Type Erasure)—— 泛型的实现原理

这是 Java 泛型的核心机制,也是很多限制的根源。

1. 是什么?
Java 的泛型是在编译器层面实现的,而不是在运行时。在编译后,所有的泛型类型信息都会被移除(擦除)。编译器在生成字节码时:

  • 将泛型类型参数替换为它的边界(Bound)(如 T extends Number 则替换为 Number)。

  • 如果类型参数是无边界的(如 <T>),则替换为 Object

  • 随之插入必要的强制类型转换,以保持类型安全。

2. 例子:

// 源代码(编译前)
public class Box<T> {private T value;public void set(T value) { this.value = value; }public T get() { return value; }
}Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get(); // 无需强转// 编译后(概念上,字节码级别)
public class Box { // T 被擦除private Object value; // T 被替换为 Objectpublic void set(Object value) { this.value = value; }public Object get() { return value; } // 返回Object
}Box stringBox = new Box(); //  raw type
stringBox.set("Hello");
String value = (String) stringBox.get(); // 编译器插入了强转!

3. 带来的影响与限制:

  • 不能使用基本类型:如 List<int> 是错误的,必须用 List<Integer>。因为擦除后是 Object,而 Object 不能持有 int

  • instanceof 和 getClass():运行时无法检测泛型类型。

    List<String> list = new ArrayList<>();
    System.out.println(list instanceof List<String>); // 编译错误
    System.out.println(list instanceof List); // 正确,但只能检查到是List,不是List<String>
  • 不能创建泛型数组new T[] 或 new List<String>[] 都是错误的。因为数组需要在运行时知道其确切的元素类型来保证类型安全,而擦除破坏了这个机制。

  • 不能实例化类型参数new T() 是错误的,因为擦除后是 new Object(),这通常不是你想要的。


三、桥接方法(Bridge Method)—— 保护多态

桥接方法是编译器为了解决类型擦除多态冲突而自动生成的方法。

场景: 当一个类继承或实现了一个泛型类/接口,并重写了其中的泛型方法时。

例子:

// 泛型接口
public interface Comparable<T> {int compareTo(T other);
}// 实现类
public class String implements Comparable<String> {// 我们重写的方法签名:int compareTo(String other)@Overridepublic int compareTo(String other) { ... }
}

由于类型擦除,父接口 Comparable 中的方法在字节码层面变成了 int compareTo(Object other)。这导致子类 String 实际上有两个方法:

  1. int compareTo(String other) (我们自己写的)

  2. int compareTo(Object other) (编译器生成的桥接方法

桥接方法内部做了什么?

// 编译器生成的桥接方法(概念上)
public int compareTo(Object other) {// 在桥接方法中,进行类型检查和安全地向下转型return compareTo((String) other); // 调用我们重写的那个具体类型的方法
}

桥接方法确保了即使在类型擦除后,Java的多态机制(父类引用调用子类方法)也能正常工作,同时保证了类型安全。


四、泛型继承和通配符(Wildcards):extends & super

这是泛型中最难理解但最强大的部分,通常用 PECS(Producer-Extends, Consumer-Super) 原则来概括。

1. 泛型不变性(Invariance)
首先,理解这一点至关重要:Box<String> 和 Box<Object> 没有继承关系,即使 String 是 Object 的子类。

Box<Object> box = new Box<String>(); // 编译错误!不兼容的类型

这种特性称为不变性(Invariance)。它保证了类型安全。如果上面成立,你就可以 box.set(new Integer(100)),从而把一个 Integer 放进一个声明为 String 的盒子里。

2. 通配符 ?
为了解决需要泛型协变的需求,引入了通配符 ?

3. 上界通配符 ? extends T (Producer)

  • 含义:表示“未知的某种类型,但它是 T 或 T 的子类”。

  • 用途:当你主要从泛型结构中读取数据(Producer) 时使用。

  • 规则:你可以安全地从中读取(赋值给 T 或父类引用),但不能向其写入(除了 null)。因为编译器不知道具体是哪种子类,写入可能破坏类型安全。

    List<? extends Number> numbers = new ArrayList<Integer>(); // 协变,允许
    Number num = numbers.get(0); // OK, 可以读取为Number
    numbers.add(new Integer(100)); // 编译错误!不能写入

4. 下界通配符 ? super T (Consumer)

  • 含义:表示“未知的某种类型,但它是 T 或 T 的父类”。

  • 用途:当你主要向泛型结构中写入数据(Consumer) 时使用。

  • 规则:你可以安全地向其写入 T 或 T 的子类对象,但读取出来只能赋值给 Object 引用。因为编译器只知道容器里是 T 的父类,无法确定具体类型。

    List<? super Integer> list = new ArrayList<Number>(); // 逆变,允许
    list.add(new Integer(123)); // OK, 可以写入Integer及其子类
    Integer num = list.get(0); // 编译错误!无法安全读取
    Object obj = list.get(0); // OK, 只能读取为Object

5. PECS 原则总结

  • Producer-Extends (P-E):如果你需要一个提供(生产) T 对象的泛型结构(主要调用 get()),使用 <? extends T>。例如:Collection<? extends T>.get()

  • Consumer-Super (C-S):如果你需要一个接收(消费) T 对象的泛型结构(主要调用 add()),使用 <? super T>。例如:Collection<? super T>.add(T)

  • 既生产又消费:如果你既要读又要写,那就不要用通配符,直接用确切的类型,如 <T>

经典应用:Collections.copy()

public static <T> void copy(List<? super T> dest, List<? extends T> src) {// dest 是消费者 (Consumer),消费T对象,所以用 ? super T// src 是生产者 (Producer),生产T对象,所以用 ? extends Tfor (int i =0; i < src.size(); i++) {dest.set(i, src.get(i));}
}

五、常见问题总结

Q:“详细讲讲Java的泛型。”

A:

“Java泛型的核心目的是提供编译时类型安全消除强制类型转换。它的实现机制是类型擦除,即在编译后泛型信息会被移除,类型参数会被替换为它的边界或Object,并由编译器自动插入强制转换。

类型擦除带来了一些限制,比如不能使用基本类型、不能进行泛型的instanceof检查、不能创建泛型数组等。为了解决擦除与多态的冲突,编译器会生成桥接方法,它在子类重写泛型方法时,负责进行类型检查和安全转型,从而保证多态性。

泛型具有不变性Box<String> 不是 Box<Object> 的子类。为了更灵活的API设计,引入了通配符 ? 和 PECS原则

  • ? extends T 用于生产者,表示可以安全读取,但不能写入。

  • ? super T 用于消费者,表示可以安全写入,但读取受限。

http://www.dtcms.com/a/356866.html

相关文章:

  • mes表结构思维导图
  • 基于机器学习的多个模型的预测Backtrader自动化交易系统设计
  • Java设计模式是什么?核心设计原则有哪些?
  • 编程速递:RAD Studio 13 即将到来的功能
  • Linux应用软件编程--->数据库
  • C++函数继承
  • 【C++闯关笔记】STL:vector的学习与使用
  • 论文阅读:ICLR 2024 GAIA: A Benchmark for General AI Assistants
  • DBeaver中禁用PostgreSQL SSL的配置指南
  • SQL Server 查看备份计划
  • Creed —— 设置玩家属性(生命/耐力/经验值等)
  • 初学python的我开始Leetcode题-17
  • Azure Marketplace 和 Microsoft AppSource的区别
  • 订餐后台管理系统 -day03 登录模块
  • Linux操作系统Shell脚本-第一章
  • 数据防泄与最小可见:ABP 统一封装行级安全(RLS)+ 列级脱敏
  • 前端vue3入门学习
  • 数据分析编程第七步:分析与预测
  • 【MFC自动生成的文件详解:YoloClassMFC.cpp 的逐行解释、作用及是否能删除】
  • 科技赋能医疗:陪诊小程序系统开发,让就医不再孤单
  • cursor的setting設置換行
  • 舰用燃气机数字孪生:舰船动力智慧管控核心
  • 从0到1玩转 Google SEO
  • 循环高级(1)
  • Parasoft赋能测试:精准捕捉运行时缺陷
  • 深度学习入门Day10:深度强化学习原理与实战全解析
  • 彻底弄清URI、URL、URN的关系
  • 基于LangChain框架搭建AI问答系统(附源码)
  • 将2D基础模型(如SAM/SAM2)生成的2D语义掩码通过几何一致性约束映射到3D高斯点云
  • android 不同分辨图片放错对应文件夹会怎样?