Java中常见错误-泛型擦除及桥接方法问题及解决方案

news2024/11/17 14:35:05

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_BRIDGEACC_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> {
    @Override
    public void setValue(String value) {
        System.out.println("Pig2.setValue called");
        super.setValue(value);
    }
}

public class Animal<T> {

    AtomicInteger updateCount = new AtomicInteger();

    private T value;
    @Override
    public 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中常见错误-泛型擦除及桥接方法问题及解决方案 文章到此结束,创作不易,望我佬们三连支持一下

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1792549.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

html+CSS+js部分基础运用15

1、完成输入框内容的实时反向输出。 2、银行账户余额变动自动通知项目。 设计要求&#xff1a;单击按钮后&#xff0c;余额按照输入框的数额减少&#xff0c;同时将按钮式的提示信息&#xff08;金额&#xff09;同步改变。利用侦听属性实现余额发生变化时发出提示信息&#x…

python-flask项目的服务器线上部署

在部署这部分我首先尝试了宝塔面板&#xff0c;始终连接失败 换了一种思路选择了Xshell成功连接 首先我们需要下载个免费版本的Xshell 免费的&#xff1a;家庭/学校免费 - NetSarang Website 下载完毕打开 1新建-> 输入服务器的账号密码&#xff1a; 在所有会话中点击自…

NDIS Filter开发-PNP响应和安装

NDIS filter驱动可能是最容易生成的驱动之一&#xff0c;如果你安装了VS 2015 WDK之后&#xff0c;你可以直接生成一个能运行的Filter驱动&#xff0c;它一般是ndislwf。 和大部分硬件不同&#xff0c;NDIS Filter驱动介于软件和硬件抽象层之上&#xff0c;它和硬件相关&…

工业无线wifi系统搭配高速路由,解决联网及数据传输

​面对日益复杂的工业应用场景,企业对无线网络的高速、可靠和安全提出了更高要求。星创易联SR600系列多网口4G路由器应运而生,为工业无线WiFi系统提供了一个性能卓越的高速路由方案。&#xff08;key-iot.com/iotlist/sr600-5.html&#xff09; SR600路由器集4G LTE、虚拟专用…

c++(内存分配,构造,析构)

#include <iostream>using namespace std; class Per { private:string name;int age;double *height;double *weigh; public://无参构造Per(){cout << "Per::无参构造" << endl;}//有参构造Per(string name,int age,double height,double weigh):…

C++候捷stl-视频笔记4

一个万用的hash function 哈希函数的形式&#xff0c;一种是一般函数(右边)&#xff0c;一种是成员函数(左边)&#xff0c;类的对象将成为函数对象 具体做法例子。直接把属性的所有hash值加起来&#xff0c;会在hashtable中会产生很多的碰撞&#xff0c;放在同一个bucket中的元…

Nginx的https功能

一.HTTPS功能简介 Web网站的登录页面都是使用https加密传输的&#xff0c;加密数据以保障数据的安全&#xff0c;HTTPS能够加密信息&#xff0c;以免敏感信息被第三方获取&#xff0c;所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议&#xff0c;HTTPS其实…

优化家庭网络,路由器无线中继配置全攻略(中兴E1600无线中继设置/如何解决没有预埋有线网络接口的问题/使用闲置路由实现WIFI扩展)

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 网络优化 📒📒 操作步骤 📒💡适用场景🚨 常见问题及解决方案⚓️ 相关链接 ⚓️📖 介绍 📖 在现代家庭生活中,WiFi已经渗透到我们生活的每一个角落,成为了日常生活中不可或缺的一部分。然而,不少用户常常遇到W…

Bytebase 作为唯一数据库工具厂商,亮相亚马逊云科技中国峰会

作为云计算行业的风向标&#xff0c;亚马逊云科技中国峰会每年都吸引着全球顶尖企业和行业精英。此次峰会不仅展示了最新的 AI 技术趋势和解决方案&#xff0c;还为参展商和与会者提供了一个卓越的交流与合作平台。 Bytebase 作为全场唯一的数据库工具厂商亮相数据区&#xff0…

Windows下Qt5.14.2连接华为IoTDA平台

一、华为IoTDA简介 华为云物联网平台&#xff08;IoT 设备接入云服务&#xff09;提供海量设备的接入和管理能力&#xff0c;将物理设备联接到云&#xff0c;支撑设备数据采集上云和云端下发命令给设备进行远程控制&#xff0c;配合华为云其他产品&#xff0c;帮助您快速构筑物…

学习笔记——IP地址网络协议——VLSM-可变长子网掩码(子网划分)

四、VLSM-可变长子网掩码(子网划分) 1、为什么要子网划分 为什么要子网划分&#xff1a;有类IP地址规划的缺陷。IP地址空间只能按照默认的类别使用&#xff0c;例如一个B类地址&#xff0c;默认掩码为255.255.0.0&#xff0c;意味着这个地址空间里有2的16次方个IP&#xff0c;…

从零开始实现自己的串口调试助手(3) - 显示底部收发,优化串口打开/关闭

注意: 1. 我们要实现自发自收&#xff0c;要将tx&#xff0c;rx连起来 2.发送的 不能是中文符号&#xff0c;因为这可能导致&#xff0c;读取到的是英文符号 --> 导致接收到的size 和发送的size 大小不一致 3.注意同时定义两个槽函数的时候两个槽函数都会被调用&#xff0c;…

2024 年最新安装MAC-vue教学包括常见错误

花了一上午时间终于将 vue 的工程文件安装好了&#xff0c;本教材是傻瓜式操作&#xff0c;按着教程一步一步操作最后就可以看到页面了。 安装Node 1.在线地址&#xff1a; https://nodejs.org/en 2、点击 Download Node.js下载即可&#xff0c;下载完成后&#xff0c;傻瓜式的…

【数智化CIO展】吉家宠物CIO张志伟:深度挖掘数据价值是数字化发展趋势,才能实现企业精细化运营...

张志伟 本文由吉家宠物CIO张志伟投递并参与由数据猿联合上海大数据联盟共同推出的《2024中国数智化转型升级优秀CIO》榜单/奖项评选。丨推荐企业&#xff1a;观远数据 大数据产业创新服务媒体 ——聚焦数据 改变商业 中国“宠物经济”热潮不断攀升&#xff0c;国内宠物市场的竞…

共享使用模型以节省磁盘空间

如果同时使用了多个工具&#xff08;例如 Easy Diffusion, Stable Diffusion UI, Comfy)&#xff0c;则可以通过共享使用保存在某个目录下的模型文件来节省磁盘空间。 1. Easy Diffusion 在Easy Diffusion中可以创建一个链接文件夹&#xff0c;以便在不同的 Stable Diffusion…

宜选影票特惠电影票api接口需要哪些技术支持?宜选影票api文档

特惠电影票API接口的开发和对接需要一系列技术支持&#xff0c;以确保数据的准确性、接口的稳定性以及用户使用的便捷性。以下是所需的主要技术支持&#xff0c;以清晰的分点表示和归纳&#xff1a; 1. API开发技术 RESTful API&#xff1a;特惠电影票API接口通常采用RESTful…

【UML用户指南】-05-对基本结构建模-类

目录 1、名称&#xff08;name&#xff09; 2、属性 &#xff08;attribute&#xff09; 3、操作&#xff08;operation&#xff09; 4、对属性和操作的组织 4.1、衍型 4.2、职责 &#xff08;responsibility&#xff09; 4.3、其他特征 4.4、对简单类型建模 5、结构良…

Coolmuster iOS 数据擦除:隐私保护的终极方案

手机和平板电脑是我们不可或缺的伙伴&#xff0c;它们存储着我们的照片、联系人、私人消息以及工作文件。然而&#xff0c;当这些设备需要更换或者出售时&#xff0c;如何确保存储在其中的数据不被他人恢复和滥用&#xff0c;成为了一个严峻的问题。Coolmuster iOS 数据擦除&am…

2024年船舶、机械制造与海洋科学国际会议(ICSEMMS2024)

2024年船舶、机械制造与海洋科学国际会议&#xff08;ICSEMMS2024&#xff09; 会议简介 我们诚挚邀请您参加将在重庆隆重举行的2024年国际造船、机械制造和海洋科学大会&#xff08;ICSEMMS024&#xff09;。作为一项跨学科和跨学科的活动&#xff0c;本次会议旨在促进造船…

基于SSM+Jsp的二手手机回收平台系统

开发语言&#xff1a;Java框架&#xff1a;ssm技术&#xff1a;JSPJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包…