背景

昨天,在逛论坛时遇到个这么个问题,上代码:

  1. public class GenericTest {
  2. //方法一
  3. public static <T extends Comparable<T>> List<T> sort(List<T> list) {
  4. return Arrays.asList(list.toArray((T[]) new Comparable[list.size()]));
  5. }
  6. //方法二
  7. public static <T extends Comparable<T>> T[] sort2(List<T> list) {
  8. // 这里没报错
  9. return list.toArray((T[]) new Comparable[list.size()]);
  10. }
  11. public static void main(String[] args) {
  12. List<Integer> list = new ArrayList<>();
  13. list.add(1);
  14. list.add(2);
  15. // 方法一调用正常
  16. System.out.println(sort(list).getClass());
  17. // 方法二调用报错了,这里报错了
  18. System.out.println(sort2(list).getClass());
  19. }
  20. }

这个问题有以下四个现象:

(1)方法一调用完全正常;

(2)方法二调用报错了;

(3)方法二报错的地方是在System.out.println(sort2(list).getClass());这行,而不是return list.toArray((T[]) new Comparable[list.size()]);这行;

(4)报的错是[Ljava.lang.Comparable; cannot be cast to [Ljava.lang.Integer;

怎么样?你心中有答案嘛?类型擦除?怎么擦?摩擦摩擦?

解决

刚拿到这道题,我也是一脸懵逼,这要报错也应该是在return list.toArray((T[]) new Comparable[list.size()]);这行啊,而且要报错应该两个方法都报错啊。

抱着不放弃不抛弃的心态,彤哥做了大量的实验,终于得出了泛型的本质,且听我娓娓道来。

小插曲

首先,我们要明白,java中的数组是不支持向下转型的,但是如果本身就是那个类型的是可以转过去的,请看下面的例子:

  1. public static void main(String[] args) {
  2. Object[] objs = new Object[]{1};
  3. // 类型转换错误
  4. // Integer[] ins = (Integer[]) objs;
  5. Object[] objs2 = new Integer[]{1};
  6. // 不报错
  7. Integer[] ins2 = (Integer[]) objs2;
  8. }

类型擦除

java里的泛型是假泛型,只在编译期有效,在运行时是没有泛型的概念的,举个简单的例子:

  1. public static void main(String[] args) {
  2. List<String> strList = Arrays.asList("1");
  3. List<Integer> intList = Arrays.asList(1);
  4. // 打印:true
  5. System.out.println(strList.getClass() == intList.getClass());
  6. }

可以看到两个list的类型是一样的,如果你觉得这个例子不够说服力,那我给你个过分点的例子:

  1. public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
  2. List<String> strList = new ArrayList<>();
  3. Method addMethod = strList.getClass().getMethod("add", Object.class);
  4. addMethod.invoke(strList, 1);
  5. addMethod.invoke(strList, true);
  6. addMethod.invoke(strList, new Long(1));
  7. addMethod.invoke(strList, new Byte[]{1});
  8. // 打印:[1, true, 1, 1]
  9. System.out.println(strList);
  10. }

瞧,我可以往一个String类型的List中扔任何我想扔的东西,服不服?!

所以说java里面的泛型是假的,运行时不存在滴。

回归正题

数组不能向下强转我懂了,类型擦除我也懂了,似乎还是过不好这一生,呃不是,是还是解决不了这道题啊?

呃,好像是~~

我们再来看一个简单的例子:

  1. // GenericTest2.java(源码)
  2. public class GenericTest2 {
  3. public static void main(String[] args) {
  4. System.out.println(raw("1"));
  5. }
  6. public static <T> T raw(T t) {
  7. return t;
  8. }
  9. }
  10. // GenericTest2.class(反编译)
  11. public class GenericTest2 {
  12. public GenericTest2() {
  13. }
  14. public static void main(String[] args) {
  15. System.out.println((String)raw("1"));
  16. }
  17. public static <T> T raw(T t) {
  18. return t;
  19. }
  20. }

嗯~似乎看出来点端倪,反编译后多了个构造方法。

呃,没错。还有呢?

仔细一看,System.out.println((String)raw("1"));这一句多加了个String强转。

这就是关键所在,结合类型擦除,运行时并没有所谓的泛型,所以raw()返回的其实是Object,但是调用者自己知道我要的是String类型啊,所以我就知道强转一下喽。

我们再来看个极端的例子:

  1. // GenericTest2.java(源码)
  2. public class GenericTest2 {
  3. public static void main(String[] args) {
  4. System.out.println(raw("1"));
  5. }
  6. public static <T> T raw(T t) {
  7. return (T)new Integer(1);
  8. }
  9. }
  10. // GenericTest2.class(反编译)
  11. public class GenericTest2 {
  12. public GenericTest2() {
  13. }
  14. public static void main(String[] args) {
  15. System.out.println((String)raw("1"));
  16. }
  17. public static <T> T raw(T t) {
  18. return new Integer(1);
  19. }
  20. }

仔细观察,可以发现,raw()方法里的强转(T)new Integer(1)变成了new Integer(1),强转被擦除了,实际上在运行时这里的T变成了Object,所有类型都是Object的子类,也就不需要强转了。

(String)raw("1")的强转还是加上的,这是调用者知道类型是String,所以raw()返回后自己强转成String一下。

当然,这个代码运行是会报错的,java.lang.Integer cannot be cast to java.lang.String,因为raw()返回的是Integer类型,强转成String类型失败了。

好了,基本思路就是这样。

泛型类呢?

我们上面举的例子都是泛型方法,那么泛型类呢?

同样地,我们来看个例子:

  1. // GenericTest3.java(源码)
  2. public class GenericTest3 {
  3. public static void main(String[] args) {
  4. System.out.println(new Raw<String>().raw("1"));
  5. }
  6. }
  7. class Raw<T> {
  8. public T raw(T t) {
  9. return (T)new Integer(1);
  10. }
  11. }
  12. // GenericTest3.class(反编译)
  13. public class GenericTest3 {
  14. public GenericTest3() {
  15. }
  16. public static void main(String[] args) {
  17. System.out.println((String)(new Raw()).raw("1"));
  18. }
  19. }
  20. class Raw<T> {
  21. Raw() {
  22. }
  23. public T raw(T t) {
  24. return new Integer(1);
  25. }
  26. }

可以看到,跟泛型方法的表现一模一样。当然,这里运行时也会报java.lang.Integer cannot be cast to java.lang.String这个错误。

总结

java中的泛型只在编译期有效,在运行时只有调用者知道需要什么类型,且调用者调用泛型方法后自己做强制转换,被调用者是完全无感的。

所以,出现问题不要问被调用者,而是要问调用者,你丫是怎么调用的?!

解答开篇

为了方便我们还是把开篇的问题拿过来。

  1. // GenericTest.java(源码)
  2. public class GenericTest {
  3. //方法一
  4. public static <T extends Comparable<T>> List<T> sort(List<T> list) {
  5. return Arrays.asList(list.toArray((T[]) new Comparable[list.size()]));
  6. }
  7. //方法二
  8. public static <T extends Comparable<T>> T[] sort2(List<T> list) {
  9. // 这里没报错
  10. return list.toArray((T[]) new Comparable[list.size()]);
  11. }
  12. public static void main(String[] args) {
  13. List<Integer> list = new ArrayList<>();
  14. list.add(1);
  15. list.add(2);
  16. // 方法一调用正常
  17. System.out.println(sort(list).getClass());
  18. // 方法二调用报错了,这里报错了
  19. System.out.println(sort2(list).getClass());
  20. }
  21. }

这里似乎又不太一样,变成了<T extends Comparable<T>>,其实是一样的啦,如果单独写<T>是相当于<T extends Object>的。

那么,我们就延伸一下,被调用者是完全无感的,它只能尽力拿到它知道的类型,比如这里就只能尽力拿到Comparable,如果是<T>拿到的就是Object。

所以,方法二返回的就是实打实的Comparable[]类型,作为被调用者,它一点问题都没有。

但是,调用方是知道我需要的是Integer[]类型的,因为list里面是Integer类型,所以返回的应该是Integer[]类型,所以我就强转喽,然后就报错了。

到底是不是这样?我们来看看反编译后的代码:

  1. // GenericTest.class(反编译)
  2. public class GenericTest {
  3. public GenericTest() {
  4. }
  5. public static <T extends Comparable<T>> List<T> sort(List<T> list) {
  6. return Arrays.asList(list.toArray((Comparable[])(new Comparable[list.size()])));
  7. }
  8. public static <T extends Comparable<T>> T[] sort2(List<T> list) {
  9. // 这里使用的是Comparable[]强转,所以返回的也是实打实的Comparable[]类型
  10. return (Comparable[])list.toArray((Comparable[])(new Comparable[list.size()]));
  11. }
  12. public static void main(String[] args) {
  13. List<Integer> list = new ArrayList();
  14. list.add(1);
  15. list.add(2);
  16. System.out.println(sort(list).getClass());
  17. // 数组向下转型失败
  18. System.out.println(((Integer[])sort2(list)).getClass());
  19. }
  20. }

可以看到,跟我们的分析完全一致。

一句话,一辈子

java中的泛型只在编译期有效,在运行时只有调用者知道它自己需要什么类型,且调用者调用泛型方法后自己做强制转换,被调用者是完全无感的,被调用者只能尽力拿到它所知道的类型。

此时,我的脑海中不经响起那熟悉的旋律,“一句话,一辈子……”,今天的这句话你记住了吗?


一句话,讲清楚java泛型的本质(非类型擦除)的更多相关文章

  1. Java 泛型,你了解类型擦除吗?

    泛型,一个孤独的守门者. 大家可能会有疑问,我为什么叫做泛型是一个守门者.这其实是我个人的看法而已,我的意思是说泛型没有其看起来那么深不可测,它并不神秘与神奇.泛型是 Java 中一个很小巧的概念,但 ...

  2. 从头认识java-13.11 对照数组与泛型容器,观察类型擦除给泛型容器带来什么问题?

    这一章节我们继续类型擦除的话题,我们将通过对照数组与泛型容器,观察类型擦除给泛型容器带来什么问题? 1.数组 package com.ray.ch13; public class Test { pub ...

  3. JAVA泛型中的有界类型(extends super)(转)

    JDK1.5中引入了泛型(Generic)机制.泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数.这种参数类型可以用在类.接口和方法的创建中,分别称为泛型类.泛型接口.泛型方法. Ja ...

  4. Java泛型(11):潜在类型机制

    泛型的目标之一就是能够编写尽可能广泛应用的代码. 为了实现这一点,我们需要各种途径来放松对我们的代码将要作用的类型所做的限制,同时不丢失静态类型检查的好处.即写出更加泛化的代码. Java泛型看起来是 ...

  5. Java泛型之自限定类型

    在<Java编程思想>中关于泛型的讲解中,提到了自限定类型: class SelfBounded<T extends SelfBounded<T>> 作者说道: 这 ...

  6. java泛型-自定义泛型方法与类型推断总结

    下面是自定义泛型方法的练习: package com.mari.generic; import java.util.ArrayList; import java.util.Collection; im ...

  7. 关于JAVA泛型中的通配符类型

    之前对JAVA一知半解时就拿起weiss的数据结构开始看,大部分数据结构实现都是采取通配符的思想,好处不言而喻. 首先建立两个类employee和manager,继承关系如下.其次Pair类是一个简单 ...

  8. java反射之java 泛型的本质

    1.泛型 反射API用来生成在当前JAVA虚拟机中的类.接口或者对象的信息.Class类:反射的核心类,可以获取类的属性,方法等内容信息.Field类:Java.lang.reflect.表示类的属性 ...

  9. java泛型 8 泛型的内部原理:类型擦除以及类型擦除带来的问题

    参考:java核心技术 一.Java泛型的实现方法:类型擦除 前面已经说了,Java的泛型是伪泛型.为什么说Java的泛型是伪泛型呢?因为,在编译期间,所有的泛型信息都会被擦除掉.正确理解泛型概念的首 ...

随机推荐

  1. 译MassTransit 消息契约

    消息契约 在MassTransit中,使用.NET .NET系统定义消息契约.消息可以使用类和接口来定义,但是,建议类型使用只读属性而不使用行为. 注意:强烈建议使用消息接口的接口,基于多年的经验,具 ...

  2. Centos7下安装PHP5.5,5.6,7.0----(转载记录一下)

    由于centOS7 默认的php版本是5.4的,偏低,所以收录了一下怎样安装5.5/5.6/7.0版本 默认的版本太低了,手动安装有一些麻烦,想采用Yum安装的可以使用下面的方案: 1.检查当前安装的 ...

  3. Quartz简单案例

    需求需要开发一个每天定时推送消息给微信用户,第一次接触quartz,简单案例 1. 先编辑要执行的任务 测试类代码 package com.wqq.test.quartz; import org.sp ...

  4. BZOJ_1015_[JSOI2008]星球大战_并查集

    BZOJ_1015_[JSOI2008]星球大战_并查集 题意:很久以前,在一个遥远的星系,一个黑暗的帝国靠着它的超级武器统治者整个星系.某一天,凭着一个偶然的 机遇,一支反抗军摧毁了帝国的超级武器, ...

  5. SSH通过SSH代理连接到内网机器

    要解决的问题? 需要解决的问题:https://q.cnblogs.com/q/105319/ 简单来说就是本地机器通过一台公网机器SSH到公网机器后面的私网机器. 网络环境如下图:本地机器可访问代理 ...

  6. Helm学习笔记

    Helm学习笔记 Helm 是 Kubernetes 生态系统中的一个软件包管理工具.本文将介绍 Helm 中的相关概念和基本工作原理,并通过一个具体的示例学习如何使用 Helm 打包.分发.安装.升 ...

  7. 强化学习(十六) 深度确定性策略梯度(DDPG)

    在强化学习(十五) A3C中,我们讨论了使用多线程的方法来解决Actor-Critic难收敛的问题,今天我们不使用多线程,而是使用和DDQN类似的方法:即经验回放和双网络的方法来改进Actor-Cri ...

  8. netcore服务程序暴力退出导致的业务数据不一致的一种解决方案(优雅退出)

    一: 问题提出 现如今大家写的netcore程序大多部署在linux平台上,而且服务程序里面可能会做各种复杂的操作,涉及到多数据源(mysql,redis,kafka).成功部署成后台 进程之后,你以 ...

  9. Vue源码解析(二):数据驱动

    一.数据驱动: 数据驱动是vue.js最大的特点.在vue.js中,数据驱动就是当数据发生变化的时候,用户界面发生相应的变化,开发者不需要手动的去修改dom.数据驱动还有一部分是数据更新驱动视图变化. ...

  10. 在Unity中实现小地图(Minimap)

    小地图的基本概念众所周知,小地图(或雷达)是用于显示周围环境信息的.首先,小地图是以主角为中心的.其次,小地图上应该用图标来代替真实的人物模型,因为小地图通常很小,玩家可能无法看清真实的模型.大多数小 ...