Java中常见错误-泛型擦除及桥接方法问题及解决方案
Java中泛型擦除及桥接方法
- 泛型擦除
- 无界擦除
- 上界擦除
- 下界擦除
- 桥接方法
- 演示案例
- wrong1
- wrong2
- wrong3
- right
- 原理总结
泛型擦除
泛型擦除是Java泛型机制的一个特性,它意味着**在编译期间,所有的泛型信息都会被移除,而在运行时,所有泛型类型都转换为其边界类型(通常是Object,或者对于参数化的类型参数,是其指定的边界)。这是因为Java的泛型是类型安全的,但不是类型保留的**。换句话说,泛型只在编译时提供类型检查,而运行时并不保留这些类型信息。泛型擦除可以分为以下三类:
无界擦除
无界擦除是指没有指定泛型类型的边界,通常使用 ? 作为通配符。这意味着方法或变量可以接受任何类型的对象。例如,想象你有一个动物观察站,它记录任何动物的信息,但不关心它们的类型。
public class ObservationStation {public void recordAnyAnimal(Animal<?> animal) {// 不关心动物的类型,只是记录System.out.println(animal);}
}@Data
@NoArgsConstructor
public class Animal<T> {private T attribute; // 使用泛型T来表示动物的一个属性,比如名字、年龄等public Animal(T attribute) {this.attribute = attribute;}
}public class unBoundMain {public static void main(String[] args) {ObservationStation station = new ObservationStation();Animal<String> animal1 = new Animal<>("Dog");Animal<Integer> animal2 = new Animal<>(1);station.recordAnyAnimal(animal1); // 无界擦除,因为可以接受任何Animal类型station.recordAnyAnimal(animal2); // 同理}
}
在运行时,recordAnyAnimal 方法的参数类型被擦除为 Animal,这意味着它接受任何 Animal 类型的对象,而不管具体的 T 是什么类型。编译器在调用方法时会确保传入的参数是 Animal 的实例。
运行结果:
上界擦除
上界擦除指的是在使用泛型时,将泛型参数限制
为某种类型的子类型
或该类型本身
。
@Data
@NoArgsConstructor
public class Animal<T extends LivingBeing> { // 设置上界,T必须是LivingBeing或其子类private T attribute; // 使用泛型T来表示动物的一个属性,比如名字、年龄等public Animal(T attribute) {this.attribute = attribute;}
}public class LivingBeing {private Age age; // 假设Age是一个枚举,表示生物的年龄阶段public enum Age {PUPPY,ADULT,SENIOR}// 其他属性和方法
}@Data
public class Dog extends LivingBeing{private Age age;
}public class ObservationStation {public void recordAnyAnimal(Animal<? extends LivingBeing> animal) { // 上界擦除,接受任何LivingBeing或其子类的Animal// 不关心动物的类型,只是记录System.out.println(animal);}
}public class upBoundMain {public static void main(String[] args) {ObservationStation station = new ObservationStation();Dog dog = new Dog(); // 创建Dog对象dog.setAge(LivingBeing.Age.ADULT); // 设置Dog的年龄Animal<Dog> animal1 = new Animal<>(dog); // 将Dog放入Animal<Dog>station.recordAnyAnimal(animal1); // 正确,Dog是LivingBeing的子类}
}
在这个案例中,我引入了一个假设的LivingBeing基类,并修改了Animal类的泛型声明Animal<T extends LivingBeing>
以及ObservationStation类的recordAnyAnimal方法签名private T attribute;Animal(T attribute)
,以确保只有LivingBeing或其子类的Animal实例可以被记录。这样就实现了上界擦除效果
运行结果:
下界擦除
下界通配符允许你指定一个类型参数的下界,意味着可以接受该类型
及其父类型
。类型擦除的确会让泛型参数具体类型信息消失,但不影响使用下界或上界通配符来表达类型约束
尽管类型擦除导致泛型类型参数的具体类型信息在运行时不可用,但Java依然支持通过下界通配符(如? super T)来限定泛型参数,确保能够接受类型T及其父类型。这并不意味着下界概念在擦除过程中失效,而是指定了类型使用的灵活性在编译时得到保障,即使实际类型信息在运行时不可获取。
public class LivingBeing {
}public class Dog extends LivingBeing {// Dog类的其他属性和方法
}@Data
@NoArgsConstructor
public class Animal<T> {private T attribute;public Animal(T attribute) {this.attribute = attribute;}
}public class ObservationStation {public <T extends LivingBeing> void recordSpecificAnimal(Animal<? super T> animal, T specificType) {// 这里可以使用specificType来访问animal的attribute,因为animal能接收specificType或其父类型System.out.println("Recording " + specificType + " with attribute: " + animal.getAttribute());}public class lowBoundMain {public static void main(String[] args) {ObservationStation station = new ObservationStation();Dog dog = new Dog();Animal<Dog> animal1 = new Animal<>(dog);station.recordSpecificAnimal(animal1, dog); // 正确,Dog是Anima}
}
Dog类扩展了LivingBeing,这样它就可以用于Animal实例。在ObservationStation类的recordSpecificAnimal方法中,Dog实例可以作为T传入,因为Dog是LivingBeing的子类。Animal可以作为Animal<? super T>类型的参数传入,其中T是LivingBeing或其子类,因为Dog是LivingBeing的子类,所以Animal满足Animal<? super T>的约束。
运行结果:
public <T extends LivingBeing> void recordSpecificAnimal(Animal<? super T> animal, T specificType)
对 此处 <T extends LivingBeing>
的理解
是Java泛型中的一个类型参数声明,它定义了一个名为 T 的类型变量,这个变量必须是 LivingBeing 类或其子类的类型。这里的 extends 关键字用来指定类型参数的边界,表明 T 只能是 LivingBeing 或者 LivingBeing 的任何子类
对 此处 ? super T
的理解
? super T 是下界通配符,super 关键字在这里表示下界,T 是一个类型参数,代表一个未知的具体类型。
当你看到 ? super T,你可以理解为“任何类型,只要它是 T 或 T 的超类型”。这意味着 animal 变量可以接收任何 Animal 实例,只要这个实例的类型是 T 或者是 T 的父类。例如,如果 T 被实例化为 Dog,那么 animal 可以是 Animal,Animal,或者其他任何 Dog 的父类的 Animal 实例。
桥接方法
桥接方法(Bridge Method)是Java编译器在泛型类型擦除过程中自动生成的一种特殊方法,旨在解决泛型类型擦除后可能导致的多态性问题。具体来说,当一个类继承了泛型类或者实现了泛型接口,并且重写了其中的方法时,如果子类或实现类的方法签名中的类型参数不同于父类或接口中的类型参数,编译器就会自动插入一个桥接方法来确保类型兼容性和多态性的正确实现。
桥接方法的特点如下:
- 透明性:对于开发者来说,桥接方法是不可见的,它在源代码中不会显示,仅存在于编译后的字节码中。
- 自动生成:桥接方法由Java编译器自动生成,无需程序员手动编写。
- 方法签名:桥接方法的签名与原始方法相同,但其返回类型或参数类型是原泛型类型擦除后的类型(通常是Object或其父类),而实际执行逻辑则委托给擦除类型参数后的具体实现方法。
访问标志:桥接方法在字节码层面会被标记为 ACC_BRIDGE
和 ACC_SYNTHETIC
,表示这是一个桥接方法和编译器生成的方法。
用途:主要目的是为了确保在泛型类型擦除后,能够正确地调用到子类中重写的泛型方法,维持多态行为。
演示案例
公共部分
public class Pig1 extends Animal {public void setValue(String value) {System.out.println("Pig1.setValue called");super.setValue(value);}
}public class Pig2 extends Animal<String> {@Overridepublic void setValue(String value) {System.out.println("Pig2.setValue called");super.setValue(value);}
}public class Animal<T> {AtomicInteger updateCount = new AtomicInteger();private T value;@Overridepublic String toString() {return String.format("value: %s updateCount: %d", value, updateCount.get());}public void setValue(T value) {System.out.println("Animal.setValue called");this.value = value;updateCount.incrementAndGet();}
}
wrong1
public static void main(String[] args) {wrong1();}public static void wrong1() {Pig1 pig1 = new Pig1();Arrays.stream(pig1.getClass().getMethods()).filter(method -> method.getName().equals("setValue")).forEach(method -> {try {System.out.println(method);method.invoke(pig1, "test");} catch (Exception e) {e.printStackTrace();}});System.out.println(pig1.toString());}
运行结果:
执行过程:
wrong1方法中,对于Pig1对象,通过反射获取所有公共方法(包括继承的方法)中名为setValue的方法。由于Pig1重写了父类Animal的setValue方法,但没有改变参数类型,所以这里会看到两个方法:一个是直接从Pig1类中声明的方法,另一个是从父类继承而来的方法(因为泛型擦除后,父类的setValue(T value)变为setValue(Object value),与子类重写的方法签名相同)。这里Pig1重写并不是我们规范化的重写
输出解释:
首先调用了子类Pig1的setValue(String value),打印出"Pig1.setValue called",然后由于继承关系和方法重写规则,继续调用了父类的Animal.setValue(Object value),这一步实际上也是对Pig1中重写方法的调用,因此再次打印"Animal.setValue called"。最后,updateCount被增加了2次,输出为"value: test updateCount: 2"。
wrong2
public static void main(String[] args) {wrong2();}public static void wrong2() {Pig1 pig1 = new Pig1();Arrays.stream(pig1.getClass().getDeclaredMethods()).filter(method -> method.getName().equals("setValue")).forEach(method -> {try {System.out.println(method);method.invoke(pig1, "test");} catch (Exception e) {e.printStackTrace();}});System.out.println(pig1.toString());}
运行结果:
wrong2的结果及解释
执行过程:
与wrong1不同的是,这里仅获取Pig1声明的自身方法(不包括继承的),因此只找到了一个setValue(String value)方法。
输出解释:
仅调用了一次Pig1.setValue,内部虽然调用了super.setValue(value),但因为是在同一个方法体内,updateCount只增加了一次,所以输出为"value: test updateCount: 1"。
wrong3
public static void main(String[] args) {wrong3();}public static void wrong3() {Pig2 pig2 = new Pig2();Arrays.stream(pig2.getClass().getDeclaredMethods()).filter(method -> method.getName().equals("setValue")).forEach(method -> {try {System.out.println(method);method.invoke(pig2, "test");} catch (Exception e) {e.printStackTrace();}});System.out.println(pig2.toString());}
运行结果:
执行过程:
对于Pig2,它继承自具有泛型参数的Animal,且重写了setValue方法。在反射调用时,因为泛型擦除,父类会产生一个桥接方法setValue(Object)。wrong3试图调用所有声明的方法(包括桥接方法),所以setValue(String)和桥接方法setValue(Object)都被调用了。
输出解释:
首先调用子类Pig2的setValue(String value),打印"Pig2.setValue called",然后因为泛型擦除和桥接方法,又调用了父类的Animal.setValue(Object)(这里也是调用的子类重写的版本),再次打印"Animal.setValue called"。最终updateCount被增加了2次,输出为"value: test updateCount: 2"。
right
public static void main(String[] args) {right();}public static void right() {Pig2 pig2 = new Pig2();Arrays.stream(pig2.getClass().getDeclaredMethods()).filter(method -> method.getName().equals("setValue") && !method.isBridge()).findFirst().ifPresent(method -> {try {System.out.println(method);method.invoke(pig2, "test");} catch (Exception e) {e.printStackTrace();}});System.out.println(pig2.toString());}
运行结果:
执行过程:
此方法正确地处理了泛型导致的桥接方法问题。它通过检查方法是否为桥接方法(!method.isBridge()
),只调用了实际需要的setValue(String)方法,忽略了桥接方法。
输出解释:
只调用了Pig2.setValue(String value)一次,内部调用super.setValue(value)也是针对同一个具体类型的方法,因此updateCount只增加了一次,输出为"value: test updateCount: 1"。
原理总结
类型擦除
:Java泛型在编译后会被擦除,这意味着像Animal< String >在字节码层面等同于Animal< Object >,导致方法签名可能变得不明确。
桥接方法
:为了保证泛型擦除后的多态性,编译器会为某些泛型方法生成桥接方法,这些方法通常指向具体的实现。
反射调用
:直接通过反射调用可能遇到桥接方法,导致意外的行为,如多次调用或不期望的类型转换。正确处理泛型和桥接方法对于避免这些问题至关重要。
本次 Java中常见错误-泛型擦除及桥接方法问题及解决方案 文章到此结束,创作不易,望我佬们三连支持一下
相关文章:

Java中常见错误-泛型擦除及桥接方法问题及解决方案
Java中泛型擦除及桥接方法 泛型擦除无界擦除上界擦除下界擦除 桥接方法演示案例wrong1wrong2wrong3right 原理总结 泛型擦除 泛型擦除是Java泛型机制的一个特性,它意味着**在编译期间,所有的泛型信息都会被移除,而在运行时,所…...
Linux 程序守护脚本
引言 程序是由代码形成的,代码是由人写的。只要是人,都会有疏忽的时候,导致写出的程序有bug,当然最严重的bug就是程序闪退。 本文旨在提供一个程序守护脚本,当监测到程序闪退后,立马将程序再起启动&#…...

跨境电商|Facebook Marketplace怎么做?
2016 年,Facebook打造了同名平台 Facebook Marketplace。通过利用 Facebook 现有的庞大客户群,该平台取得了立竿见影的成功,每月访问量将超过 10 亿。对于个人卖家和小企业来说,Facebook Marketplace是一个不错的销货渠道…...

.gitignore 文件
一.什么是 .gitignore 文件 在任何当前工作的 Git 仓库中,每个文件都是这样的: 追踪的(tracked)- 这些是 Git 所知道的所有文件或目录。这些是新添加(用 git add 添加)和提交(用 git commit 提…...

qt中实现多语言功能
qt中实现多语言功能 原理: 其本质就是生成ts文件,然后使用Linguist软件手工翻译,再生成qm文件,最后在主程序的开始加载不同的qm文件,实现多语言。 步骤: 修改程序文件 在pro文件中加入说明 TRANSLATI…...
数据结构与算法之 leetcode 513. 找树左下角的值 (BFS) 广度优先
513. 找树左下角的值 /*** Definition for a binary tree node.* function TreeNode(val, left, right) {* this.val (valundefined ? 0 : val)* this.left (leftundefined ? null : left)* this.right (rightundefined ? null : right)* }*/ /*** param {T…...
mysql中的函数
MySQL提供了丰富的内置函数,涵盖了字符串操作、数字计算、日期和时间处理、条件判断、聚合计算等多个方面。这些函数可以帮助开发者在查询和数据处理时更高效地完成任务。下面是对MySQL中常见的函数分类及其主要函数的介绍: 字符串函数 CONCAT()&#x…...
Shell正则表达式与文本处理器
一、grep 1. 正则表达式 是一种匹配字符串的方法,通过一些特殊符号,快速实现查找,删除,替换某特定字符串。 选项: -a 不要忽略二进制数据。 -A 显示该行之后的内容。 -b 显示该行之前的内容。 -c 计算符合范本样…...

双指针法 ( 三数之和 )
题目 :给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k ,同时还满足 nums[i] nums[j] nums[k] 0 。请 你返回所有和为 0 且不重复的三元组。 注意:答案中不可以包含重复…...
感染恶意代码之后怎么办?
隔离设备 立即将感染设备与网络隔离,断开与互联网和其他设备的连接。这可以防止恶意代码进一步传播到其他设备,并减少对网络安全的威胁。 确认感染 确认设备是否真的感染了恶意代码。这可能需要使用安全软件进行全面扫描,以检测和识别任何已…...

【计算机网络】P3 计算机网络协议、接口、服务的概念、区别以及计算机网络提供的三种服务方式
目录 协议什么是协议协议是水平存活的协议的组成 接口服务服务是什么服务原语 协议与服务的区别计算机网络提供的服务的三种方式面向连接服务与无连接服务可靠服务与不可靠服务有应答服务与无应答服务 协议 什么是协议 协议,就是规则的集合。 在计算机网络中&…...
多角度剖析事务和事件的区别
事务和事件这两个概念在不同的领域有着不同的含义,尤其是在计算机科学、数据库管理和软件工程中。下面从多个角度来剖析事务和事件的区别: 计算机科学与数据库管理中的事务 事务(Transaction): 定义:在数据库管理中,…...

模糊小波神经网络(MATLAB 2018)
模糊系统是一种基于知识或规则的控制系统,从属于智能控制,通过简化系统的复杂性,利用控制法来描述系统变量之间的关系,采用语言式的模糊变量来描述系统,不必对被控对象建立完整的数学模型。相比较传统控制策略…...
HTML布局
标准流: 标准流就是元素在页面中的默认排列方式,也就是元素在页面中的默认位置。 1.1 块元素----独占一行----从上到下排列 1.2 行内元素----不独占一行----从左到右排列,遇到边界换行 1.3 行内块元素----不独占一行…...

数据结构:双链表
数据结构:双链表 题目描述参考代码 题目描述 输入样例 10 R 7 D 1 L 3 IL 2 10 D 3 IL 2 7 L 8 R 9 IL 4 7 IR 2 2输出样例 8 7 7 3 2 9参考代码 #include <iostream>using namespace std;const int N 100010;int m; int idx, e[N], l[N], r[N];void init…...
Python3 元组、列表、字典、集合小结
前言 本文主要对Python中的元组、列表、字典、集合进行小结,主要内容包括知识点回顾、异同点、使用场景。 文章目录 前言一、知识点回顾1、列表(List)2、 元组(Tuple)3、 字典(Dictionary)4.、…...

2024会声会影破解免费序列号,激活全新体验!
会声会影2024序列号注册码是一款专业的视频编辑软件,它以其强大的功能和易用性受到了广大用户的喜爱。在这篇文章中,我将详细介绍会声会影2024序列号注册码的功能和特色,帮助大家更好地了解这款产品。 会声会影全版本绿色安装包获取链接&…...
机器学习18个核心算法模型
1. 线性回归(Linear Regression) 用于建立自变量(特征)和因变量(目标)之间的线性关系。 核心公式: 简单线性回归的公式为: , 其中 是预测值, 是截距, 是斜…...
平滑值(pinghua)
平滑值 题目描述 一个数组的“平滑值”定义为:相邻两数差的绝对值的最大值。 具体的,数组a的平滑值定义为 f ( a ) m a x i 1 n − 1 ∣ a i 1 − a i ∣ f(a)max_{i1}^{n-1}|a_{i1}-a_i| f(a)maxi1n−1∣ai1−ai∣ 现在小红拿到了一个数组…...

使用matplotlib绘制折线条形复合图
使用matplotlib绘制折线条形复合图 介绍效果代码 介绍 在数据可视化中,复合图形是一种非常有用的工具,可以同时显示多种数据类型的关系。在本篇博客中,我们将探讨如何使用 matplotlib 库来绘制包含折线图和条形图的复合图。 效果 代码 imp…...

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
React 第五十五节 Router 中 useAsyncError的使用详解
前言 useAsyncError 是 React Router v6.4 引入的一个钩子,用于处理异步操作(如数据加载)中的错误。下面我将详细解释其用途并提供代码示例。 一、useAsyncError 用途 处理异步错误:捕获在 loader 或 action 中发生的异步错误替…...
模型参数、模型存储精度、参数与显存
模型参数量衡量单位 M:百万(Million) B:十亿(Billion) 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的,但是一个参数所表示多少字节不一定,需要看这个参数以什么…...

el-switch文字内置
el-switch文字内置 效果 vue <div style"color:#ffffff;font-size:14px;float:left;margin-bottom:5px;margin-right:5px;">自动加载</div> <el-switch v-model"value" active-color"#3E99FB" inactive-color"#DCDFE6"…...
macOS多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用
文章目录 问题现象问题原因解决办法 问题现象 macOS启动台(Launchpad)多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用。 问题原因 很明显,都是Google家的办公全家桶。这些应用并不是通过独立安装的…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...
《C++ 模板》
目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板,就像一个模具,里面可以将不同类型的材料做成一个形状,其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式:templa…...

MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...

【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...
BLEU评分:机器翻译质量评估的黄金标准
BLEU评分:机器翻译质量评估的黄金标准 1. 引言 在自然语言处理(NLP)领域,衡量一个机器翻译模型的性能至关重要。BLEU (Bilingual Evaluation Understudy) 作为一种自动化评估指标,自2002年由IBM的Kishore Papineni等人提出以来,…...