文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java 泛型编程所说的类型擦除到底是什么?

2024-12-13 15:01

关注

什么是泛型

在 Java 中,不会泛型,寸步难行。泛型可能是一个 Java 初学者需要攻克的第一个难点。随便跟着一门教程或 任何一本《Java入门到精通》,前面关于变量、关键字、语法(if、while、for等等)这些基本上是一看就懂,而当内容来到泛型的时候,大部分人可能就突然感觉没那么轻松了。

如果没有编程经验的话,可能需要练习一段时间才能完全掌握泛型编程概念和技巧,这么说吧,有些人写了好几年代码,碰到泛型的时候可能还是不太熟练。

说到Java泛型,最明显的标志就是 <> 。

泛型是什么呢?通俗的说就是一个类型是没有固定类型的,即可以是Integer 也可以是 Long,还可能是你自定义的类。

泛型使类型(类和接口)能够在定义类、接口和方法时成为参数。与方法声明中使用的更熟悉的形式参数非常相似,类型参数为您提供了一种通过不同输入重复使用相同代码的方法。区别在于形式参数的输入是值,而类型参数的输入是类型。

例如在类定义中使用泛型,最常见的 ArrayList

public class ArrayList extends AbstractList
        implements List, RandomAccess, Cloneable, java.io.Serializable
{
//... code
}

例如在方法参数中使用泛型,来一个复杂的例子

public static , U extends List, R extends T> R complexMethod(U list, T element) {

}

在这个例子中,有两个传入参数 U list, T element,而这两个参数需要在方法的返回类型前用<>做出说明,也就是 , U extends List, R extends T>这一部分。

返回值也是一个泛型 R。

为什么是 T、U、R

经常看到泛型类型用 T、U、R,还有K、V 这样的符号表示。我们肯定知道不用T也完全没问题,用 X 也可以。

之所以这么统一是因为这是官方比较推荐的写法,推荐的规则如下:

泛型的作用

前面也说了,当一个参数预期可能有多种类型的时候,就会用到泛型,那既然是类型不确定,那直接用 Object 不就行了吗,何必费事儿呢?一会儿讲到类型擦除的时候会发现,本身类型擦除的核心就是把泛型类型转为 Object。但是这是编译器干的,为了给JVM看的。而作为开发者和编译器,使用泛型还是有很大好处的。

在编译时提供更严格的类型检查,如果代码违反类型安全,编译器可以及时发现,而不是等到运行的时候抛出运行时异常。

使程序员能够实现通用算法。通过使用泛型,程序员可以实现适用于不同类型集合的泛型算法,可以自定义,并且类型安全且更易于阅读。

例如下面这个方法,只接受Number 类型的参数,用来比较两数的大小。

public static  Boolean compare(T first, T second) {
        double firstValue = first.doubleValue();
        double secondValue = second.doubleValue();

        return firstValue > secondValue;
    }

消除不必要的类型转换。

例如下面不用泛型的情况,每次取数据的时候都要转换一下类型。

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

而用了泛型后,就不用自己转换了。

List list = new ArrayList();
list.add("hello");
String s = list.get(0);

类型擦除

Java 中的泛型实现可以说就是用的类型擦除原理。通俗一点说,类型只在编译期存在,在运行时就不在了,都变为了 Object,一视同仁。

在我们写好代码进行编译时,编译器会将泛型参数的类型进行替换,大部分情况下会将类型替换为 0bject 类型。这种行为模式用类型擦除来描述就非常形象。

类型擦除原理

在类型擦除过程中,Java 编译器会擦除所有类型参数,如果类型参数有界,则用其第一个边界替换每个参数;如果类型参数无界,则用 Object 替换。

在类型擦除过程中,编译器会按照以下规则来处理泛型类型参数:

如果类型参数有界(bounded type),即使用了extends关键字限定了类型的上界,例如,则编译器会用该类型的第一个边界来替换类型参数。

例如下面这个例子,泛型 T 继承了Number类型,又实现了 Displayable 接口(没错,泛型可以这样定义)

interface Displayable {
    void display();
}

public class Result {
    private T value;

    public Result(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
    
 public void show() {
        value.display();
    }
}

在编译器进行类型擦除后会变成下面这样,因为 T 的上限是 Number,所以直接将 T 替换为 Number。

public class Result {
    private Number value;

    public Result(Number value) {
        this.value = value;
    }

    public Number getValue() {
        return value;
    }
}

如果类型参数无界(unbounded type),即没有限定类型的上界,例如,则编译器会用Object类型来替换类型参数。

例如下面方法,没有指定类型上限类型。

public static  int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

经过编译器的擦除处理后,就变成下面这样,都替换成了 Object。

public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

桥接方法

来看一下下面这段代码

public class Node {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
        this.data = data;
    }
}

public class SubNode extends Node {
    public SubNode(Integer data) { super(data); }

    public void setData(Integer data) {
        super.setData(data);
    }
    
 public static void main(String[] args) {
        SubNode subNode = new SubNode(8);
        Node node = subNode;
        node.setData("Hello");
        Integer x = subNode.data;
    }

}

这段代码大家一看就知道肯定是有问题的,运行的时候会出现 ClassCastException,但是编译是可以通过的。

而运行时出现错误的代码是 node.setData("Hello");这一行,但是经过前面对类型擦除的了解,Node 类的 setData 参数肯定被擦除成了 Object 类型了,既然是 Object,那Integer 和 String 都满足啊,为啥还会报错呢。

这就要说到桥接了。

当编译器对泛型扩展的类或接口进行编译处理的时候,会根据实际的类型进行方法的桥接处理。什么意思呢,还是拿上面的 Node 和 SubNode 类说明。

类型擦除后的代码是下面这样的,多了一个桥接方法。

public class Node {

    public Object data;

    public Node(Object data) { this.data = data; }

    public void setData(Object data) {
        this.data = data;
    }
}

public class SubNode extends Node {

    public SubNode(Integer data) { super(data); }

 
 public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        super.setData(data);
    }
}

为什么需要这个桥接方法呢?

Node 类的 setData 方法入参是 Object 类型。

public void setData(Object data) {
    this.data = data;
}

而 SubNode 的setData 方法入参是 Integer。

public void setData(Integer data) {
 super.setData(data);
}

所以,SubNode 的 setData 方法并不会重写父类 Node 的setData 方法,而想要重写的话,就必须让 SubNode 的setData 的入参也是 Object,这就是桥接方法的由来。

public void setData(Object data) {
 setData((Integer) data);
}

这样一来重写父类的方法,但是要把参数强转成 Integer。

前面说的 node.setData("Hello");这一行会报错,那大家就知道为什么了吧,是因为把 Hello强转为 Integer 的时候出现的错误。

总结

正是类型擦除的机制帮助 Java 实现了泛型编程,让我们作为开发者能够更好的了解和控制我们正在使用类型的是什么,而不是 Object 满天飞。

来源:古时的风筝内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