一、序言
变化一:
在引入范型之前,Java中的类型分为原始类型、复杂类型,其中复杂类型分为数组和类;引入范型后,一个复杂类型可以细分成更多的类型。
例如,原先的List类型,现在细分成List<Object>, List<String>等更多的类型。
注:List<Object>和List<String>是两种不同的类型, 它们之间没有继承关系,即使String继承了Object。下述代码是非法的:
List<String> ls = new ArrayList<String>(); List<Object> lo = ls;
这样设计的原因:根据lo的声明,编译器允许你向lo中添加任意对象(例如Integer),但是为其赋值List<String>类型的值,破坏了数据类型的完整性。
变化二:
在引入范型之前,要让类中的方法支持多个数据类型,就需要对方法进行重载;在引入范型之后,可以更进一步定义多个参数以及返回值之间的关系。
例如,public void write(Integer i, Integer[] ia);及public void write(Double d, Double[] da);的范型版本为:public <T> void write(T t, T[] ta);
二、详述Java泛型
在细说Java泛型之前,大家可以先看一下如下这样一段代码,相信大家肯定不会感觉陌生:
package com.soft; public class WW { public <T> void write(T t, T[] ta){ } }
在编码时,可经常见到类似于上面这样的代码,接下来将详述Java泛型。
那么我们首先要知道什么是泛型,泛型即“参数化类型”,顾名思义,就是将类型由原来具体的类型变为参数化的形式。在定义泛型接口、泛型类和泛型方法的过程中,我们常见的T、E、S等形式的参数用于表示泛型形参,用于接收来自外部使用时候传入的类型实参。
A、自定义泛型类和泛型方法
(1)定义带类型参数的类
package com.soft; public class MM<T, S extends T> { public T say(){ T t=null; return t; } }
规则:在定义带类型参数的类时,在紧跟类命之后的<>内,指定一个或多个类型参数的名字,同时也可以对类型参数的取值范围进行限定,多个类型参数之间用","进行分隔。
说明:定义完类型参数后,可以在类中定义位置之后的几乎任意地方使用类型参数(静态块,静态属性,静态方法除外),就像使用普通的类型一样。
注意:父类定义的类型参数不能被子类继承。
(2)定义带类型参数方法
package com.soft; public class WW { public <T> void write(T t, T[] ta){ } public <S> void read(S s, S[] sa){ } public <T, S extends T> T test(T t, S s){ S ss=null; return ss; } }
规则:在定义带类型参数的方法时,在紧跟可见范围修饰(如public)之后的<>内,指定一个或多个类型参数的名字,同时也可以对类型参数的取值范围进行限定,多个类型参数之间用","进行分隔。
说明: 定义完带类型参数的方法后,可以在方法中定义位置之后的任意地方使用类型参数,就像使用普通的类型一样。
注意:定义带类型参数的方法,其主要目的是为了表达多个参数以及返回值之间的关系。例如本例中T和S为继承关系,返回值的类型(T)和第一个类型参数(t)的类型(T)相同。
补充:
当我们需要一个在逻辑上可以用来表示同时是List<Integer>和List<Number>的类型时,类型通配符应运而生。类型通配符一般是使用"?"代替具体的类型实参。
如果仅仅是想实现多态,请优先使用通配符解决。
初始代码如下:
public <T> void test1(List<T> u){ ... }
public <S> void test2(List<S> u){ ... }
使用通配符的代码如下:
public void test(List<?> s){ ... }
B、泛型类和泛型方法的使用
(1)对带类型参数的类进行赋值
对带类型参数的类进行赋值有两种方式:
A、声明类变量或者实例化时,如下:
List<String> list; list = new ArrayList<String>();
如下赋值方式会报错:
List<Integer> a = new ArrayList<Integer>(712); List<Number> b = a;
List<Number>不能视为List<Integer>的父类,请参照前面“序言->变化一”中的描述。
B、继承类或者实现接口时,如下:
public class MyList<E> extends ArrayList<E> implements List<E> {...}
(2)对带类型参数的方法进行赋值
当调用泛型方法时,编译器自动对类型参数进行赋值,当不能成功赋值时报编译错误,如下:
package com.soft; import java.util.List; public class WW {public <T, S extends T> T test(T t, S s){ return s; } public <T> T test1(T t, List<T> list){ return t; } public <T> T test2(List<T> list1, List<T> list2){ return null; } public void common(){ Object o = null; Integer i = null; Number n = null; test(n, i);//此时T为Number, S为Integer test(o, i);//T为Object, S为Integer List<Integer> list1 = null; List<Number> list2 = null; test1(i, list1);//此时T为Integer,i为Integer类型,list1为List<Integer>类型 test1(i, list2);//此时T为Number,i为Integer类型,list2为List<Number>类型 test2(list1, list2);//编译报错 } }
请读者自行分析上述代码中着色语句为什么会编译报错,很简单的哦O(∩_∩)O哈哈~
(3)对通配符类型参数进行赋值
在上面两小节中,对是类型参数赋予具体的值,除此,还可以对类型参数赋予不确定值
假设有如下定义:
List<?> unknownList; List<? extends Number> unknownNumberList; List<? super Integer> unknownBaseLineIntgerList;
现对上述定义的变量赋值,如下:
List<String> listString = null; unknownList = listString;
三、类型通配符上限与下限
有时候,我们会听到类型通配符上限和类型通配符下限这样的说法,其实在前面的例子中有下面这样一小段代码,在该代码中已经体现出一些上下限的思想
public <T, S extends T> T test(T t, S s){ S ss=null; return ss; }
此处我们进一步说明
package com.soft; public class GenericTest { public static void main(String[] args) { Student<String> name = new Student<String>("xiaoming"); Student<Integer> age = new Student<Integer>(24); Student<Number> number = new Student<Number>(1001); getData(name); getData(age); getData(number); getUpperNumberData(name);//编译报错 getUpperNumberData(age); getUpperNumberData(number); } public static void getData(Student<?> data) { System.out.println("data :" + data.getData()); } public static void getUpperNumberData(Student<? extends Number> data){//此处限定类型实参只能是Number类及其子类 System.out.println("data :" + data.getData()); } } class Student<T> { private T data; public Student() { } public Student(T data) { this.data = data; } public T getData() { return data; } }
我想大家通过上例肯定能体会到什么是类型通配符上限,类型通配符上限通过形如Student<? extends Number>形式定义,相对应的,类型通配符下限为Student<? super Number>形式,其含义与类型通配符上限正好相反,在此不作过多阐述了。
四、注意事项
A、当对类或方法的类型参数进行赋值时,要求对所有的类型参数进行赋值,否则,将得到一个编译错误。
B、在Java集合框架中,对于参数值是未知类型的容器类,只能读取其中的元素,不能向其中添加元素,因为其类型是未知,编译器无法识别添加元素的类型和容器的类型是否兼容,唯一的例外是NULL
C、可以使用带泛型参数的类声明数组,却不可以用于创建数组
List<Integer>[] iListArray; iListArray=new ArrayList[10]; iListArray=new ArrayList<Integer>[10];//编译时错误
五、实现原理
(1)一个泛型类被其所有的实例共享
现有如下这样一段代码,其打印的结果是什么?
List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass());
或许你会说是false,but,you are wrong!它打印出true。我们可以发现虽然传入的类型实参不同,但其生成的所有对象实例的类型是一样的,即一个泛型类的所有实例在运行时具有相同的运行时类(class), 而不管他们的实际类型参数。
事实上,泛型之所以叫泛型,就是因为它对其所有可能的类型参数有同样的行为。类的静态变量和静态方法在所有的实例间共享,这就是为什么在静态变量的声明和初始化时或者在静态方法或静态初始化代码中使用类型参数是不合法的原因,类型参数是属于具体实例的。
(2)擦除(erasure)
在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,可以接收不同泛型实参的泛型类在内存中只有一个,是原来的最基本的类型(上例中为List),当然,在逻辑上我们可以理解成多个不同的泛型类型。
究其原因,Java泛型是通过java编译器的称为擦除(erasure)的前端处理来实现的,Java中的泛型只作用于代码编译阶段,在编译过程中正确检验泛型结果后,会将泛型的相关信息擦除(erasure),也就是说,成功编译之后的.class文件中是不包含任何泛型信息的,泛型信息不会进入运行时阶段。你可以把它认为是(基本上就是)一个从源码到源码的转换,它把泛型版本转换成非泛型版本。
基本上,擦除(erasure)去掉了所有的泛型类型信息,所有在尖括号之间的类型信息都被扔掉了,比如说,一个 List<String>类型被转换为List,所有对类型变量的引用被替换成类型变量的上限(通常是Object)。对此总结成一句话:泛型类型在逻辑上可以看成是多个不同的类型,但实际上都是相同的基本类型。
(3)类型转换
类型参数在运行时并不存在,这意味着它们不会添加任何时间或者空间上的负担,这很好,不幸的是,这也意味着你不能依靠它们进行类型转换,下面这段代码有如下图所示的报错信息:
public <T> T badCast(T t, Object o) { return (T) o; // unchecked warning }
类似的,如下的类型转换将得到一个unchecked warning,因为运行时环境不会为你做这样的检查。
Collection cs = new ArrayList<String>(); Collection<String> cstr = (Collection<String>) cs;
(4)instanceof
泛型类被其所有实例(instances)共享的另一个暗示就是检查一个实例是不是某一个特定类型的泛型类是没有意义的,如下图所示:
六、Class泛型的使用
Java5中的一个变化是类java.lang.Class是泛型化的(Class<T>),这是把泛型扩展到容器类之外的一个很有意思的例子。 你很可能会问,Class的类型参数T代表什么?它代表Class对象代表的类型,比如说,Class对象String.class的类型为Class<String>,Class对象Serializable.class的类型为Class<Serializable>,此处T代表什么请自行体会。
Java5为什么会有这样的变化,这可以被用来提高你反射代码的类型安全。特别的,因Class的newInstance()方法返回一个T,你可以在使用反射创建对象时得到更精确的类型。比如说,假定你要写一个工具方法来进行数据库查询,给定一个SQL语句,请返回一个数据库中符合查询条件的对象集合(collection),你该怎么做?
解决方案:
(1)定义接口:
interface Factory<T> { public T[] make(); }
(2)定义查询方法:
public <T> Collection<T> select(Factory<T> factory, String statement) { Collection<T> result = new ArrayList<T>(); /* run sql query using jdbc */ for ( int i=0; i<10; i++ ) { /* iterate over jdbc results */ T item = factory.make(); /* use reflection and set all of item’s fields from sql results */ result.add( item ); } return result; }
(3)调用(匿名类方案 ):
select(new Factory<EmpInfo>(){ public EmpInfo make() { return new EmpInfo(); } } , ”selection string”);
除了上述方式外,也可以声明一个类来支持Factory接口:
class EmpInfoFactory implements Factory<EmpInfo> { ... public EmpInfo make() { return new EmpInfo();} } select(getMyEmpInfoFactory(), "selection string");//调用语句,其中getMyEmpInfoFactory()方法返回自定义的支持Factory接口的类
解说:上述两种解决方案虽然可行,却很不自然,其缺点是它们需要下面的二者之一:
a.为每个要使用的类型定义冗长的匿名工厂类;
b.为每个要使用的类型声明一个工厂类并传递其对象给调用处
此时可以选择使用class类型参数,它可以被反射使用,没有泛型的代码可能如下:
Collection emps = sqlUtility.select(EmpInfo.class, ”select * from emps”); public static Collection select(Class c, String sqlStatement) { Collection result = new ArrayList(); /* run sql query using jdbc */ for ( /* iterate over jdbc results */ ) { Object item = c.newInstance(); /* use reflection and set all of item’s fields from sql results */ result.add(item); } return result; }
上述方案虽然能达到目的,却不能给我们返回一个精确类型的集合,因Java5及以后版本中Class是泛型的,我们可以按下面这种方式写:
Collection<EmpInfo> emps=sqlUtility.select(EmpInfo.class, ”select * from emps”); public static <T> Collection<T> select(Class<T>c, String sqlStatement) { Collection<T> result = new ArrayList<T>(); /* run sql query using jdbc */ for ( /* iterate over jdbc results */ ) { T item = c.newInstance(); /* use reflection and set all of item’s fields from sql results */ result.add(item); } return result; }
上述方案通过一种类型安全的方式得到了我们想要的集合,这项技术是一个非常有用的技巧。
七、参考资料
本文只阐述了Java泛型的部分内容,此处贴出一些优质文章以供参考
(1)http://www.cnblogs.com/lwbqqyumidi/p/3837629.html