详解Java泛型(二)之类型擦除

本文详细介绍了Java泛型的工作原理,包括类型擦除的过程及规则,以及如何在编译过程中生成桥接方法来解决类型安全问题。通过实例展示了不同类型限定对泛型类的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 概述

其实Java中的泛型是伪泛型,什么意思呢?就是说它并不是一直都存在的。Java泛型的处理几乎都在编译器中进行,在生成的字节码文件(.class文件)中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,然后编译器在编译的时候去掉,这个过程就是类型擦除

比如下面这段代码使用到了泛型,当list.get(0)的时候不用显示强转变成String类型,在没有使用泛型的时候就需要显示强转一下。

import java.util.ArrayList;
import java.util.List;

public class ArrayListDemo {

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("demo");
        String str = list.get(0);
    }
}

然而,在编译以后,它还是给你强转了。我把上面的代码编译后再反编译一下,上面的代码就变成了下面这个样子

import java.util.ArrayList;
import java.util.List;

public class ArrayListDemo
{

    public ArrayListDemo()
    {
    }

    public static void main(String args[])
    {
        List list = new ArrayList();
        list.add("demo");
        String str = (String)list.get(0);
    }
}

可以看见,编译器不仅自动帮我添加上了无参构造方法,而且把List的类型变量擦除了,从后面的list.get(0)还是得强转成String类型可以看出。

那么现在问题来了,类型变量被擦除后,泛型类或者说泛型方法它会变成怎样呢?下面就要说一下它的擦除规则。

2. 类型擦除规则

无论什么时候定义一个泛型类型,都会自动提供一个相应的原始类型(raw type),原始类型的名字就是删去类型参数后泛型类型名。如Pair<T>这个泛型类的原型类型就是Pair

那还有一个问题,像上面那个例子,类型变量被擦除后,在字节码中真正的类型是什么?在类型变量被擦除(crased)后,就会用限定类型(无限定的变量用Object)来代替。

例如,我有一个Pair泛型类,代码如下

public class Pair<T> {
    private T t1;
    private T t2;
    public T getT1() {
        return t1;
    }
    public void setT1(T t1) {
        this.t1 = t1;
    }
    public T getT2() {
        return t2;
    }
    public void setT2(T t2) {
        this.t2 = t2;
    }
}

我将这个类编译后再反编译一下,这个类就变成了下面这个样子

public class Pair
{

    public Pair()
    {
    }

    public Object getT1()
    {
        return t1;
    }

    public void setT1(Object t1)
    {
        this.t1 = t1;
    }

    public Object getT2()
    {
        return t2;
    }

    public void setT2(Object t2)
    {
        this.t2 = t2;
    }

    private Object t1;
    private Object t2;
}

可以看到类型参数T就被Object替换掉了,所以第一个例子中 String str = (String)list.get(0);其实是将Object类型强转成String类型,因为List和Pair的类型变量都没有被限定,所以就用Object来替代了。

下面再来看看有限定的情况。还是Pair类,我将它的类型变量限定为实现了Comparable接口的类,代码如下

public class Pair<T extends Comparable> {
    private T t1;
    private T t2;
    public T getT1() {
        return t1;
    }
    public void setT1(T t1) {
        this.t1 = t1;
    }
    public T getT2() {
        return t2;
    }
    public void setT2(T t2) {
        this.t2 = t2;
    }
}

编译后再反编译就会变成下面这个样子

public class Pair
{

    public Pair()
    {
    }

    public Comparable getT1()
    {
        return t1;
    }

    public void setT1(Comparable t1)
    {
        this.t1 = t1;
    }

    public Comparable getT2()
    {
        return t2;
    }

    public void setT2(Comparable t2)
    {
        this.t2 = t2;
    }

    private Comparable t1;
    private Comparable t2;
}

可以看到类型变量T被限定类型Comparable替代了。

如果有多个限定类型呢?比如说泛型类Pair<T extends Comparable & Serializable>,如果有多个限定类型的话,会使用第一个限定类型来替换类型变量。如将上面的Pair<T extends Comparable>改为Pair<T extends Serializable & Comparable>,那么反编译后的代码就会是这样子

import java.io.Serializable;

public class Pair
{

    public Pair()
    {
    }

    public Serializable getT1()
    {
        return t1;
    }

    public void setT1(Serializable t1)
    {
        this.t1 = t1;
    }

    public Serializable getT2()
    {
        return t2;
    }

    public void setT2(Serializable t2)
    {
        this.t2 = t2;
    }

    private Serializable t1;
    private Serializable t2;
}

那么这里就有个建议,就是大家在有多个限定类型的时候,把标志接口(没有方法的接口)如上面的Serializable放到最后面,这样子效率会高一点。为什么呢?我用一个例子说明一下,还是上面那个类,不过我加了一个getMax方法,里面有用到Comparable的compareTo方法。

