一句话,讲清楚java泛型的本质(非类型擦除)
背景
昨天,在逛论坛时遇到个这么个问题,上代码:
public class GenericTest { //方法一 public static <T extends Comparable<T>> List<T> sort(List<T> list) { return Arrays.asList(list.toArray((T[]) new Comparable[list.size()])); } //方法二 public static <T extends Comparable<T>> T[] sort2(List<T> list) { // 这里没报错 return list.toArray((T[]) new Comparable[list.size()]); } public static void main(String[] args) { List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); // 方法一调用正常 System.out.println(sort(list).getClass()); // 方法二调用报错了,这里报错了 System.out.println(sort2(list).getClass()); } }
这个问题有以下四个现象:
(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中的数组是不支持向下转型的,但是如果本身就是那个类型的是可以转过去的,请看下面的例子:
public static void main(String[] args) { Object[] objs = new Object[]{1}; // 类型转换错误 // Integer[] ins = (Integer[]) objs; Object[] objs2 = new Integer[]{1}; // 不报错 Integer[] ins2 = (Integer[]) objs2; }
类型擦除
java里的泛型是假泛型,只在编译期有效,在运行时是没有泛型的概念的,举个简单的例子:
public static void main(String[] args) { List<String> strList = Arrays.asList("1"); List<Integer> intList = Arrays.asList(1); // 打印:true System.out.println(strList.getClass() == intList.getClass()); }
可以看到两个list的类型是一样的,如果你觉得这个例子不够说服力,那我给你个过分点的例子:
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { List<String> strList = new ArrayList<>(); Method addMethod = strList.getClass().getMethod("add", Object.class); addMethod.invoke(strList, 1); addMethod.invoke(strList, true); addMethod.invoke(strList, new Long(1)); addMethod.invoke(strList, new Byte[]{1}); // 打印:[1, true, 1, 1] System.out.println(strList); }
瞧,我可以往一个String类型的List中扔任何我想扔的东西,服不服?!
所以说java里面的泛型是假的,运行时不存在滴。
回归正题
数组不能向下强转我懂了,类型擦除我也懂了,似乎还是过不好这一生,呃不是,是还是解决不了这道题啊?
呃,好像是~~
我们再来看一个简单的例子:
// GenericTest2.java(源码) public class GenericTest2 { public static void main(String[] args) { System.out.println(raw("1")); } public static <T> T raw(T t) { return t; } } // GenericTest2.class(反编译) public class GenericTest2 { public GenericTest2() { } public static void main(String[] args) { System.out.println((String)raw("1")); } public static <T> T raw(T t) { return t; } }
嗯~似乎看出来点端倪,反编译后多了个构造方法。
呃,没错。还有呢?
仔细一看,System.out.println((String)raw("1"));
这一句多加了个String强转。
这就是关键所在,结合类型擦除,运行时并没有所谓的泛型,所以raw()返回的其实是Object,但是调用者自己知道我要的是String类型啊,所以我就知道强转一下喽。
我们再来看个极端的例子:
// GenericTest2.java(源码) public class GenericTest2 { public static void main(String[] args) { System.out.println(raw("1")); } public static <T> T raw(T t) { return (T)new Integer(1); } } // GenericTest2.class(反编译) public class GenericTest2 { public GenericTest2() { } public static void main(String[] args) { System.out.println((String)raw("1")); } public static <T> T raw(T t) { return new Integer(1); } }
仔细观察,可以发现,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类型失败了。
好了,基本思路就是这样。
泛型类呢?
我们上面举的例子都是泛型方法,那么泛型类呢?
同样地,我们来看个例子:
// GenericTest3.java(源码) public class GenericTest3 { public static void main(String[] args) { System.out.println(new Raw<String>().raw("1")); } } class Raw<T> { public T raw(T t) { return (T)new Integer(1); } } // GenericTest3.class(反编译) public class GenericTest3 { public GenericTest3() { } public static void main(String[] args) { System.out.println((String)(new Raw()).raw("1")); } } class Raw<T> { Raw() { } public T raw(T t) { return new Integer(1); } }
可以看到,跟泛型方法的表现一模一样。当然,这里运行时也会报java.lang.Integer cannot be cast to java.lang.String
这个错误。
总结
java中的泛型只在编译期有效,在运行时只有调用者知道需要什么类型,且调用者调用泛型方法后自己做强制转换,被调用者是完全无感的。
所以,出现问题不要问被调用者,而是要问调用者,你丫是怎么调用的?!
解答开篇
为了方便我们还是把开篇的问题拿过来。
// GenericTest.java(源码) public class GenericTest { //方法一 public static <T extends Comparable<T>> List<T> sort(List<T> list) { return Arrays.asList(list.toArray((T[]) new Comparable[list.size()])); } //方法二 public static <T extends Comparable<T>> T[] sort2(List<T> list) { // 这里没报错 return list.toArray((T[]) new Comparable[list.size()]); } public static void main(String[] args) { List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); // 方法一调用正常 System.out.println(sort(list).getClass()); // 方法二调用报错了,这里报错了 System.out.println(sort2(list).getClass()); } }
这里似乎又不太一样,变成了<T extends Comparable<T>>
,其实是一样的啦,如果单独写<T>
是相当于<T extends Object>
的。
那么,我们就延伸一下,被调用者是完全无感的,它只能尽力拿到它知道的类型,比如这里就只能尽力拿到Comparable,如果是<T>
拿到的就是Object。
所以,方法二返回的就是实打实的Comparable[]类型,作为被调用者,它一点问题都没有。
但是,调用方是知道我需要的是Integer[]类型的,因为list里面是Integer类型,所以返回的应该是Integer[]类型,所以我就强转喽,然后就报错了。
到底是不是这样?我们来看看反编译后的代码:
// GenericTest.class(反编译) public class GenericTest { public GenericTest() { } public static <T extends Comparable<T>> List<T> sort(List<T> list) { return Arrays.asList(list.toArray((Comparable[])(new Comparable[list.size()]))); } public static <T extends Comparable<T>> T[] sort2(List<T> list) { // 这里使用的是Comparable[]强转,所以返回的也是实打实的Comparable[]类型 return (Comparable[])list.toArray((Comparable[])(new Comparable[list.size()])); } public static void main(String[] args) { List<Integer> list = new ArrayList(); list.add(1); list.add(2); System.out.println(sort(list).getClass()); // 数组向下转型失败 System.out.println(((Integer[])sort2(list)).getClass()); } }
可以看到,跟我们的分析完全一致。
一句话,一辈子
java中的泛型只在编译期有效,在运行时只有调用者知道它自己需要什么类型,且调用者调用泛型方法后自己做强制转换,被调用者是完全无感的,被调用者只能尽力拿到它所知道的类型。
此时,我的脑海中不经响起那熟悉的旋律,“一句话,一辈子……”,今天的这句话你记住了吗?
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
tensorflow 之循环神经网络
应用场景: 应用于语音识别 语音翻译 机器翻译 RNN RNN(Recurrent Neural Networks,循环神经网络)不仅会学习当前时刻的信息,也会依赖之前的序列信息。 由于其特殊的网络模型结构解决了信息保存的问题。所以RNN对处理时间序列和语言文本序列问题有独特的优势。递归神经网络都具有一连串重复神经网络模块的形式。在标准的RNNs中,这种重复模块有一种非常简单的结构。 那么S(t+1) = tanh( UX(t+1) + WS(t))。tanh激活函数图像如下: 激活函数tanh把状态S值映射到-1和1之间. RNN通过BPTT算法反向传播误差,它与BP相似,只不过与时间有关。RNN同样通过随机梯度下降(Stochastic gradient descent)算法使得代价函数(损失函数)值达到最小。 但是随着时间间隔不断增大时,RNN会丧失学习到连接很远的信息能力(梯度消失)。原因如下: RNN的激活函数tanh可以将所有值映射到-1至1之间,以及在利用梯度下降算法调优时利用链式法则,那么会造成很多个小于1的项连乘就很快的逼近零。 依赖于我们的激活函数和网络参数,也可能...
- 下一篇
关于写作那些事之利用 js 统计各大博客阅读量
在日常文章数据统计的过程中,纯手动方式已经难以应付,于是乎,逐步开始了程序介入方式进行统计. 在上一节中,探索利用 csv 文件格式进行文章数据统计,本来以为能够应付一阵子,没想到仅仅一天我就放弃了. 原因还不是因为我懒,需要复制文章内容,然后整理成特定的 csv 格式,最后利用已编写的 java 工具类进行统计. 在这三步操作中,第一步复制文章内容最简单,第二步整理文章格式最麻烦,第三步编写 csv 工具类最技术. 因此,能不能再简单点?懒癌晚期,必须继续寻求新的解决方案. 关于如何利用 csv 文件处理统计数据,可以参考 https://snowdreams1006.github.io/static-semi-manual-with-csv.html 实现效果 慕课手记 慕课手记 : https://www.imooc.com/u/5224488/articles 简书 简书 : https://www.jianshu.com/u/577b0d76ab87 博客园 博客园 : https://www.cnblogs.com/snowdreams1006/ 腾讯云社区 腾讯云社区 :...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2更换Tomcat为Jetty,小型站点的福音