一、泛型由来
Java语言类型包括八种基本类型(byte short int long float double boolean char)和复杂类型,复杂类型包括类和数组。
早期Java版本(1.4之前)如果要代指某个泛化类对象,只能使用Object,这样写出来的代码需要增加强转,而且缺少类型检查,代码缺少健壮性。在1.5之后,Java引入了泛型(Generic)的概念,提供了一套抽象的类型表示方法。利用泛型,我们可以:
1、表示多个可变类型之间的相互关系:HashMap<T,S>表示类型T与S的映射,HashMap<T, S extends T>表示T的子类与T的映射关系
2、细化类的能力:ArrayList<T> 可以容纳任何指定类型T的数据,当T代指人,则是人的有序列表,当T代指杯子,则是杯子的有序列表,所有对象个体可以共用相同的操作行为
3、复杂类型被细分成更多类型:List<People>和List<Cup>是两种不同的类型,这意味着List<People> listP = new ArrayList<Cup>()是不可编译的。后面会提到,这种检查基于编译而非运行,所以说是不可编译并非不可运行,因为运行时ArrayList不保留Cup信息。另外要注意,即使People继承自Object,List<Object> listO = new ArrayList<People>()也是不可编译的,应理解为两种不同类型。因为listO可以容纳任意类型,而实例化的People列表只能接收People实例,这会破坏数据类型完整性。
4、简化代码实现:假设有一个执行过程,对不同类型的数据,进行某些流程一致的处理,不引入泛型的实现方法为:
public void addToArray(Integer data, Integer array[], int pos) {
array[pos] = data;
}
public void addToArray(Long data, Long array[], int pos) {
array[pos] = data;
}
这是一种典型的多态行为——重载,但是不够简化。引入泛型的写法更优雅:
public <T> void addToArray(T data, T array[], int pos) {
array[pos] = data;
}
二、泛型定义与使用(泛型类和泛型方法)
1、泛型参数的命名风格:
1)尽量用简便的命名来命名泛型,若类型无特定意义,尽量使用一个字符
2)尽量使用全大写来命名泛型形参,以此与其他类型区分开
3)单字母的泛型建议用T命名,如果有多个泛型,可以取T周围的大写字母,注意,如果泛型本身有意义,可以不遵守这一条,比如缓存管理CacheManager<PARAM, DATA>,该类负责管理缓存查询条件与数据的映射,用单字母就不太合适,使用多字母更好
4)对于泛型函数或者泛型内部类在某个泛型类中出现的情况,建议泛型函数和内部类的泛型形参名称与外层类的泛型名称保持不同,否则容易引起混淆。类似这种:
public class GenericClass<T> {
public <T> void testGenericMethod(T t) {
}
}
其实testGenericMethod方法的形参与外面GenericClass的形参完全没有关系。换句话说,泛型方法的泛型是优先使用方法泛型定义的。这种更应该写成:
public class GenericClass<T> {
public <S> void testGenericMethod(S s) {
}
}
2、泛型存在两种用法:泛型类和泛型方法
1)泛型类
定义泛型类时,在类名后加<>,尖括号内可以定义一个或多个泛型参数,并指定泛型参数的取值范围,多个参数用逗号(,)分割
泛型类中定义的泛型全类可用(静态方法、静态代码块、静态成员变量除外)
父类定义的泛型参数子类无法继承,所以子类得自己写
public class GenericClass<T extends AClass> {
T data;
void setData(T t) {
data = t;
}
T getData() {
return data;
}
}
2)泛型方法
定义泛型方法,在方法修饰符后,返回参数前加上<>,尖括号内可以定义一个或多个泛型参数,并指定泛型参数取值范围,多个参数用逗号(,)分割
泛型方法中定义的泛型作用域在方法内
public class GenericMethodClass {
public <T extends AClass, S extends T> T setData(T t, S s) {
//do something
return t;
}
}
定义泛型方法,更多是为了表达返回值和方法形参间的关系,本例中方法第一个参数T继承AClass,第二个参数S继承T,返回值是第一个参数。
如果仅仅是为了实现了多态,应优先使用通配符。类似如下:
public void addList(List<?> list) {
//todo
}
3、定义了多个泛型类型参数时,一定要在使用时都指定类型,否则会编译出错。
4、对泛型类的类型参数赋值包含两种方法:
1)类变量或实例化:List<String> listS;listS = new ArrayList<String>();
2)继承public class MyList<S> extends ArrayList<S> implements IMyInterface<S> {}S是对ArrayList内部定义的泛型E的赋值。
5、对泛型方法的赋值:
public <T> T testMethod1(T t, List<T> list) {
}
public <T> T testMethod2(List<T> list1, List<T> list2){
}
People n = null;
List<People> list1 = null;
testMethod1(n, list1);//此时泛型参数T为People
List<Integer> list2 = null;
testMethod2(list1, list2);//编译报错
三、通配符
1)上述泛型赋值都是赋予泛型参数确定值,我们还可以赋予泛型参数不确定值,也就是通配符?。使用通配符?表示一个未知的类型。类似如下:
List<?> list;存放任意的对象
List<? extends AClass> listSubAClass; //存放AClass的子类
List<? extends BClass> listSuperBClass; //存放BClass的父类
2)通配符通常与泛型关键字一起使用。
3)在Java集合框架中,对于未知类型的容器类,只能读取其中元素,不能添加元素。这是因为,对不确定的参数类型,编译器无法识别添加元素的类型和容器的类型是否兼容,唯一的例外是NULL。同时,其读取的元素只能用Object来存储。
4)通配符不能用在泛型类和泛型方法声明中,类似如下:
public class GenericClass<?> { //编译错误
public <?> void testGenericMethod(? t) { //编译错误
}
}
四、泛型关键字
1、泛型关键字有二个 extends和super,分别表示类型上界和类型下界
T extends AClass 表示T继承自AClass类
? super AClass 表示?是AClass的父类,注意:super只能与通配符?搭配使用,我们不能写:
public class GenericClass<T super AClass> { //错误
}
此例子中super换成extends是正确的,表示泛型T继承自AClass,T换成通配符?也是可以的,表示未知类型的下界是AClass。
2、通配符与泛型关键字组合使用
举两个例子:
下界:
List<? super People> list = new ArrayList<>();
People people = new People();
list.add(people);
People data= list.get(0 ); //编译出错,报错Object不能转为People
上界:
List<? extends People> list = new ArrayList<>();
People people = new People();
list.add(people);// 编译出错,不能向容器中添加确定的元素
People data= list.get( 0);
总结就是:上界添加(add)受限,下界查询(get)受限
五、泛型实现原理
1、Java泛型是编译时技术,在运行时不包含类型信息,仅其实例中包含类型参数的定义信息。
2、Java利用编译器擦除(erasure,前端处理)实现泛型,基本上就是泛型版本源码到非泛型版本源码的转化。
3、擦除去掉了所有的泛型类内所有的泛型类型信息,所有在尖括号之间的类型信息都被扔掉.
举例来说:List<String>类型被转换为List,所有对类型变量String的引用被替换成类型变量的上限(通常是Object)。
而且,无论何时结果代码类型不正确,会插入一个到合适类型的转换。
<T> T badCast(T t, Object o) {
return (T) o; // unchecked warning
}
这说明String类型参数在List运行时并不存在。它们也就不会添加任何的时间或者空间上的负担。但同时,这也意味着你不能依靠他们进行类型转换。
4、一个泛型类被其所有调用共享
对于上文中的GenericClass,在编译后其内部是不存入泛型信息的,也就是说:
GenericClass<AClass> gclassA = new GenericClass<AClass>();
GenericClass<BClass> gclassB = new GenericClass<BClass>();
gClassA.getClass() == gClassB.getClass()
这个判断返回的值是true,而非false,因为一个泛型类所有实例运行时具有相同的运行时类,其实际类型参数被擦除了。
那么是不是GenericClass里完全不存AClass的信息呢?这个也不是,它内部存储的是泛型向上父类的引用,比如:
GenericClass<AClass extends Charsequence>, 其编译后内部存储的泛型替代是Charsequence,而不是Object。
那么我们编码时的泛型的类型判断是怎么实现的呢?
其实这个过程是编译时检查的,也就是说限制gClassA.add(new BClass()) 这样的使用的方式的主体,不是运行时代码,而是编译时监测。
泛型的意义就在于,对所有其支持的类型参数,有相同的行为,从而可以被当作不同类型使用;类的静态变量和方法在所有实例间共享使用,所以不能使用泛型。
5、泛型与instanceof
泛型擦除了类型信息,所以使用instanceof检查某个实例是否是特定类型的泛型类是不可行的:
GenericClass genericClass = new GenericClass<String>();
if (genericClass instanceof GenericClass<String>) {} // 编译错误
同时:
GenericClass<String> class1 = (GenericClass<String>) genericClass; //会报警告
六、Class与泛型(摘自网络)
从Java1.5后Class类就改为了泛型实现,Class类内定义的泛型T指的是Class对象代表的类型。比如说String.class类型代表Class<String>,People.class类型代表Class<People>。
主要用于提高反射代码的类型安全。
Class类的newInstance返回泛型T的对象,故而可以在反射时创建更精确的类型。
举例来说:假定你要写一个工具方法来进行一个数据库查询,给定一个SQL语句,并返回一个数据库中符合查询条件
的对象集合(collection)。
一个方法是显式的传递一个工厂对象,像下面的代码:
interface Factory<T> {
public T[] make();
}
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;
}
你可以这样调用:
select(new Factory<EmpInfo>() {
public EmpInfo make() {
return new EmpInfo();
}
} , ”selection string”);
也可以声明一个类 EmpInfoFactory 来支持接口 Factory:
class EmpInfoFactory implements Factory<EmpInfo> {
...
public EmpInfo make() {
return new EmpInfo();
}
}
然后调用:
select(getMyEmpInfoFactory(), "selection string");
这个解决方案的缺点是它需要下面的二者之一:
调用处那冗长的匿名工厂类,或为每个要使用的类型声明一个工厂类并传递其对象给调用的地方
这很不自然。
使用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;
}
}
但是这不能给我们返回一个我们要的精确类型的集合。现在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;
}
籍此以类型安全的方式获取我们需要的集合。
这项技术是一个非常有用的技巧,在处理注释(annotations)的新API中被广泛使用。
七、容器与泛型
Java泛型的最深入人心的应用就是容器(Collections)了。容器不需要考虑它要装什么东西,它的职责就是表达它装的东西的集合所具有的功能。因此是天然的泛型支持者。
在没有泛型时,如果要封装一个列表,简化应该是这样的:
public class ArrayList {
Object[] array = new Object[10];
int i = 0;
public void add(Object object) {
array[i++] = object;
}
public Object get(int index) {
return array[index];
}
}
这意味着我们把元素存进去,取出来还要强转,类型安全无法保证(存入一个Integer再存一个Long,转出时强转成Integer就崩溃了)。用泛型可以在编译时保证不能存入非泛型支持的数据,保证类型安全。
按照我们之前说的,ArrayList内不存储泛型信息,而是存储泛型的最近父类,对ArrayList<T>而言就是Object,所以其内部代码是:
public class ArrayList<T> {
Object[] array = new Object[10];
int i = 0;
public void add(T object) {
array[i++] = object;
}
public T get(int index) {
return (T)array[index];
}
}
保证我们加进去和取出来的数据都是经过类型检查的。
八、总结
1、泛型是Java为类型安全而做的一个优化,它在内部保证了数据类型一致性,细化了可变参数的类型,且能更好的表示类型间的相关性。
2、泛型是编译时技术,其起作用的时机是编译时,完善的编译器会报告错误的泛型使用,保证代码不能编译通过。
3、平常写代码时,要认真思考是否有使用泛型的必要。通常来讲,如果方法或类描述的是数据类型无关的逻辑,且其数据类型可变时,则应该使用泛型。
参考: