一句话,讲清楚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());
    }
}

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

那么,我们就延伸一下,被调用者是完全无感的,它只能尽力拿到它知道的类型,比如这里就只能尽力拿到Comparable,如果是&lt;T&gt;拿到的就是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中的泛型只在编译期有效,在运行时只有调用者知道它自己需要什么类型,且调用者调用泛型方法后自己做强制转换,被调用者是完全无感的,被调用者只能尽力拿到它所知道的类型。

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



原文地址:https://blog.51cto.com/14267003/2378165

时间: 2024-10-15 20:25:20

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

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

这一章节我们继续类型擦除的话题,我们将通过对比数组与泛型容器,观察类型擦除给泛型容器带来什么问题? 1.数组 package com.ray.ch13; public class Test { public static void main(String[] args) { Fruit[] fruits = new Apple[5]; fruits[0] = new Apple(); fruits[1] = new Fuji(); fruits[2] = new Fruit(); } } cla

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

这一章节我们继续类型擦除的话题,我们将通过对照数组与泛型容器,观察类型擦除给泛型容器带来什么问题? 1.数组 package com.ray.ch13; public class Test { public static void main(String[] args) { Fruit[] fruits = new Apple[5]; fruits[0] = new Apple(); fruits[1] = new Fuji(); fruits[2] = new Fruit(); } } cla

一文讲解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 exten

泛型中的类型擦除

通过反射理解泛型的本质(类型擦除) Java中的泛型是通过类型擦除来实现的.所谓类型擦除,是指通过类型参数合并,将泛型类型实例关联到同一份字节码上.编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上.类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法. 下面通过两个例子来证明在编译时确实发生了类型擦除. 例1分别创建实际类型为String和Integer的ArrayList对象,通过getClass()方法获取两个实例的类,最后判断这个

java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题

java泛型(二).泛型的内部原理:类型擦除以及类型擦除带来的问题 参考:java核心技术 一.Java泛型的实现方法:类型擦除 前面已经说了,Java的泛型是伪泛型.为什么说Java的泛型是伪泛型呢?因为,在编译期间,所有的泛型信息都会被擦除掉.正确理解泛型概念的首要前提是理解类型擦出(type erasure). Java中的泛型基本上都是在编译器这个层次来实现的.在生成的Java字节码中是不包含泛型中的类型信息的.使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉.这个过程就称为类型

Java进阶(四)Java反射TypeToken解决泛型运行时类型擦除的问题解决

在开发时,遇到了下面这条语句,不懂,然习之. private List<MyZhuiHaoDetailModel> listLottery = new ArrayList<MyZhuiHaoDetailModel>(); Gson gson=new Gson(); JSONObject object=new JSONObject(callbackValue); listLottery =  gson.fromJson(object.getString("lists&quo

Java进阶 四 Java反射TypeToken解决泛型运行时类型擦除问题

在开发时,遇到了下面这条语句,不懂,然习之. private List<MyZhuiHaoDetailModel> listLottery = new ArrayList<MyZhuiHaoDetailModel>(); Gson gson=new Gson(); JSONObject object=new JSONObject(callbackValue); listLottery =  gson.fromJson(object.getString("lists&quo

Java 泛型(Generics) 综述

一. 引子 一般的类和方法,只能使用具体类型:要么是基本类型,要么是自定义类型.如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大. 多态算是一种泛化机制,但对代码的约束还是太强(要么继承父类,要么实现接口). 有许多原因促成了泛型的出现,而最引人注目的一个原因,就是为了创造容器类.(泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性) 例如,在 Java 实现加入泛型前,ArrayList 只维护一个 Object 类型的数组: publ

java 泛型 窜讲

一.为什么使用泛型      复用性:泛型的本质就是参数化类型,因而使用编写的泛型代码可以被许多不同类型的对象所复用.      安全性:在对类型Object引用的参数操作时,往往需要进行显式的强制类型转换.这种强制类型转换需要在运行时才能被发现是否转换异常,通过引入泛型能将在运行时才能检查类型转换,提前到编译时期就能检查. 二.自定义泛型 java中自定义泛型分为三种:泛型类.泛型接口.泛型方法. 下面使用一个案例演示泛型类.泛型方法,泛型接口类似,所以不再演示. // 自定义泛型类publi