Java泛型类型擦除以及类型擦除带来的问题

简介: Java泛型在编译时会进行类型擦除,泛型信息被移除,仅保留原始类型(如Object或限定类型)。擦除后,List<String>和List<Integer>均变为List,导致getClass()相等;反射可绕过泛型限制,证明类型检查基于引用而非对象。静态成员不能使用类的泛型参数,因泛型实例化依赖对象创建,而静态上下文无此支持。

1.什么是泛型擦除

我们都知道Java的泛型是伪泛型,即编译期间所有的泛型信息都会被擦除,如我们代码定义了:List<Object>和List<String>,但是对于JVM而言,看到的只有List,由泛型附加的类型信息对于JVM而言是看不到的。代码说明如下:

1.1 原始类型擦除后相等

public class Test {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");
        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);
        System.out.println(list1.getClass() == list2.getClass());
    }
}

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型

1.2 反射添加的元素被擦除

public static void main(String[] args) 
    throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
        list.getClass().getMethod("add", Object.class).invoke(list, "asd");
        for (int i = 0; i < list.size(); i++) {
            // 输出1  asd
            System.out.println(list.get(i));
        }
    }

如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型

2.什么是泛型擦除后保留的原始类型

原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。举例说明

class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T  value) {  
        this.value = value;  
    }  
}

其对应的原始类型就是

class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

但如果该类的定义有限定,比如继承了,那么就会产生变化:

public class Pair<T extends Comparable> {}

此时原始类型就是Comparable,而不再是Object

3.泛型擦除引起的问题及解决方法

3.1 先检查,再编译以及编译的对应和引用传递问题

这里我们可能会有一个疑问,既然说类型变量会在编译的时候擦除掉,那为什么上面的ArrayList中添加String类型的时候就报错了呢,因为String编译时候也会变成Object啊?

A:因为JAVA编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译的。那么这个检查到底是针对谁的,我们需要再明确下

A2:如我们上面代码是:

ArrayList list = new ArrayList();

现在我们写成:

ArrayList<String> list = new ArrayList<String>();

此时如果我们与之前的代码兼容,各种引用传值之间,必然会出现下面情况:

ArrayList<String> list1 = new ArrayList(); //第一种 情况
ArrayList list2 = new ArrayList<String>(); //第二种 情况

这样没错,但是会有个编译时警告,不过在第一种情况下,可以实现与完全使用泛型参数一样的效果,但是第二种没有效果。

因为类型检查是编译时完成的,new ArrayList()只是在内存中开辟一个存储空间,可以存储任何类型的对象,而真正涉及类型检查的是“它的引用”,即list1的方法调用,如add方法,所以list1引用能够完成泛型类型检查(前面声明了String),但是list2(后面声明的只是开辟内存空间,不涉及)由于前面的声明没有添加泛型,所以不行。

所以这里我们也大概知道了,所谓的类型(泛型)检查,是针对引用的。谁是一个引用,用这个引用调用泛型方法,就会对这个引用所调用的方法进行类型检查,而无关它真正引用的对象。

3.2 自动类型转换

因为类型擦除的问题,所以所有的泛型类型变量在最后都会被替换成原始类型,既然都被替换了,那么为什么获取的时候,不需要进行强制类型转换呢?可以看下 ArrayList.get() 方法

public E get(int index) {  
    RangeCheck(index);  
    return (E) elementData[index];  
}

可以看到,在return之前,会根据泛型变量进行强转。假设泛型类型变量为Date,虽然泛型信息会被擦除掉,但是会(E) elementData[index],编译为(Date) elementData[index]。所以我们不用自己进行强转。当存取一个泛型域时也会自动插入强制类型转换。假设Pair类的value域是public的,那么表达式:

Date date = pair.value;

也会自动地在结果字节码中插入强制类型转换。

3.3 泛型擦除与多态的冲突与解决方法

假设有一个泛型类

class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T value) {  
        this.value = value;  
    }  
}

然后有一个子类需要继承

class DateInter extends Pair<Date> {  
    @Override  
    public void setValue(Date value) {  
        super.setValue(value);  
    }  
    @Override  
    public Date getValue() {  
        return super.getValue();  
    }  
}

在这个子类中,我们设定父类的泛型类型为Pair,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型

所以,我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override标签中也可以看到,一点问题也没有,实际上是这样的吗?

分析:实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

而此时,子类中类型依然是Date,这如果还是在继承关系中,那么根本就不是重写,而是重载了。通过反编译会发现子类中的方法Object getValue()和Date getValue()是同 时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别

3.4 泛型类型变量不能是基本数据类型

不能用类型参数替换基本类型。就比如,没有ArrayList<double>,只有ArrayList<Double>。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。

3.5 编译时集合的instanceof(可能面试考察)

ArrayList<String> arrayList = new ArrayList<String>();

因为类型擦除之后,ArrayList<String>只剩下原始类型,泛型信息String不存在了。那么,编译时进行类型查询的时候使用下面的方法是错误的

if( arrayList instanceof ArrayList<String>)

3.6 泛型在静态方法和静态类中的问题(可能面试考察)

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数,举例说明:

