Java中泛型区别以及泛型擦除详解

一、引言

  复习javac的编译过程中的解语法糖的时候看见了泛型擦除中的举例,网上的资料大多比较散各针对性不一,在此做出自己的一些详细且易懂的总结。

二、泛型简介

  泛型是JDK 1.5的一项新特性,一种编译器使用的范式,语法糖的一种,能保证类型安全。【注意:继承中,子类泛型数必须不少于父类泛型数】

  为了方便理解,我将泛型分为普通泛型通配泛型

三、泛型分类

1、普通泛型  

  就是没有设置通配的泛型,泛型表示为某一个类。

  声明时: class Test<T>{...}

  使用时: Test<Integer> test = new Test<Integer>();

  作为无界泛型,其实就是约束左右泛型必须一致。

2、通配泛型

  通配泛型包括两种,无界通配和有界通配。

  【无界通配符】

  <?>通配符——表示所有类型都能与它匹配

  【有界通配符】

  extends(上界)通配符——声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类;

  super(下界)通配符——声明了类型的下界,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至Object。类型擦除后剩下(下面以extends举例)

//有界泛型类型语法 - 继承自某父类
<T extends ClassA>
//有界泛型类型语法 - 实现某接口
<T extends InterfaceB>
//有界泛型类型语法 - 多重边界
<T extends ClassA & InterfaceB & InterfaceC ... >

//示例
<N extends Number> //N标识一个泛型类型,其类型只能是Number抽象类的子类
<T extends Number & Comparable & Map> //T标识一个泛型类型,其类型只能是Person类型的子类,并且实现了Comparable 和Map接口

【注意:多重边界里,只允许第一个能为类,后续必须为接口】

四、List<T> 和 List<?> 和 List<Object> 的区别

  类声明的时候采用List<T>,此时T可以为任何字母,都指代普通泛型。

    例如: class Test<T>{...}

  实例化的时候采用List<?>,此时?可以为任何类,表示只能存入此类对象,也可以就写‘<?>’,代表可以存入任何类对象,属于通配泛型。

    例如:List<?> listOfString = new ArrayList<String>;

  但是注意List<?>与List<Object>不一样,前者是所有泛型的通配符,即所有泛型的引用都能与他进行匹配(作为实例化的右边),而Object只是一个单独的类,当为实例化左边的时候,有且仅有为<Object>相匹配(或者不写泛型),例如:List<Object> list = new ArrayList<Object>();,实例化左边的泛型作为“答案范围”,实例化右边的泛型只能为“答案”是某一个类。例如:

List<String> list = new ArrayList<?>(); // 编译错误:通配符是“答案范围”不能作为“答案”出现在实例化的右边
List<?> list = new ArrayList<String>(); // String与?匹配成功
List<? extends Number> list = new ArrayList<? extends Integer>(); // 编译错误:有界泛型同样也是“答案范围”,不能出现在实例化的右边
List<? extends Number> list = new ArrayList<Integer>(); // 右边的"答案"与左边的“答案范围”匹配成功

五、泛型擦除

 由来:一开始java并没有泛型,后来1.5加入了泛型,为了能向前兼容(旧版本的jvm能解释运行新版本的.class文件)所以就采用了伪泛型——“泛型擦除”,并一直保留了下来。

  原理:泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,擦除后会变成原始类型(去掉<T>,将方法内的T擦除成Object)例如Generic<T>会被擦除成Generic。还需要注意的是,不同的通配符的擦除的方式也有不同:

  口诀:【传入:取下界;返回:取上界】

  当泛型作为方法的传入参数的时候,此时替换成通配泛型的下界,例如add方法

  当泛型作为方法的返回参数的时候,此时替换成通配泛型的上界,例如get方法

List<? extends Integer> list1 = new ArrayList<Integer>();
list1.add(null); // 此时传入取<? extends Integer> 下界————无 所以只能传null,否则报错
Integer integer1 =  list1.get(0); // // 此时返回取<? extends Integer> 上界————Integer

List<? super Integer> list2 = new ArrayList<Integer>();
list2.add(111); // 此时传入取<? super Integer> 下界——————Integer
Integer integer2 =  (Integer) list2.get(0); // // 此时返回取<? super Integer> 上界————Object

  所以同理可得,当泛型为<?>的时候,取下界是null,取上界是Object。

  所以得出结论,因为add和get方法的擦除的限制,尽量少使用通配泛型

  泛型擦除有什么隐患,有什么解决方法

  1、如果不加泛型继承,擦除后会变成原始类型,所以能加入非泛型的类型。 List<Integer> list = new ArrayList<Integer>(); list.add("呀哈");

    并且能完成不同泛型之间的引用传递。 List<String> list = new ArrayList<Integer>();

    以上两种情况怎么解决?      

    ——java编译器是通过先检查代码中泛型的类型,如果出现上面两种情况则会在编译期报错,检查通过后,然后再进行类型擦除,再进行编译。

    【注意:先检查实例化左右泛型是否匹配,然后以实例化的左边的泛型为基准对添加元素进行检查(所以,只在右边写泛型和没写是一个意思)】例如:

List<String> list1 = new ArrayList<String>(); // 此时按照String检查泛型
List list2 = new ArrayList<String>();  // 此时不检查泛型

  2、擦除后泛型信息就没了,获取的时候再强转?

    ——泛型补偿:在泛型检查的保证下,存入的都是符合泛型的对象,编译期间利用反射获取元素对象的类型(getClass()方法)对要传出元素进行强转。

  3、子类继承泛型方法,然后对其重写并将泛型改成真实类型,但是在擦除之后原来父类的泛型方法会变成Object方法,变为两个不同的方法,这样一来此方法就不是继承重写,而是子类的重载了。如下面代码所示:

