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、泛型的定义与使用
我们使用一个泛型首先要定义它,其次就是使用它进行泛型实例化。
一般来说我们有几种方式去确定泛型类别:
声明时确定
运行时延后赋值确定
泛型界限确定基本类型
/*
* 泛型界限
*/
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泛型——你所不知道的那些泛型背后的主要内容,如果未能解决你的问题,请参考以下文章