一句话,讲清楚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中的泛型只在编译期有效,在运行时只有调用者知道它自己需要什么类型,且调用者调用泛型方法后自己做强制转换,被调用者是完全无感的,被调用者只能尽力拿到它所知道的类型。
此时,我的脑海中不经响起那熟悉的旋律,“一句话,一辈子……”,今天的这句话你记住了吗?

一句话,讲清楚java泛型的本质(非类型擦除)的更多相关文章
- Java 泛型,你了解类型擦除吗?
泛型,一个孤独的守门者. 大家可能会有疑问,我为什么叫做泛型是一个守门者.这其实是我个人的看法而已,我的意思是说泛型没有其看起来那么深不可测,它并不神秘与神奇.泛型是 Java 中一个很小巧的概念,但 ...
- 从头认识java-13.11 对照数组与泛型容器,观察类型擦除给泛型容器带来什么问题?
这一章节我们继续类型擦除的话题,我们将通过对照数组与泛型容器,观察类型擦除给泛型容器带来什么问题? 1.数组 package com.ray.ch13; public class Test { pub ...
- JAVA泛型中的有界类型(extends super)(转)
JDK1.5中引入了泛型(Generic)机制.泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数.这种参数类型可以用在类.接口和方法的创建中,分别称为泛型类.泛型接口.泛型方法. Ja ...
- Java泛型(11):潜在类型机制
泛型的目标之一就是能够编写尽可能广泛应用的代码. 为了实现这一点,我们需要各种途径来放松对我们的代码将要作用的类型所做的限制,同时不丢失静态类型检查的好处.即写出更加泛化的代码. Java泛型看起来是 ...
- Java泛型之自限定类型
在<Java编程思想>中关于泛型的讲解中,提到了自限定类型: class SelfBounded<T extends SelfBounded<T>> 作者说道: 这 ...
- java泛型-自定义泛型方法与类型推断总结
下面是自定义泛型方法的练习: package com.mari.generic; import java.util.ArrayList; import java.util.Collection; im ...
- 关于JAVA泛型中的通配符类型
之前对JAVA一知半解时就拿起weiss的数据结构开始看,大部分数据结构实现都是采取通配符的思想,好处不言而喻. 首先建立两个类employee和manager,继承关系如下.其次Pair类是一个简单 ...
- java反射之java 泛型的本质
1.泛型 反射API用来生成在当前JAVA虚拟机中的类.接口或者对象的信息.Class类:反射的核心类,可以获取类的属性,方法等内容信息.Field类:Java.lang.reflect.表示类的属性 ...
- java泛型 8 泛型的内部原理:类型擦除以及类型擦除带来的问题
参考:java核心技术 一.Java泛型的实现方法:类型擦除 前面已经说了,Java的泛型是伪泛型.为什么说Java的泛型是伪泛型呢?因为,在编译期间,所有的泛型信息都会被擦除掉.正确理解泛型概念的首 ...
随机推荐
- python反编译工具
开发类在线工具:https://tool.lu/一个反编译网站:https://tool.lu/pyc/ 一看这个标题,就是搞坏事用的, 用 java 写程序多了,很习惯用反编译工具了,而且玩java ...
- Go性能优化小结
1 内存优化 1.1 小对象合并成结构体一次分配,减少内存分配次数 做过C/C++的同学可能知道,小对象在堆上频繁地申请释放,会造成内存碎片(有的叫空洞),导致分配大的对象时无法申请到连续的内存空间, ...
- bzoj3631[JLOI2014 松鼠的新家 倍增lca+差分
裸的树上差分+倍增lca 每次从起点到终点左闭右开,这就有一个小技巧,要找到右端点向左端点走的第一步,然后差分就好了 #include<cstdio> #include<cstrin ...
- BZOJ_3223: Tyvj 1729 文艺平衡树 _splay
题意: 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1 分析: ...
- AMBA总线协议AHB、APB
一.什么是AMBA总线 AMBA总线规范是ARM公司提出的总线规范,被大多数SoC设计采用,它规定了AHB (Advanced High-performance Bus).ASB (Advanced ...
- ceph osd 自动挂载的N种情况
直接上干货: ceph自动挂载原理 系统启动后,ceph 通过扫描所有磁盘及分区的 ID_PART_ENTRY_TYPE 与自己main.py中写死的osd ready 标识符来判断磁盘(及其分区)是 ...
- 阿里开源分布式事务解决方案 Fescar
微服务倡导将复杂的单体应用拆分为若干个功能简单.松耦合的服务,这样可以降低开发难度.增强扩展性.便于敏捷开发.当前被越来越多的开发者推崇,系统微服务化后,一个看似简单的功能,内部可能需要调用多个服务并 ...
- Spark学习之编程进阶总结(一)
一.简介 这次介绍前面没有提及的 Spark 编程的各种进阶特性,会介绍两种类型的共享变量:累加器(accumulator)与广播变量(broadcast variable).累加器用来对信息进行聚合 ...
- 【view绘制流程】理解
一.概述 View的绘制是从上往下一层层迭代下来的.DecorView-->ViewGroup(--->ViewGroup)-->View ,按照这个流程从上往下,依次measure ...
- Java进阶篇设计模式之五-----外观模式和装饰器模式
前言 在上一篇中我们学习了结构型模式的适配器模式和桥接模式.本篇则来学习下结构型模式的外观模式和装饰器模式. 外观模式 简介 外观模式隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口.这 ...