前言

Java 泛型(Generic)的引入加强了参数类型的安全性,减少了类型的转换,但有一点需要注意:Java 的泛型在编译器有效,在运行期被删除,也就是说所有泛型参数类型在编译后都会被清除掉,看下面一个列子,代码如下:

public class Foo {

public void listMethod(List stringList){

}

public void listMethod(List intList) {

}

}

代码很简单,看起来没什么问题,但是编译器却报出如下错误信息:

Method listMethod(List) has the same erasure listMethod(List) as another method in type Foo

此错误的意思是说listMethod(List) 方法在编译时擦除类型后的方法是listMethod(List),它与另外一个方法重复,也就是方法签名重复。反编译之后的方法代码如下:

public void listMethod(List list){

}

从上面代码可以看出 Java 编译后的字节码中已经没有泛型的任何信息,在编译后所有的泛型类型都会做相应的转化,转化如下:

List、List 擦除后的类型为 List。

List、List[] 擦除后的类型为 List[]。

List extends E>、List super E> 擦除后的类型为 List。

List 擦除后类型为 List。

Java 为什么这么处理呢?有以下两个原因:

避免 JVM 的大换血。如果 JVM 将泛型类型延续到运行期,那么到运行期时 JVM 就需要进行大量的重构工作了,提高了运行期的效率。

版本兼容。 在编译期擦除可以更好地支持原生类型(Raw Type)。

明白了 Java 泛型是类型擦除的,下面的问题就很好理解了:

泛型的 class 对象是相同的

每个类都有一个 class 属性,泛型化不会改变 class 属性的返回值,例如:

public static void main(String[] args) {

List ls = new ArrayList();

List li = new ArrayList();

System.out.println(ls.getClass() == li.getClass());

}

代码返回值为 true,原因很简单,List 和 List 擦除后的类型都是 List。

泛型数组初始化时不能声明泛型类型

如下代码编译时通不过:

List[] list = new List[];

在这里可以声明一个带有泛型参数的数组,但是不能初始化该数组,因为执行了类型擦除操作后,List[] 与 List[] 就是同一回事了,编译器拒绝如此声明。

instanceof 不允许存在泛型参数

以下代码不能通过编译,原因一样,泛型类型被擦除了。

List list = new ArrayList();

System.out.println(list instanceof List);

错误信息如下:

Cannot perform instanceof check against parameterized type List. Use the form List> instead since further generic type information will be erased at runtime

类型擦除

正确理解泛型概念的首要前提是理解类型擦除(type erasure)。 Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List和List等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。

很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:

泛型类并没有自己独有的Class类对象。比如并不存在List.class或是List.class,而只有List.class。

静态变量是被泛型类的所有实例所共享的。对于声明为MyClass的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass还是new MyClass创建的对象,都是共享一个静态变量。

泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException和MyException的。对于JVM来说,它们都是 MyException类型的。也就无法执行与异常对应的catch语句。

类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉<>的内容。比如T get()方法声明就变成了Object get();List就变成了List。接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码:

class MyString implements Comparable {

public int compareTo(String str) {

return 0;

}

}

当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样的话,类MyString就会有编译错误,因为没有实现接口Comparable声明的String compareTo(Object)方法。这个时候就由编译器来动态生成这个方法。

实例分析

了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的List和List为例来具体分析:

public void inspect(List list) {

for (Object obj : list) {

System.out.println(obj);

}

list.add(1); //这个操作在当前方法的上下文是合法的。

}

public void test() {

List strs = new ArrayList();

inspect(strs); //编译错误

}

这段代码中,inspect方法接受List作为参数,当在test方法中试图传入List的时候,会出现编译错误。假设这样的做法是允许的,那么在inspect方法就可以通过list.add(1)来向集合中添加一个数字。这样在test方法看来,其声明为List的集合中却被添加了一个Integer类型的对象。这显然是违反类型安全的原则的,在某个时候肯定会抛出ClassCastException。因此,编译器禁止这样的行为。编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。

通配符与上下界

