---------------------- ASP.Net+Unity开发、.Net培训、期待与您交流! ----------------------
本文是将Java深度历险之Java泛型与Java 5 泛型深入研究合并修改而成的。
1、泛型概述
泛型是JAVA SE 1.5的新特性,泛型的本质是参数化的类型,也就是说所操作的数据类型被指定为一个参数。这种类型参数可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
JAVA语言引入泛型的好处是安全简单。
在JAVA SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
例1:未使用与使用了泛型的区别
//未使用泛型
class Gen2 {
private Object ob; //定义一个通用类型成员
public Gen2(Object ob) {
this.ob = ob;
}
public Object getOb() {
return ob;
}
public void setOb(Object ob) {
this.ob = ob;
}
public void showTyep() {
System.out.println("T的实际类型是: " + ob.getClass().getName());
}
}
public class GenDemo2 {
public static void main(String[] args) {
//定义类Gen2的一个Integer版本
Gen2 intOb = new Gen2(new Integer(88));
intOb.showTyep();
int i = (Integer) intOb.getOb(); //显式的强制类型转换
System.out.println("value= " + i);
System.out.println("----------------------------------");
//定义类Gen2的一个String版本
Gen2 strOb = new Gen2("Hello Gen!");
strOb.showTyep();
String s = (String) strOb.getOb(); //显式的强制类型转换
System.out.println("value= " + s);
}
}
//使用了泛型
class Gen<T> {
private T ob; //定义泛型成员变量
public Gen(T ob) {
this.ob = ob;
}
public T getOb() {
return ob;
}
public void setOb(T ob) {
this.ob = ob;
}
public void showTyep() {
System.out.println("T的实际类型是: " + ob.getClass().getName());
}
}
public class GenDemo {
public static void main(String[] args){
//定义泛型类Gen的一个Integer版本
Gen<Integer> intOb=new Gen<Integer>(88);
intOb.showTyep();
int i= intOb.getOb(); //所有的强制转换都是自动和隐式的
System.out.println("value= " + i);
System.out.println("----------------------------------");
//定义泛型类Gen的一个String版本
Gen<String> strOb=new Gen<String>("Hello Gen!");
strOb.showTyep();
String s=strOb.getOb(); //所有的强制转换都是自动和隐式的
System.out.println("value= " + s);
}
}
输出结果:
两个例子输出结果相同,均为:
T的实际类型是: java.lang.Integer
value= 88
----------------------------------
T的实际类型是: java.lang.String
value= Hello Gen!
泛型中涉及的术语:以ArrayList<E>定义和ArrayList<Integer>引用为例,
1)ArrayList<E>整个称为泛型类型;
2)ArrayList<E>中的E称为类型变量或类型参数;
3)ArrayList<Integer>称为参数化的类型;
4)ArrayList<Integer>中的Integer称为类型参数的实例或实际类型参数;
5)<>念作typeof;
6)ArrayList称为原始类型。
2、泛型类别
1)泛型类
//泛型类
class Utils<T>
{
private T t;
public void setObject(T t)
{
this.t=t;
}
public T getObject()
{
return t;
}
}
public class GenericDemo
{
public static void main(String[] args)
{
Utils<String> u=new Utils<String>();
u.setObject("AAA");
String s=u.getObject();
}
}
当类中要操作的引用数据类型不确定的时候,早期定义Object来完成扩展,现在定义泛型来完成扩展。
2)泛型方法
public <T> void show(T t){}
泛型类定义的泛型,在整个类中有效。当泛型类的对象确定要操作的具体类型后,类内用相同类型参数修饰的成员,其要操作的类型也相应确定。
若想让不同的方法可以操作不同类型,而且类型还不确定,那么可以将泛型定义在方法上。
例2:泛型类、泛型方法的混合
//泛型类
class Utils<T>
{
private T t;
public <Q> void show(Q q) //泛型方法
{
System.out.println("show:"+q);
}
public void print(T t)
{
System.out.println("print:"+t);
}
}
public class GenericDemo
{
public static void main(String[] args)
{
Utils<String> u=new Utils<String>();
u.print("aaa");
//u.print(1); //错误,print()只能打印String类型
u.show(1); //正确,show()可以打印与对象确定的类型不同的变量
}
}
输出结果:
print:aaa
show:1
3)静态泛型方法
class Utils<T>
{
private T t;
public void print(T t)
{
System.out.println("print:"+t);
}
/*静态方法不可以访问类上定义的泛型
**如果静态方法操作的应用数据类型不确定,可以将泛型定义在方法上
public static void show(T t) //错误
{
System.out.println("show:"+t);
}
*/
public static <Q> void show(Q q)//正确
{
System.out.println("show:"+q);
}
}
4)泛型接口
两种情况:
①
//泛型接口1
interface Inter<T>
{
void show(T t);
}
class Utils implements Inter<String>
{
private String t;
public void show(String t)
{
System.out.println("show:"+t);
}
}
public class GenericDemo
{
public static void main(String[] args)
{
Utils u=new Utils();
u.show("bbb");
}
}
②
//泛型接口2
interface Inter<T>
{
void show(T t);
}
class Utils<T> implements Inter<T>
{
private T t;
public void show(T t)
{
System.out.println("show:"+t);
}
}
public class GenericDemo
{
public static void main(String[] args)
{
Utils<String> u=new Utils<String>();
u.show("bbb");
//u.show(1);错误
}
}
3、通配符?与泛型限定
1)泛型限定
T extends SomeClass/SomeInterface,如T extends Collection:
extends后面可以是类也可以是接口。其意义为,T类型是实现Collection接口的类型。Collection是上限。
T super SomeClass/SomeInterface,如T super ArrayList:
super后面同样既可以是类也可以是接口。其意义为,T类型是ArrayList的父类型。ArrayList是下限。
import java.util.*;
public class GenericDemo
{
public static void main(String[] args)
{
CollectionGenFoo<ArrayList> listFoo = null;
listFoo = new CollectionGenFoo<ArrayList>(new ArrayList());
//出错了,HashMap不是Collection的子类。
//CollectionGenFoo<HashMap> listFoo = null;
//listFoo=new CollectionGenFoo<HashMap>(new HashMap());
System.out.println("实例化成功!");
}
}
class CollectionGenFoo<T extends Collection> {
private T x;
public CollectionGenFoo(T x) {
this.x = x;
}
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
}
2)通配符?
① 在使用泛型的时候,既可以指定一个具体的类型,如List<String>就声明了具体的类型是String;也可以用通配符?来表示未知类型,如List<?>就声明了List中包含的元素类型是未知的。 通配符所代表的其实是一组类型,但具体的类型是未知的。List<?>所声明的就是所有类型都是可以的。
② List<?>并不等同于List<Object>。List<Object>实际上确定了List中包含的是Object及其子类,在使用的时候都可以通过Object来进行引用。而List<?>其中所包含的元素类型是不确定的。其中可能包含的是String,也可能是 Integer。如果它包含了String的话,往里面添加Integer类型的元素就是错误的。
③ 因为类型未知,就不能通过new ArrayList<?>()的方法来创建一个新的ArrayList对象。因为编译器无法知道具体的类型是什么。但是对于 List<?>中的元素确总是可以用Object来引用的,因为虽然类型未知,但肯定是Object及其子类。同理,也不能调用与类型参数有关的方法,但可以调用与类型参数无关的方法,如下面的注意所示。
? extends E:可以接收E类型或者E的子类型,E为上限。
? super E:可以接收E类型或者E的父类型,E为下限。
import java.util.*;
public class GenericDemo
{
public static void main(String[] args)
{
CollectionGenFoo<ArrayList> listFoo = null;
listFoo = new CollectionGenFoo<ArrayList>(new ArrayList());
//不会出错了
//?通配符解决了类型被限制死了,不能动态根据实例来确定的缺点
CollectionGenFoo<? extends Collection> listFoo1 = null;
listFoo1=new CollectionGenFoo<LinkedList>(new LinkedList());
System.out.println("实例化成功!");
}
}
class CollectionGenFoo<T extends Collection> {
private T x;
public CollectionGenFoo(T x) {
this.x = x;
}
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
}
注意:
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()等方法。
4、类型擦除
正确理解泛型概念的首要前提是理解类型擦除(type erasure)。 Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List<Object>和List<String>等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。
很多泛型的奇怪特性都与这个类型擦除的存在有关:
1)泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。2)静态成员是被泛型类的所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。因此, 静态成员不可以拥有类级别的类型参数。
3)泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException<String>和MyException<Integer>的。对于JVM来说,它们都是 MyException类型的。也就无法执行与异常对应的catch语句。不过, 可以用在方法的throws列表中,如下所示。
private static <T extends Exception> sayHello() throws T
{
try{}catch(Exception e){throw (T)e;}
}
类型擦除的基本过程:
首先是找到用来替换类型参数的具体类。 这个具体类一般是Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明。比如T get()方法声明就变成了Object get();List<String>就变成了List。 接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码:class MyString implements Comparable<String> {
public int compareTo(String str) {
return 0;
}
}
当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样的话,类MyString就会有编译错误,因为没有实现接口Comparable声明的int compareTo(Object)方法。这个时候就由编译器来动态生成这个方法。
编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的List<Object>和List<String>为例来具体分析:
例3:
public void inspect(List<Object> list) {
for (Object obj : list) {
System.out.println(obj);
}
list.add(1); //这个操作在当前方法的上下文是合法的。
}
public void test() {
List<String> strs = new ArrayList<String>();
inspect(strs); //编译错误
}
这段代码中,inspect方法接受List<Object>作为参数,当在test方法中试图传入List<String>的时候,会出现编译错误。假设这样的做法是允许的,那么在inspect方法就可以通过list.add(1)来向集合中添加一个数字。这样在test方法看来,其声明为List<String>的集合中却被添加了一个Integer类型的对象。这显然是违反类型安全的原则的,在某个时候肯定会抛出ClassCastException。因此,编译器禁止这样的行为。编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。
5、类型系统
在Java中,大家比较熟悉的是通过继承机制而产生的类型体系结构。比如String继承自Object。根据Liskov替换原则,子类是可以替换父类的。当需要Object类的引用的时候,如果传入一个String对象是没有任何问题的。但是反过来的话,即用父类的引用替换子类引用的时候,就需要进行强制类型转换。编译器并不能保证运行时刻这种转换一定是合法的。这种自动的子类替换父类的类型转换机制,对于数组也是适用的。String[]可以替换Object[]。但是泛型的引入,对于这个类型系统产生了一定的影响。正如前面提到的List<String>是不能替换掉List<Object>的。
引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于List<String>和List<Object>这样的情况,类型参数String是继承自Object的。而第二种指的是 List接口继承自Collection接口。
对于这个类型系统,有如下的一些规则:
1)相同类型参数的泛型类型的关系取决于泛型类型自身的继承体系结构。即List<String>是Collection<String>的子类型,List<String>可以替换Collection<String>。这种情况也适用于带有上下界的类型声明。
2)当泛型类型的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对Collection<? extends Number>来说,其子类型可以在Collection这个维度上展开,即List<? extends Number>和Set<? extends Number>等;也可以在Number这个层次上展开,即Collection<Double>和 Collection<Integer>等。如此循环下去,ArrayList<Long>和 HashSet<Double>等也都算是Collection<? extends Number>的子类型。
3)如果泛型类型中包含多个类型参数,则对于每个类型参数分别应用上面的规则。
4)参数化类型与原始类型兼容。参数化类型可以引用一个原始类型的对象,如Collection<String> c=new Vector();原始类型可以引用一个参数化类型的对象,如Collection c= new Vector<String>();两者编译器都只会报告警告。
6、类型推断
编译器判断泛型方法的实际类型参数的过程称为类型推断,这是一个非常复杂的过程。
真实的实际参数类型其实是调用方法时传入的参数的类型的最小公共父类型,例如,
package com.cn.itcast;
public class FanxingTest {
public static void main(String[] args){
Integer a1=add(1,1);
Double a2=add(1.1,1.2);
//Integer a3=add(1,1.1);//报错
//Double a4=add(1,1.1);//报错
//实际参数类型为Integer与Double的最小公共父类型,即Number
Number a5=add(1,1.1);
//实际参数类型为Integer与String的最小公共父类型,即Object
Object a6=add(1,"a");
}
static <T> T add(T a,T b){
//return (T)(a+b); //许多类未对'+'号进行定义
return a;
}
}
7、泛型中的反射应用
1、由于类型擦除,编译生成的字节码会去掉泛型信息。因此,对于可以跳过编译器的反射,就可以往某个泛型集合中加入其它类型的数据,例如,
public static void main(String[] args)throws Exception{
ArrayList<String> arrayList=new ArrayList<String>();
arrayList.add("abc");
//arrayList.add(1);//报错,不能添加非String类型的元素
//利用反射添加其他类型的元素
Method addObj=arrayList.getClass().getMethod("add", Object.class);
addObj.invoke(arrayList, 1);
//打印集合中元素
System.out.println(arrayList);
}
输出结果:
[abc, 1]
2、通过反射获得泛型的参数化类型
public class GenericDemo {
public static void main(String[] args)throws Exception{
ArrayList<String> arrayList=new ArrayList<String>();
//无法获得arrayList的参数化类型String,但可以获得方法中的泛型的参数类型Date
Method applyMethod=GenericDemo.class.getMethod("applyArrayList", ArrayList.class);
Type[] types=applyMethod.getGenericParameterTypes();//获得该方法中所有的类型参数
ParameterizedType pType=(ParameterizedType)types[0];//applyArrayList方法只有一个参数
System.out.println(pType.getRawType()); //原始类型
System.out.println(pType.getActualTypeArguments()[0]);//泛型的参数化类型
}
public static void applyArrayList(ArrayList<Date> arrayList){}
}
输出结果:
class java.util.ArrayList
class java.util.Date
8、使用泛型的一些原则
1) 在代码中避免参数化类型和原始类型的混用。比如List<String>和List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。
2) 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。
3)泛型最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。
4)只有引用类型才能作为泛型的类型参数,基本数据类型不行。
5)不要忽视编译器给出的警告信息。
延伸阅读:
---------------------- ASP.Net+Unity开发、.Net培训、期待与您交流! ----------------------详细请查看:www.itheima.com