public class Test2<T> {    
    public static T one;   //编译错误    
    public static T show(T one){ //编译错误    
        return null;    
    }    
}

因为泛型类中的泛型参数的实例化是在对象定义时候指定的,而静态变量和静态方法是不需要通过对象来调用的,对象都没有创建,如何确定这个泛型是何类型呢?所以说上面的代码明显是错误的。


但是需要注意下面的一种特殊情况

public class Test2<T> {    
    public static <T>T show(T one){ //这是正确的    
        return null;    
    }    
}

因为这是一个泛型方法,在泛型方法中使用过的T是自己在方法中定义的T,而不是泛型中的T



相关文章
|
1天前
|
SQL 运维 监控
如何做好SQL质量监控
git-poison基于go-git实现分布式bug追溯,解决多分支开发中bug漏修、漏发问题。通过“投毒-解毒-银针”机制,自动化卡点发布流程,降低协同成本,提升发布安全性与效率,已在大型团队落地应用。
|
1天前
|
存储 搜索推荐 数据库
ES分布式搜索引擎入门
本课程学习Elasticsearch核心知识,包括倒排索引原理、IK分词器使用、Java Client操作索引的增删改查、批量导入、搜索查询(Term、全文、布尔查询)及排序分页等技能,掌握其在海量数据搜索中的高性能优势与实际应用。
 ES分布式搜索引擎入门
|
1天前
|
运维 Devops 开发工具
生产环境缺陷管理
针对大型团队中多分支开发导致的bug管理难题,本文介绍基于go-git实现的通用化工具git-poison。通过“投毒-解毒-银针”机制,实现bug的分布式追溯与自动化卡点,有效避免漏修复、漏发布等问题,降低协同成本,提升发布安全性与效率。
 生产环境缺陷管理
|
1天前
|
消息中间件 存储 Java
异步消息组件MQ高级
本文围绕消息中间件(如RabbitMQ)的可靠性机制展开,涵盖生产者重试与确认、消费者确认、消息持久化、失败处理、幂等性及延迟消息等核心内容。重点讲解如何通过重试机制、Confirm/Return确认模式保障消息发送可靠;通过手动或自动ACK确保消费可靠性;并结合定时任务实现失败消息重发,最终构建高可用的消息系统。
异步消息组件MQ高级
|
1天前
|
消息中间件 Java 数据安全/隐私保护
异步消息组件MQ基础
本课程介绍RabbitMQ在微服务中的应用,涵盖MQ的应用场景、异步调用与同步调用的区别、RabbitMQ的安装与配置、消息收发入门程序、工作队列、发布订阅模型及多种交换机(fanout、Direct、Topic)特性,同时讲解惰性队列、优先级队列、消息堆积处理及商城项目中的实际应用,帮助学员掌握消息中间件的核心技术与实践能力。
异步消息组件MQ基础
|
1天前
|
负载均衡 Java 应用服务中间件
微服务网关与配置中心
本课程学习微服务网关核心功能,包括使用Spring Cloud Gateway实现路由转发、负载均衡及全局过滤器应用。掌握通过Nacos进行服务发现与配置管理,实现统一鉴权、用户身份校验及请求头信息传递,并完成前后端联调与配置热更新等实战操作。
微服务网关与配置中心
|
1天前
|
Java 测试技术 Linux
生产环境发布管理
本文介绍大型团队如何通过自动化部署平台实现多环境(dev/test/pre/prod)高效发布。涵盖各环境职责、基于Jenkins+K8S的CI/CD流程、分支管理与一键部署,并结合Skywalking等工具实现日志链路追踪与快速排错,提升发布效率与系统稳定性。
 生产环境发布管理
|
1天前
|
Java 数据库 微服务
微服务服务注册与发现
本课程以黑马商城项目为案例,系统讲解单体架构与微服务架构的优缺点,深入剖析微服务、分布式及云原生架构的核心理念与技术实现。通过搭建项目环境、拆分商品与购物车服务、使用Nacos实现服务注册发现、OpenFeign远程调用等实践,掌握微服务开发全流程。
微服务服务注册与发现
|
1天前
|
SQL Java 数据库连接
持久层框架MyBatisPlus
MyBatisPlus是MyBatis的增强工具,简化单表CRUD操作,通过继承BaseMapper即可实现增删改查。支持条件构造器(如QueryWrapper)、分页插件、代码生成器等功能,提升开发效率,降低SQL冗余,广泛应用于企业级项目中。
 持久层框架MyBatisPlus
|
1天前
|
SQL 安全 网络协议
常见的网络攻击
恶意软件、网络钓鱼、中间人攻击、DDoS攻击、SQL注入、零日漏洞及DNS隧道是常见网络威胁。恶意软件通过漏洞入侵,窃取数据或破坏系统;网络钓鱼伪装可信来源骗取信息;MitM攻击窃听通信;DDoS以海量流量瘫痪服务;SQL注入强迫数据库泄露敏感信息;零日攻击利用未修复漏洞;DNS隧道则隐蔽传输数据,危害网络安全。
常见的网络攻击