在使用泛型类的时候,既可以指定一个具体的类型,如List就声明了具体的类型是String;也可以用通配符?来表示未知类型,如List>就声明了List中包含的元素类型是未知的。 通配符所代表的其实是一组类型,但具体的类型是未知的。List>所声明的就是所有类型都是可以的。但是List>并不等同于List。List实际上确定了List中包含的是Object及其子类,在使用的时候都可以通过Object来进行引用。而List>则其中所包含的元素类型是不确定。其中可能包含的是String,也可能是 Integer。如果它包含了String的话,往里面添加Integer类型的元素就是错误的。正因为类型未知,就不能通过new ArrayList>()的方法来创建一个新的ArrayList对象。因为编译器无法知道具体的类型是什么。但是对于 List>中的元素确总是可以用Object来引用的,因为虽然类型未知,但肯定是Object及其子类。考虑下面的代码:

public void wildcard(List> list) {

list.add(1);//编译错误

}

如上所示,试图对一个带通配符的泛型类进行操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。

因为对于List>中的元素只能用Object来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。 如List extends Number>说明List中可能包含的元素类型是Number及其子类。而List super Number>则说明List中包含的是Number及其父类。当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法。比如访问 List extends Number>的时候,就可以使用Number类的intValue等方法。

类型系统

在Java中,大家比较熟悉的是通过继承机制而产生的类型体系结构。比如String继承自Object。根据Liskov替换原则,子类是可以替换父类的。当需要Object类的引用的时候,如果传入一个String对象是没有任何问题的。但是反过来的话,即用父类的引用替换子类引用的时候,就需要进行强制类型转换。编译器并不能保证运行时刻这种转换一定是合法的。这种自动的子类替换父类的类型转换机制,对于数组也是适用的。 String[]可以替换Object[]。但是泛型的引入,对于这个类型系统产生了一定的影响。正如前面提到的List是不能替换掉List的。

引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List和List这样的情况,类型参数String是继承自Object的。而第二种指的是 List接口继承自Collection接口。对于这个类型系统,有如下的一些规则:

相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List是Collection 的子类型,List可以替换Collection。这种情况也适用于带有上下界的类型声明。

当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对Collection extends Number>来说,其子类型可以在Collection这个维度上展开,即List extends Number>和Set extends Number>等;也可以在Number这个层次上展开,即Collection和 Collection等。如此循环下去,ArrayList和 HashSet等也都算是Collection extends Number>的子类型。

如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。

理解了上面的规则之后,就可以很容易的修正实例分析中给出的代码了。只需要把List改成List>即可。List是List>的子类型,因此传递参数时不会发生错误。

开发自己的泛型类

泛型类与一般的Java类基本相同,只是在类和接口定义上多出来了用<>声明的类型参数。一个类可以有多个类型参数,如 MyClass。 每个类型参数在声明的时候可以指定上界。所声明的类型参数在Java类中可以像一般的类型一样作为方法的参数和返回值,或是作为域和局部变量的类型。但是由于类型擦除机制,类型参数并不能用来创建对象或是作为静态变量的类型。考虑下面的泛型类中的正确和错误的用法。

class ClassTest {

private X x;

private static Y y; //编译错误,不能用在静态变量中

public X getFirst() {

//正确用法

return x;

}

public void wrong() {

Z z = new Z(); //编译错误,不能创建对象

}

}

泛型传递

即泛型可以当作参数在不同的实例化的类中传递,理论上来说可以无限制层次的传递下去。最终会约束每一层的方法或者类型的泛型确定,在《泛型传递》这篇文章中对具体的用法进行详尽的描述。

最佳实践

在使用泛型的时候可以遵循一些基本的原则,从而避免一些常见的问题。

在代码中避免泛型类和原始类型的混用。比如List和List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。

在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。

泛型类最好不要同数组一块使用。你只能创建new List>[10]这样的数组,无法创建new List[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。

不要忽视编译器给出的警告信息。

描述java泛型引入原则_Java/泛型的类型擦除/README.md · oslo/LearningNotes - Gitee.com相关推荐

  1. Java 泛型是如何工作的?类型擦除是什么?

    目录 1. 问题 2. 解答 1. 问题 泛型是如何工作的?类型擦除是什么意思? 2. 解答 泛型通过类型擦除来将变量变为一个类型,编译器在编译时擦出了所有类型相关的信息,所以在运行时不存在任何类型相 ...

  2. java 接口的泛型方法_Java泛型/泛型方法/通配符/泛型接口/泛型泛型擦出

    从JDK1.5以后引入了三大常用新特性:泛型.枚举(enum).注解(Annotation).其中JDK1.5中泛型是一件非常重要的实现技术,它可以帮助我们解决程序的参数转换问题.本文为大家详细介绍一 ...

  3. java泛型常用特点_Java泛型详解

    对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下. 本文参考java 泛型详解.Java中的泛型方法. java泛型详解 1. 概述 泛型在 ...

  4. java 泛型集合应用_Java泛型集合的应用和方法

    展开全部 泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以62616964757a686964616fe78988e69d8331333 ...

  5. java 怎么为泛型参数赋值_Java泛型讲解

    1. 概述在引入泛型之前,Java类型分为原始类型.复杂类型,其中复杂类型分为数组和类.引入范型后,一个复杂类型 就可以在细分成更多的类型. 例如原先的类型List,现在在细分成List, List等 ...

  6. java泛型详解_Java泛型详解(透彻)

    定义 Java中的泛型在JavaSE5中引入. 所谓泛型,即参数化类型.就是说,类型是以参数的方式传入泛型类. 例如: ArrayList aaryList = new ArrayList(); 那么 ...

  7. java 泛型 默认值_java泛型 Java泛型的入门知识

    在学习java的过程当中,我们就肯定会接触到java泛型,那么你知道java泛型是什么吗,有关java的使用方法又有哪些呢?今天小编就从java泛型的使用方法来了解一下java泛型这一知识. java ...

  8. java泛型的作用_Java 泛型的作用及其基本概念

    一.泛型的基本概念 java与c#一样,都存在泛型的概念,及类型的参数化.java中的泛型是在jdk5.0后出现的,但是java中的泛型与C#中的泛型是有本质区别的,首先从集合类型上来说,java 中 ...

  9. java泛型与接口_Java泛型用于方法,类和接口

    什么是泛型? 型就是数据类型参数化.就是说原本定死的数据类型像方法中的形参一样,数据是不确定的,使用的时候由人传进去,泛型就是这样,数据类型不确定了.使用的时候再确定就可以了. 泛型的特点 是JDK1 ...

最新文章

  1. cxgrid中纵横单元格合并_被合并单元格折磨疯的我,真后悔没早点知道这个Excel技巧!...
  2. 计算机一级多选题没有选分,计算机一级多选题
  3. PHP实现定时任务的几种方法
  4. 国外管理学博士国内计算机博士,教育部认可的国外管理学博士
  5. android qq 登陆 简书,使用QQ第三方登录
  6. D3---01基础的柱状图制作(转)
  7. 【实践】Angel深度学习在腾讯广告推荐训练优化中的实践
  8. 调用谷歌的方法输出图表,运行中的结果
  9. Android异步加载全解析之引入二级缓存
  10. CCNP精粹系列之十七--路由映射实战,博主推荐
  11. C/C++ —— signed 与 unsigned 有符号和无符号数及其移位
  12. 常用网线类别介绍(超五类、六类)
  13. 前端学习-吃豆子游戏设计
  14. 剖析广州“开四停四”交通限行的实现技术
  15. 电源电压测试所用到的电流探头-品致探头
  16. 爬虫项目十八:用Python对拉钩网全部城市所有招聘信息爬取
  17. 哔哩哔哩我来了,see goodbye 马总!!!
  18. QT学习Github地址,git使用记录
  19. 阿里云CentOS7.2清除wnTKYg木马
  20. 架构演进|研究mvp到mvvm(传统架构mvvm和Jetpack下的区别)

热门文章

  1. python series增加一行_关于python:将一个列表或系列作为一行添加到pandas DataFrame中?...
  2. php 调试 500,利用WordPress开发者调试模式解决PHP500内部服务器错误 | 骤雨打新荷...
  3. socket连接时间太长受什么原因影响?_晶振不起振的原因和应对措施
  4. windows7没pdf打印机_公司中常见的八种打印机故障解决方法
  5. bootstrap table 服务端分页
  6. 带你秒学JavaScript
  7. codeforces 580C Kefa and Park(DFS)
  8. sql 执行 delete 的时候,结合子查询 exists ,怎样支持别名呢?
  9. [转]JSP中常见的Tomcat报错错误解析(一)
  10. Codeforces_448C 分治