前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一句话,讲清楚java泛型的本质(非类型擦除)

一句话,讲清楚java泛型的本质(非类型擦除)

作者头像
彤哥
发布2019-07-08 15:01:22
4740
发布2019-07-08 15:01:22
举报
文章被收录于专栏:彤哥读源码彤哥读源码

背景

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

代码语言:javascript
复制
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());这行,而不是 returnlist.toArray((T[])newComparable[list.size()]);这行;

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

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

解决

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

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

小插曲

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

代码语言:javascript
复制
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里的泛型是假泛型,只在编译期有效,在运行时是没有泛型的概念的,举个简单的例子:

代码语言:javascript
复制
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的类型是一样的,如果你觉得这个例子不够说服力,那我给你个过分点的例子:

代码语言:javascript
复制
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里面的泛型是假的,运行时不存在滴。

回归正题

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

呃,好像是~~

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

代码语言:javascript
复制
// 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类型啊,所以我就知道强转一下喽。

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

代码语言:javascript
复制
// 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)newInteger(1)变成了 newInteger(1),强转被擦除了,实际上在运行时这里的T变成了Object,所有类型都是Object的子类,也就不需要强转了。

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

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

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

泛型类呢?

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

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

代码语言:javascript
复制
// 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.Integercannot be cast to java.lang.String这个错误。

总结

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

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

解答开篇

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

代码语言:javascript
复制
// 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>是相当于 <TextendsObject>的。

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

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

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

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

代码语言:javascript
复制
// 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());    }}

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-04-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 彤哥读源码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 解决
    • 小插曲
      • 类型擦除
        • 回归正题
          • 泛型类呢?
          • 总结
            • 解答开篇
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档