Java泛型——你所不知道的那些泛型背后

Posted David-Kuper

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java泛型——你所不知道的那些泛型背后相关的知识,希望对你有一定的参考价值。

Java泛型(一)——泛型的定义与使用
Java泛型(二)——使用Gson解析嵌套泛型数组

一、泛型

1、编译期确定类型安全——泛型(Generics)

泛型是提供给Javac编译器使用的。可以限定集合中输入的类型,让编译器在编译期间避免原始程序的非法输入,编译器编译带类型说明的集合时会去掉“类型”信息,使程序运行效率不受影响,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样,由于编译生成的字节码会去掉泛型的类型信息,只要能跳过编译器,就可以往某个泛型集合中加入其它类型的数据,例如,用反射得到集合,再调用其add方法添加自己不同的对象即可,这样便可以实现动态内容变化化的数组。

ArrayList类定义和ArrayList类引用中涉及如下术语:

泛型类型: 整个ArrayList<E>

类型参数(类型变量): E

参数化的类型: 整个ArrayList<Integer>

类型参数的实例: ArrayList中的Integer

typeof: ArrayList<Integer>中的<Integer>

原始类型: ArrayList


2、参数化类型与原始类型的兼容性

参数化类型可以引用一个原始类型的对象,编译报告警告,例如,   

Collection<String> c = new Vector ();

原始类型可以引用一个参数化类型的对象,编译报告警告,例如,

Collection c = new Vector<String>();

参数化类型不考虑类型参数的继承关系:

Vector<String> v = new Vector<Object>()    // 错误
Vector<Object> v = new Vector<String>()    // 也错误

3、类型擦除

正确理解泛型概念的首要前提是理解类型擦除(type erasure)。 Java中的泛型类似于C++中的模板,但是这种相似性仅限于表面,Java中的泛型基本上都是在编译器这个层次来实现的。属于编译器执行类型检查和类型诊断,然后生成普通的非泛型的字节码,也就是在生成的Java字节代码中是不包含泛型中的类型信息的,使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这种实现技术称为类型擦除。如在代码中定义的List<Object>和List<String>等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。

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

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

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

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


4、泛型的定义与使用

我们使用一个泛型首先要定义它,其次就是使用它进行泛型实例化。
一般来说我们有几种方式去确定泛型类别:

  1. 声明时确定

  2. 运行时延后赋值确定

  3. 泛型界限确定基本类型

/*
* 泛型界限
*/
static class Apple<T extends InputStream> 
        T data;

/*
*正常的泛型
*/  
static class Orange<T>
        T data;


    /**
     * 泛型的实例化 1:声明确定         2:运行时确定          3:泛型界限 + 运行时确定 
     * 
     * @throws IOException
     */
public static void test3() throws IOException 

        /**
         * 1:声明确定
         */

        Orange<InputStream> orange = new Orange<>();
        //此处编译不通过,因为声明时泛型参数化不会区分继承关系,属于精确性的声明。
        //Orange<InputStream> orange = new Orange<FileInputStream>();

        //此处报错,因为String并不是InputStream类型
        //orange.data = new String();

        //虽然是创建赋值,但这里也没有报错。此处是由于继承关系,FileInputStream也是InputStream,而且自动向上转型为InputStream,但它与泛型界限并不相同,泛型界限限制了T只能是某一个类别的派生,但没有界限的继承则可以在声明的时候修改T至任何一个类别,继承自这些类别的类也都可以被向上转型赋值,它的范围要广很多。
        orange.data = new FileInputStream(new File("123.txt"));
        System.out.println("1、声明确定:"+orange.data.getClass().getName());
        System.out.println();


        /**
         * 2:运行时确定
         */
        Orange orange2 = new Orange<>();
        //此处编译通过,因为没有在构造的时候初始化泛型类型,因此泛型是由编译器决定(也就是根据赋值决定)。
        orange2.data = new String();

        System.out.println("2、运行时确定:" + orange2.data.getClass().getName());
        orange2.data = new FileInputStream(new File("123.txt"));
        System.out.println("2、运行时确定:" +orange2.data.getClass().getName());
        System.out.println();

        /**
         * 3:泛型界限 + 运行时确定
         */
        Apple apple = new Apple<>();
        // 报错,因为String不是继承自InputStream
        // apple.data = new String();

        // 编译通过,泛型在编译期确定类型,因为ObjectInputStream是继承自InputStream,因此赋值成功
        apple.data = new BufferedInputStream(new FileInputStream("123.txt"));
        System.out.println("3、泛型界限 + 运行时确定:" +apple.data.getClass().getName());
        apple.data = new FileInputStream("123.txt");
        System.out.println("3、泛型界限 + 运行时确定" +apple.data.getClass().getName());

    

程序输出结果:

1、声明确定:java.io.FileInputStream

2、运行时确定:java.lang.String
2、运行时确定:java.io.FileInputStream

3、泛型界限 + 运行时确定:java.io.BufferedInputStream
3、泛型界限 + 运行时确定java.io.FileInputStream

从上面的结果我们可以看出,泛型只是我们躲过编译器编译的一个手段,让我们的程序在运行时拥有更强大的灵活性,可以在编译期间对泛型,但它也意味着我们的程序很有可能在运行时出现错误(转型失败引起程序崩溃)。


下一篇文章Java泛型(二)——使用Gson解析嵌套泛型数组会分享一下Gson中使用泛型来解析List<E>的泛型,以及更加复杂的一种:泛型里面还有泛型的情况,比如如何使用Gson来解析像List<CardBean<T>>这样的数据。这种情况会存在于当我们的界面List展示的View是不同的类别时,这个时候就需要动态的去解析每一个View的Bean。

以上是关于Java泛型——你所不知道的那些泛型背后的主要内容,如果未能解决你的问题,请参考以下文章

sudo命令背后你所不知道的秘密

你所不知道的 ChaosBlade 那些事

java遗珠之泛型类型擦除

java遗珠之泛型类型擦除

你所不知道的那些英语成语

Android drawable微技巧,你所不知道的drawable的那些细节