简单理解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中的泛型只在编译期有效,在运行时只有调用者知道它自己需要什么类型,且调用者调用泛型方法后自己做强制转换,被调用者是完全无感的,被调用者只能尽力拿到它所知道的类型。

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java构造函数的相互调用代码示例

    在Java中,当为一个类创建了多个构造函数时,有时想在一个构造函数中调用另一个构造函数以减少代码量.这时可以使用this关键字来实现. 有关构造函数的相关内容,大家可以参阅:Java编程中的构造函数详细介绍 通常,当使用this关键字时,它意味着"这个对象"或者"当前对象",并且它自身产生对当前对象的引用.在一个构造函数中,当给传递给它一个参数列表时,它就有了不同的意义. 它将直接的调用能够匹配这个参数列表的构造函数.因此,我么可以直接的调用其它构造函数: pack

  • 不同Java泛型构造函数的详解

    1.概述 我们之前讨论过Java Generics的基础知识.在本文中,我们将了解Java中的通用构造函数. 泛型构造函数是至少需要有一个泛型类型参数的构造函数.我们将看到泛型构造函数并不都是在泛型类中出现的,而且并非所有泛型类中的构造函数都必须是泛型. 2.非泛型类 首先,先写一个简单的类:Entry,它不是泛型类: public class Entry { private String data; private int rank; } 在这个类中,我们将添加两个构造函数:一个带有两个参数的

  • Java中泛型总结(推荐)

    Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型. 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数. 泛型类 范例:泛型类的基本语法 class MyClass<T> { T value1; } 尖括号 <> 中的 T 被称作是类型参数,用于指代任何类型.实际上这个T你可以任意写,但出于规范的目的,Java还是建议我们用单个大写字母来代表类型参数.常见的如: T 代表

  • 深入理解Java中的构造函数引用和方法引用

    JDK 8 见证了一个特殊特性的出现:构造函数引用和方法引用.在本文中, Adrian D. Finlay 探讨了开发人员如何释放构造函数引用的真正潜力. 方法引用的一些背景 如果你还不知道 Java 构造函数本身就是特殊的方法,那么阅读方法引用的基本示例将对读者有所帮助,通过了解这些内容,可以了解构造函数引用是什么. 「方法引用为已经有名称的方法提供易读的 lambda 表达式.」 「它们提供了一种无需执行就可以引用方法的简单方式.」 以上引自<Java 8 编程参考官方教程(第 9 版)>

  • Java构造函数与普通函数用法详解

    函数也被称为方法! 函数的作用及特点: 1.用于定义功能,将功能封装. 2.可以提高代码的复用性. 函数注意事项: 1.不能进行函数套用(不可以在函数内定义函数). 2.函数只有被调用才能被执行. 3.基本数据类型(String.int.-.)修饰的函数类型,要有return返回值. 4.void修饰的函数,函数中的return语句可以省略不写. 5.函数名可以根据需求进行命名. 代码示例:(有无函数/方法的区别) 无函数/方法代码例子: public class NoFunc { public

  • 详解Java泛型及其应用

    引出泛型 我们通过如下的示例,引出为什么泛型的概念. public class Test { public static void main(String[] args) { List list = new ArrayList(); list.add("abc"); list.add(2); for (int i = 0; i < list.size(); i++) { String name = (String) list.get(i); // error System.out

  • java泛型基本知识及通用方法

    泛型的基本使用 泛型是Java SE 1.5的新特性, 泛型的本质是参数化类型, 也就是说所操作的数据类型被指定为一个参数. 这种参数类型可以用在类.接口和方法的创建中, 分别称为泛型类.泛型接口.泛型方法.  Java语言引入泛型的好处是安全简单. 今天就从以下几个方面介绍一下java的泛型: 基础, 泛型关键字, 泛型方法, 泛型类和接口. 基础: 通过集合的泛型了解泛型的基本使用 public void testBasis(){ List<String> list = new Array

  • 简单理解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 Compara

  • 重新理解Java泛型

    这篇文章的目的在于介绍Java泛型,使大家对Java泛型的各个方面有一个最终的,清晰的,准确的理解,同时也为下一篇<重新理解Java反射>打下基础. 简介 泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用.本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理,以及让人苦恼的类型擦除. 泛型基础 泛型类 我们首先定义一个简单的Box类: public class Box { private String object; public void set(St

  • 深入理解java泛型Generic

    1. 背景 泛型技术诞生之前(JDK5以前),创建集合的类型都是Object 类型的元素,存储内容没有限制,编译时正常,运行时容易出现ClassCastException 异常. public class Test { public static void main(String[] args) { ArrayList list = new ArrayList(); list.add("java"); list.add(100); list.add(true); for(int i =

  • 深入浅出理解Java泛型的使用

    目录 一.泛型的意义 二.泛型的使用 三.自定义泛型类 1.关于自定义泛型类.泛型接口: 2.泛型在继承方面的体现 3.通配符的使用 一.泛型的意义 二.泛型的使用 1.jdk 5.0新增特性 2.在集合中使用泛型: 总结: A.集合接口或集合类在jdk5.0时都修改为带泛型的结构. B.在实例化集合类时,可以指明具体的泛型类型. C.指明完以后,在集合类或接口中凡是定义类或接口时,内部结构(比如:方法.构造器.属性等)使用类的泛型的位置, 都指定为实例化的泛型类型.比如:add(E e) --

  • 深入理解java泛型详解

    什么是泛型? 泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样. 可以在集合框架(Collection framework)中看到泛型的动机.例如,Map 类允许您向一个 Map 添加任意类的对象,即使最常见的情况是在给定映射(map)中保存某个特定类型(比如 String)的对象. 因为 Map.get(

  • 通过代码理解java泛型

    泛型数据java基础,但真正理解需要悉心品尝.毕竟在工作中用到的是在是太多了. 不要以为new ArrayList<>这就是泛型,这只能属于会使用. 在工作中,相对于现有的项目源码的数据库操作层,无论是mybatis,hibernate或者是自己封装的baseModel层,都会使用到泛型. 以及<? extends T> 和 <? super T>这个屌东西. 泛型使用情况分为三类 1. 泛型类. 2. 泛型方法. 3. 泛型接口. 出于规范的目的,Java 还是建议我

  • 简单理解Java的垃圾回收机制与finalize方法的作用

    垃圾回收器要回收对象的时候,首先要调用这个类的finalize方法(你可以 写程序验证这个结论),一般的纯Java编写的Class不需要重新覆盖这个方法,因为Object已经实现了一个默认的,除非我们要实现特殊的功能(这 里面涉及到很多东西,比如对象空间树等内容). 不过用Java以外的代码编写的Class(比如JNI,C++的new方法分配的内存),垃圾回收器并不能对这些部分进行正确的回收,这时就需要我们覆盖默认的方法来实现对这部分内存的正确释放和回收(比如C++需要delete). 总之,f

  • 简单理解Java的抽象类

    在自上而下的继承层次结构中,位于上层的类更具有通用性,甚至可能更加抽象.从某种角度看,祖先类更加通用,它只包含一些最基本的成员,人们只将它作为派生其他类的基类,而不会用来创建对象.甚至,你可以只给出方法的定义而不实现,由子类根据具体需求来具体实现. 这种只给出方法定义而不具体实现的方法被称为抽象方法,抽象方法是没有方法体的,在代码的表达上就是没有"{}".包含一个或多个抽象方法的类也必须被声明为抽象类. 使用 abstract 修饰符来表示抽象方法和抽象类. 抽象类除了包含抽象方法外,

  • Java 泛型总结(一):基本用法与类型擦除

    简介 Java 在 1.5 引入了泛型机制,泛型本质是参数化类型,也就是说变量的类型是一个参数,在使用时再指定为具体类型.泛型可以用于类.接口.方法,通过使用泛型可以使代码更简单.安全.然而 Java 中的泛型使用了类型擦除,所以只是伪泛型.这篇文章对泛型的使用以及存在的问题做个总结,主要参考自 <Java 编程思想>. 这个系列的另外两篇文章: Java 泛型总结(二):泛型与数组 Java 泛型总结(三):通配符的使用 基本用法 泛型类 如果有一个类 Holder 用于包装一个变量,这个变

  • Java的类型擦除式泛型详解

    Java选择的泛型类型叫做类型擦除式泛型.什么是类型擦除式泛型呢?就是Java语言中的泛型只存在于程序源码之中,在编译后的字节码文件里,则全部泛型都会被替换为原来的原始类型(Raw Type),并且会在相应的地方插入强制转型的代码. 因此,对于运行期间的Java程序来说ArrayList< Integer>和ArrayList< String>其实是同一个类型.这也就是Java选择的泛型类型叫做类型擦除式泛型的原因. ArrayList<String> stringAr

随机推荐