class Node<T> {
    public void setData(T data) {
        System.out.println("Node.setData");
    }
}
class MyNode<T> extends Node<Integer> {
    public void setData(Integer data) {
        System.out.println("MyNode.setData:"+data);
    }
} 
Node<Integer> n = new MyNode<Integer>();
n.setData(1213); // 如果是擦除后的Object方法则会执行父类的方法,打印出“Node.setData”

    运行结果: MyNode.setData:1213

   可见执行的却是子类的方法?完成了多态的实现。这是怎么解决的?

    ——桥方法:顾名思义,因为擦除之后子类中方法的参数列表与父类参数列表不同,不能形成重写,所以编译器在编译的时候,擦除后往子类中插入一些方法用来重载父类中的所有泛型擦除之后的Object方法,并在方法内部调用相对应的子类方法,以此重新形成父子之间多态,这些方法被称为桥方法。(下面是编译器擦除编译之后的内容)

class Node {
    public void setData(Object data) {
        System.out.println("Node.setData");
    }
}
class MyNode extends Node {
    // 编译器生成的桥方法
    public void setData(Object data) {
       setData((Integer) data);
    }
    public void setData(Integer data) {
        System.out.println("MyNode.setData:"+data);
    }
}

原文地址:https://www.cnblogs.com/Xieyang-blog/p/9215934.html

时间: 2024-08-03 14:57:13

Java中泛型区别以及泛型擦除详解的相关文章

Java中堆内存和栈内存详解2

Java中堆内存和栈内存详解 Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用. 堆内存用于存放由new创建的对象和数组.在堆中分配的内存,由java虚拟机自动垃圾回收器来管理.在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的

Java中List,Set和Map详解及其区别

Java中的集合包括三大类,它们是Set(集).List(列表)和Map(映射),它们都处于java.util包中,Set.List和Map都是接口,它们有各自的实现类.Set的实现类主要有HashSet和TreeSet,List的实现类主要有ArrayList,Map的实现类主要有HashMap和TreeMap. Collection是最基本的集合接口,声明了适用于JAVA集合的通用方法,list和set都继承自collection接口. Collection接口的方法 boolean add

java中的String类常量池详解

test1: package StringTest; public class test1 { /** * @param args */ public static void main(String[] args){ String a = "a1"; String b = "a"+ 1; System.out.println(a==b); }//true } test2: package StringTest; public class test2 { /** *

深入理解Java中的流---结合Hadoop进行详解

在JavaSe的基础课程当中,可以说流是一个非常重要的概念,并且在Hadoop中得到了广泛的应用,本篇博客将围绕流进行深入的详解. (一)JavaSe中流的相关概念 1.流的定义 ①在Java当中,若一个类专门用于数据传输,则这个类称为流 ②流就是程序和设备之间嫁接以来的一根用于数据传输的管道,这个设备可以是本地硬盘,可以是内存条,也可以是网络所关联的另外一台计算机等等,其中不同管道上有不同的按钮,按下不同的按钮相当于调用不同的方法,这根带按钮的用于数据传输的管道就是流,即流就是一根管道 ③流一

Java中堆内存和栈内存详解

Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用. 堆内存用于存放由new创建的对象和数组.在堆中分配的内存,由java虚拟机自动垃圾回收器来管理.在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中

Java中堆内存和栈内存详解【转】

Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用. 堆内存用于存放由new创建的对象和数组.在堆中分配的内存,由java虚拟机自动垃圾回收器来管理.在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中

java中堆内存和栈内存详解(转)

Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用. 堆内存用于存放由new创建的对象和数组.在堆中分配的内存,由java虚拟机自动垃圾回收器来管理.在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中

java中的数组类与集合类详解及原理介绍

一.类结构概述 当需要存储大量数据对象时,需要用到数组类或者集合类.java中的类结构如下(红色为接口,蓝色为类): Iterator接口:是对collection进行迭代的迭代器,它允许调用者利用定义良好的语义在迭代期间从迭代器所指向的collection移除元素. Collection接口:Collection表示一组对象,最小存储数据颗粒是单一的 List接口:是数组形式,允许数据重复:是有序的 collection(也称为序列),此接口的用户可以对列表中每个元素的插入位置进行精确地控制,

Java中的equals和hashCode方法详解

Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要重写这两个方法,今天就来介绍一些这两个方法的作用. equals()和hashCode()方法是用来在同一类中做比较用的,尤其是在容器里如set存放同一类对象时用来判断放入的对象是否重复. 这里我们首先要明白一个问题: equals()相等的两个对象,hashcode()一定相等,equals()不相等的两个对象,却并不能证明他们的hashcode()不相等.换

Java中自动装箱与拆箱详解

在讲装箱与拆箱之前我们要先了解一下这个问题的来源: Java中的类型分为基本类型(Primitive type)和类类型(Class type)两种: 基本类型包括byte型.char型.short型.int型.long型.float型.double型.boolean型八种.基本类型指的是直接包含值得类型,可提供语言级别的支持. 类类型包括Byte.Character.Short.Integer.Long.Float.Double.Boolean.为什么要用类类型呢?原因其实很简单,因为Java