你,可能没完全搞懂 Java 泛型

·  阅读 847

大家好,我是yes。

今天我们来谈谈泛型。其实在初学的时候,我就对泛型有点蒙,因为看到有人说 Java 的泛型不是真的泛型,我搞不懂。

还有人说 Java 的泛型在实际运行时候会把类型给擦除了,我想着擦除是什么意思?为什么要擦除?

那把类型给擦除了为什么反射的时候还能得到泛型的类型信息?

我们今天就来盘一盘泛型:

  • 为什么需要泛型?
  • 为什么都说Java的泛型是伪泛型?
  • 为什么Java泛型的实现是类型擦除?
  • 既然擦除了类型,为什么在运行期仍能反射获得类型?

话不多说,发车!

为什么需要泛型

我们都知道在 Java5 之前是没有泛型的,没泛型都能用的好好的,那为什么要加个泛型呢,能给我们带来什么呢?

我们先来看下下面这段代码:

  List list = new ArrayList();
  list.add("yes"); // 加入string
  list.add(233); // 加入int
复制代码

在没有泛型的时候,加入的集合的数据并不会做任何约束,都会被当作成 Object 类型

可能有人说,这很好呀,多自由!确实,自由是自由了,但是代码的约束能力越低,就越容易出错,使用上也有诸多不便,比如获取的时候需要强转。

如果一不小心取错类型,编译的时候能过,但是运行的时候却抛错。

综上,Java 引入了泛型。

而泛型的作用就是加了一层约束,约束了类型。

有了这一层约束就好办事儿了,由于声明了类型,可以在编译的时候就识别出不准确的类型元素。使得错误提早抛出,避免运行时才发现。 并且也不需要在代码上显示的强转,从以下代码可以看出,能直接获取 String 类型元素。

我们再小结一下泛型的好处:

  • 提高了代码的可读性,一眼就能看出集合(其它泛型类)的类型
  • 可在编译期检查类型安全,增加程序的健壮性
  • 省心不需要强转(其实内部帮做了强转,下面会说)
  • 提高代码的复用率,定义好泛型,一个方法(类)可以适配所有类型 (其实以前 Object 也行,就是比较麻烦)

为什么都说Java的泛型是伪泛型

看起来我们平日用的一些泛型好像没啥毛病啊?为什么都说Java的泛型是伪泛型?哪里伪了?

我们再来看一段代码:

可以看到,我声明的是一个 String 类型的集合,但是通过反射往集合中插入了 int 类型的数据,居然成功了???

这说明在运行时泛型根本没有起作用!也就是说在运行的时候 JVM 获取不到泛型的信息,也会不对其做任何的约束

你可以认为 Java 的泛型就是编译的时候生效,运行的时候没有泛型,所以大家才说 Java 是伪泛型!

因此,虽然在 IDE 写代码的时候泛型生效了,而实际上在运行的时候泛型的类型是被擦除的

一言蔽之,Java的泛型只在编译时生效,JVM 运行时没有泛型。

为什么Java泛型的实现是类型擦除?

类型擦除 (type Erasure)。

Java 之所以在运行时将类型擦除的原因是为了向下兼容,即兼容 Java5 之前的编译的 class 文件。

例如 Java 1.2 上正在跑的代码,可以在 Java 5 的 JRE 上运行。

就是为了这该死的向下兼容,才使得 Java 实现的是伪泛型。

我从现有的实现倒推伪泛型的设计可能思路(我个人瞎掰的,您随意听听)是这样的:

  1. 这 Java 5 以前的版本,线上已经有很多应用在跑了,我好像不能新加一套,影响推广还可能被骂的很惨
  2. 咋办,泛型毕竟是加一个约束,以前的代码没这个约束啊,该如何兼容?
  3. 有了,要不我在编译器上动手脚,在编译的时候识别和约束泛型,然后编译过了就把泛型的信息擦除了。这样运行的时候约束不是没了吗?不就和之前保持一致了吗?好,就这样干了!

总而言之,就是为了向下兼容才采用类型擦除来实现的。

这里还有个坑,也就是泛型不支持基本类型,比如 int。因为泛型擦除后就变成了Object,这个 int 和 Object 兼容有点麻烦。

我在网上看 R大的解释如下:

GJ / Java 5说:这个问题有点麻烦,赶不及在这个版本发布前完成了,就先放着不管吧。于是Java 5的泛型就不支持原始类型,而我们不得不写恶心的ArrayList、ArrayList…
这就是一个偷懒了的地方。

emmm,这说明啥?写 Java 的也是程序员,也是要发版有上线需求的,所以说......

好了,言归正传,现在 Java 的泛型实现确实是伪泛型。看到这不经有人会发问?难道就只能一直伪泛型了吗?

那啥,我觉得吧,只要时间允许,只要钱够,应该都能做?哈哈哈。

既然擦除了类型,为什么在运行期仍能反射获得类型?

难道是没擦干净?别急,我们慢慢看。

我们先来回顾一下这段代码:

我们定义了泛型类型为 String 的 list,并且获取的 str 不需要强转,这一步是怎么做的呢? 我们 javap -c 看下字节码:

我们从反编译看生成的字节码可以看到, new 的 list 没有保存泛型的信息,所以是被擦除了。

然后看到 #7 没,有个 checkcast ,强转的类型是 String,看到这大伙儿应该都明白,为什么类型擦除了,但是我们 get 的时候不需要强转呢?因为编译器隐性的帮我们插入了强转的代码!所以我们的 Java 代码中不需要写强转。

再回到此小节标题:既然擦除了类型,为什么在运行期仍能反射获得类型?

答案就藏在 class 文件中。我们来看下这段代码:

通过反射,我确实获得了 list 的类型。那既然类型被擦除了,这又是怎么做到的呢?

我们直接进行一手 javap -v,反编译看到字节码里面有这样的记录:

这下很好理解了,class 文件里面存了这个信息,所以我们通过反射自然而然的就能得到这个类型。没错,就是这么简单。

也正因为原理如此,所以我们只能对以下三种情况利用反射获取泛型类型:

  • 成员变量的泛型
  • 方法入参的泛型
  • 方法返回值的泛型

对于局部变量这种是无能为力的。

最后

好了,今天关于泛型的文章暂时先到这,其实泛型的东西还没讲完,比如通配符、上界下界的限制(泛型的 PECS 原则),再如泛型的桥接,以及桥接的坑。

东西还挺多的,所以放下篇!等着哈。

我是 yes,从一点点到亿点点,欢迎关注我的公众号【yes的练级攻略】我们下篇见~

参考

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改