import java.io.Serializable;

public class Pair<T extends Serializable & Comparable> {
    private T t1;
    private T t2;
    public T getT1() {
        return t1;
    }
    public void setT1(T t1) {
        this.t1 = t1;
    }
    public T getT2() {
        return t2;
    }
    public void setT2(T t2) {
        this.t2 = t2;
    }

    public T getMax(){
        return t1.compareTo(t2) > 0 ?t1:t2;
    }
}

反编译后

import java.io.Serializable;

public class Pair
{

    public Pair()
    {
    }

    public Serializable getT1()
    {
        return t1;
    }

    public void setT1(Serializable t1)
    {
        this.t1 = t1;
    }

    public Serializable getT2()
    {
        return t2;
    }

    public void setT2(Serializable t2)
    {
        this.t2 = t2;
    }

    public Serializable getMax()
    {
        return ((Comparable)(Comparable)t1).compareTo(t2) <= 0 ? t2 : t1;
    }

    private Serializable t1;
    private Serializable t2;
}

可以看出,在getMax方法中做了类型强转,如果将Comparable接口放到限定类型的前面,将Serializable接口放到后面,就是这样子Pair<T extends Comparable & Serializable>,因为会用第一个限定类型即Comparable去替换类型变量,就不会有这种问题,getMax方法就是这样子的

   public Comparable getMax()
    {
        return t1.compareTo(t2) <= 0 ? t2 : t1;
    }

3. 桥接方法

现在我有一个Person泛型类,里面有个age成员变量和setAge方法,注意现在age的类型还不知道

public class Person<T> {
    private T age;

    public void setAge(T age) {
        this.age = age;
    }

    public T getAge() {
        return age;
    }
}

然后有个Student类继承了Person类,并且确定了T的类型为Integer,并且重写了setAge方法

public class Student extends Person<Integer>{

    @Override
    public void setAge(Integer age) {
        if(age > 0)
            super.setAge(age);
    }
}

然后我将Student类反编译

public class Student extends Person
{

    public Student()
    {
    }

    public void setAge(Integer age)
    {
        if(age.intValue() > 0)
            super.setAge(age);
    }

    public volatile void setAge(Object obj)
    {
        setAge((Integer)obj);
    }
}

大家有没有发现多了个setAge(Object obj)方法,很明显这个方法与setAge(Integer age),因为它们的参数列表不同。至于为什么会有volatile关键字在我也不清楚,麻烦知道的朋友能说一下。那么为什么会多出一个这样子的方法呢?

大家思考一下下面的代码

public class Main {

    public static void main(String[] args) {
        Student student = new Student();
        Person<Integer> person = student;
        person.setAge(123);
    }
}

这段代码是没有问题的,就是多态性的展示。下面就要说一下它的执行过程。

  1. 创建了一个Student对象
  2. 然后用Person类型的变量person引用Student对象
  3. 因为变量person是Person类型的,而Person类型在编译后只有一个简单的方法setAge(Object),但是虚拟机引用的对象是Student类型的,因而会调用Student.setAge(Object)方法,这个方法就是桥接方法,在这个方法里面调用了Student.setAge(Integer)方法,而这正是我们想要的。

然而如果让Student类再重写Person类的getAge方法的时候就会发生很诡异的事情,反编译后是这样子的

public class Student extends Person
{

    public Student()
    {
    }

    public void setAge(Integer age)
    {
        if(age.intValue() > 0)
            super.setAge(age);
    }

    public Integer getAge()
    {
        return (Integer)super.getAge();
    }

    public volatile void setAge(Object obj)
    {
        setAge((Integer)obj);
    }

    public volatile Object getAge()
    {
        return getAge();
    }
}

还没发现问题所在吗?有两个getAge方法呀,就是返回值和修饰符不同,编译器是根据方法签名(方法名加参数列表)来判断重载的,所以这根本不能重载呀,只有方法名相同并且参数列表不同才是重载呀,对于编译器来说这特么的就是一样的方法!为什么可以这样子,如果直接这样子写的话编译是通过不了的呀!

通过上面的情况可以看出编译器可能会产生两个仅返回类型和修饰符不同的方法字节码,而且虚拟机能够处理这种情况

更多关于桥接方法的介绍大家可以去这里看看http://blog.youkuaiyun.com/timheath/article/details/53557045

<think>嗯,用户之前问过Java和C++语法区别,现在接着问Java类型擦除。看来他对Java机制不太清楚,特别是和C++模板的区别。类型擦除Java实现的一个核心点,可能他在实际编程中遇到了相关的问题,或者想深入理解Java的设计选择。 我需要先确认类型擦除的定义,然后解释为什么Java要用类型擦除,比如为了兼容旧版本,保持字节码兼容。然后要举例子说明类型擦除的具体表现,比如编译后的类型信息丢失,无法在运行时获取参数类型。可能还需要对比C++的模板,因为用户之前的问题涉及到两者的比较,这样他能更好地理解两者的差异。 另外,用户可能想知道类型擦除带来的限制,比如不能创建数组,instanceof无法使用,以及重载的问题。这些实际开发中的痛点需要具体例子说明。同时,要提到绕过类型擦除的方法,比如通过传递Class对象,这样用户知道如何处理类型信息丢失的情况。 还要注意用户的技术水平,他可能已经有一定编程经验,但可能对底层机制不熟悉,所以需要用简洁明了的语言,避免过于技术化的术语,但又要准确。可能需要用代码示例来辅助解释,比如ArrayList<Integer>和ArrayList<String>在运行时的类型是一样的,这样更直观。 最后总结类型擦除的意义,优缺点,让用户理解Java设计的选择背后的权衡。可能用户想了解为什么Java不采用C++那样的具体化,所以需要提到兼容性和迁移成本的问题。</think>以下是关于 Java 类型擦除(Type Erasure)的详细说明,从设计原理到实际影响逐步解析: --- ### 一、**什么是类型擦除** 类型擦除Java (Generics)的核心实现机制。**编译时保留类型信息,但在编译后会将类型替换为原始类型(Raw Type)或边界类型(如 `Object`),并插入类型转换**。这一设计是为了兼容 Java 5 之前的旧版本代码。 **示例**: ```java // 编译前代码 List<String> list = new ArrayList<>(); list.add("Hello"); String s = list.get(0); // 编译后等效代码(类型擦除后) List list = new ArrayList(); // 原始类型 list.add("Hello"); String s = (String) list.get(0); // 制转换 ``` --- ### **类型擦除的具体表现** #### 1. **类型信息丢失** - 运行时无法获取参数的具体类型: ```java List<Integer> intList = new ArrayList<>(); List<String> strList = new ArrayList<>(); // 输出均为 true(运行时类型相同) System.out.println(intList.getClass() == strList.getClass()); ``` #### 2. **类型参数替换规则** - 无界类型参数(如 `<T>`)替换为 `Object`: ```java // 编译前 public class Box<T> { private T value; } // 编译后等效 public class Box { private Object value; } ``` - 有界类型参数(如 `<T extends Number>`)替换为边界类型: ```java // 编译前 public class Box<T extends Number> { private T value; } // 编译后等效 public class Box { private Number value; } ``` --- ### 三、**类型擦除带来的限制** #### 1. **无法创建数组** ```java // 编译错误:Generic array creation T[] array = new T[10]; ``` #### 2. **无法使用 `instanceof` 检查类型** ```java if (list instanceof List<String>) { ... } // 编译错误 ``` #### 3. **重载冲突** ```java // 编译错误:方法签名冲突(擦除后均为 List<Object>) public void print(List<String> list) {} public void print(List<Integer> list) {} ``` #### 4. **无法直接实例化类型** ```java public class Box<T> { private T obj = new T(); // 编译错误:类型擦除后 T 变为 Object } ``` --- ### 四、**如何绕过类型擦除的限制** #### 1. **显式传递 `Class` 对象** ```java public class Box<T> { private Class<T> type; public Box(Class<T> type) { this.type = type; } public T createInstance() throws Exception { return type.getDeclaredConstructor().newInstance(); } } ``` #### 2. **通过反射获取信息(有限场景)** ```java // 获取父类参数类型(需继承带的父类) public class StringList extends ArrayList<String> {} Type genericType = StringList.class.getGenericSuperclass(); if (genericType instanceof ParameterizedType) { Type[] types = ((ParameterizedType) genericType).getActualTypeArguments(); System.out.println(types[0]); // 输出 class java.lang.String } ``` --- ### 五、**类型擦除的意义与争议** #### **优点**: - **兼容性**:确保代码与旧版本 JVM 兼容。 - **性能**:运行时无需维护类型信息,减少开销。 #### **缺点**: - **灵活性降低**:部分高级特性无法实现(如具体化类型)。 - **代码冗余**:需手动处理类型转换(如集合类)。 --- ### 六、**与 C++ 模板的对比** | 特性 | Java 类型擦除) | C++ 模板 | |------------------|------------------------------------------|-----------------------------------| | **实现机制** | 编译时擦除类型,运行时无类型信息 | 编译时生成具体类型代码(代码膨胀)| | **类型检查** | 编译时检查,运行时无类型 | 编译时生成类型安全检查 | | **性能** | 无额外优化(依赖制转换) | 可生成高度优化的机器码 | | **灵活性** | 受限于类型擦除 | 支持模板元编程等复杂特性 | --- ### 总结 类型擦除Java 实现的核心机制,**在保证兼容性的同时牺牲了部分运行时灵活性**。理解这一机制有助于避免编程中的常见陷阱(如类型转换错误),并合理利用反射等工具突破限制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值