首页 > Java > 高新技术(六) 泛型
2014
06-05

高新技术(六) 泛型

①了解泛型

知识点一、ArrayList<E> 类定义、ArrayList<Integer> 类引用中涉及如下术语:

|— ArrayList<E>称为泛型类型,其中的E称为类型变量或类型参数

|— ArrayList<Integer>称为参数化的类型,其中的Integer称为类型参数的实例或实际类型参数。

|— ArrayList<Integer>中的<>读作typeof

|— ArrayList称为原始类型(raw type)

知识点二、参数化类型与原始类型的兼容性:

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

Collection<String> c = new ArrayList();

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

Collection c = new ArrayList<String>();

知识点三:下面的代码会报错么?

ArrayList al1 = new ArrayList<String>(); //A.

ArrayList<Object> al2 = al1; //B.

答案:不会报错。

② 泛型的内部原理:类型擦除

Java中的泛型类似于C++中的模板,但是这种相似性仅限于表面。Java中的泛型是提供给编译器(javac)使用的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入(类型检查)。编译器编译带类型说明的集合时会去除掉“类型”信息,使程序运行效率不受影响,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样。

ArrayList<String> al1 = new ArrayList();
ArrayList<Integer> al2 = new ArrayList();
System.out.println(al1.getClass() == ArrayList.class);//输出true
System.out.println(al1.getClass() == al2.getClass());//输出true

由于编译生成的字节码会去掉泛型的类型信息,只要能跳过编译器,就可以往某个泛型集合中加入其他类型的数据,例如,用反射得到集合,再调用其add方法即可。

ArrayList<Integer> al = new ArrayList();
al.getClass().getMethod("add", Object.class).invoke(al,"abc");
System.out.println(al.get(0));

③ 泛型的通配符

问题:定义一个方法,该方法用于打印出任意参数化类型的集合中的所有数据,该方法如何定义呢?

	public static void printCollection(Collection<?> collection){
		for(Object obj : collection){
			System.out.println(obj);
		}

		collection.add("abc");//这儿会报错
		collection.size();//不会报错
		collection = new HashSet<String>();
	}

总结:使用?通配符可以引用其他各种参数化的类型。 ?通配符定义的变量主要用作引用,可以调用与类型参数无关的方法,不能调用与类型参数有关的方法。

扩展:

|— 限定通配符的上边界,如:

ArrayList<? extends Number> al = new ArrayList<Integer>();

|— 限定通配符的下边界,如:

ArrayList<? super Integer> al = new ArrayList<Number>();

④简单应用:用迭代的方法取出HashMap中的每一个数据。

HashMap<String,Integer> hm = new HashMap<String,Integer>();
hm.put("jy", 24);
hm.put("xt", 22);
hm.put("jn", 21);

Set<Map.Entry<String, Integer>> entrySet = hm.entrySet();
for(Map.Entry<String, Integer> entry : entrySet){
	System.out.println(entry.getKey() + ":" + entry.getValue());
}

特别要注意上例中Set<Map.Entry<String, Integer>>部分。

⑤ 定义泛型方法

第一个例子:add()方法。Java中的泛型方法没有c++模板函数功能强大,java中的如下代码无法通过编译:

<T> T add(T x, T y){
	return (T)(x + y);
}

第二个例子:swap()方法,交换数组中两个元素的位置:

public static <T> void swap(T[] arr , int i ,int j){
	T temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
}

注意:

|— 只有引用类型才能作为泛型的实际参数,swap(new int[]{1,2,3},2,1);语句会报编译错误。

|— 除了在使用泛型时可以使用extends限定符,在定义泛型时也可以使用extends限定符。

练习1、编写一个泛型方法,自动地将Object类型的对象转换成其他类型。

public static <T> T autoConvert(Object obj){
	return (T)obj;
}

在main()中使用该方法:

 

Object obj = "abc";
String str = autoConvert(obj);

练习2、定义一个方法,把任意参数类型的集合中的数据安全地复制到相应类型的数组中。

public static <T> void copy(Collection<T> src, T[] dest){
	…………
}

在main()中使用该方法: 

copy(new ArrayList<String>(),new String[10]);  //正确
copy(new ArrayList<Date>(),new String[10]);  //错误

练习3、定义一个方法,把任意参数类型的数组中的数据安全地复制到相应类型的另一个数组中。

public static <T> void copy(T[] src, T[] dest){
	…………
}

在main()中使用该方法:

copy(new Date[10],new String[10]);  //正确

注意:在练习3中,将一个Date数组复制到String数组中是可以的,此时,实际类型参数为Object;练习2中,ArrayList<Date>即确定了实际类型参数为Date,因此错误。个中原因请参考 ⑥类型推断。

 

⑥ 类型参数的类型推断:编译器判断泛型方法的实际类型参数的过程称为类型推断。

对于泛型方法中实际类型参数推断的具体规则如下:

|— 当某个类型变量在整个参数列表中的所有参数和返回值中多处被应用,且调用方法时这多处的实际参数类型都对应同一种类型,这很容易凭着感觉推断出来,例如:

add(3,5) —> static <T> T add(T a,T b)

|— 当某个类型变量在整个参数列表中的所有参数和返回值中多处被应用,且调用方法时这多处的实际参数类型对应不同的类型,且没有使用返回值,这时候取多个参数中的最大交集类型,例如:下面语句实际对应的类型就是Number了,编译没问题,只是运行时出问题:

fill(new Integer[3],5.5f) —> static <T> void fill(T[] a,T b)

|— 当某个类型变量在整个参数列表中的所有参数和返回值中多处被应用,且调用方法时这多处的实际参数类型对应不同的类型,且有使用返回值,这时候优先考虑返回值的类型,例如:下面语句实际对应的类型就是Integer,编译将报告错误;将变量x的类型改为float,对比eclipse报告的错误提示,接着再将变量x类型改为Number,则没有错误:

int x=add(3,3.5f)–>static <T> T add(T a,T b)

|— 参数类型的类型推断具有传递性,下面第一种情况推断实际参数类型为Object,编译没问题,而第二种情况则根据参数化的ArrayList类实例将类型变量直接确定为String类型,编译将出现问题:

copy(new Integer[5],new String[5])–>static <T> void copy(T[] a,T[] b)

copy(new ArrayList<String>(),new integer[5])–>static <T> void copy(Collection<T> a,T[] b)

⑦ 定义泛型类

如果类的实例对象中的多处都要用到同一个泛型参数,即这些地方引用的泛型类型要保持同一个实际类型时,这时候就要采用泛型类型的方式进行定义(类级别的泛型)。

泛型类中是根据引用该类名时指定的类型信息来参数化类型变量的,这和泛型方法中需要进行类型推断不同。

第一个例子:定义一个通用的DAO类,可以满足不同实体对象的CRUD操作。

扩展:
DAO (data access object) –> crud: create retrieve update delete (增删改查)
|–> DAO就是对某个实体对象的crud操作。

public class GenericDAO <E> {
	public void add(E e){

	}
	public E getById(int id){
		return null;
	}
	public Set<E> getByConditions(String keyword){
		return null;
	}
	public void update(E e){

	}
	public void delete(E e){

	}
	public void delete(int id){

	}
}

注意:

|— 在对泛型类进行参数化时,类型参数的实例必须是引用类型,不能是基本类型。

|— 当一个变量被声明为泛型时,只能被实例变量和方法调用,而不能被静态变量和静态方法调用。因为静态成员是被所有参数化的类所共享的,所以静态成员不应该调用类级别的类型参数。

问题:类中只有一个方法需要使用泛型,是使用类级别的泛型还是使用方法级别的泛型?

回答:类级别的泛型

⑧ 高级:通过反射获得泛型的实际类型

public class GetActualParam{
	public static void main(String[] args) throws Exception{
		//1、不能直接获得实际类型参数,可通过中间方法
		Method applyMethod = GetActualParam.class.getMethod("applyRef", HashMap.class);

		//2、得到方法参数中所有泛型:getGenericParameterTypes()方法。不能用getParameterTypes(),因为class中不包含泛型类型
		Type[] parameterTypes = applyMethod.getGenericParameterTypes();
		ParameterizedType pt = (ParameterizedType)parameterTypes[0];

		//3、本例方法中只有一个参数,该参数为一个泛型,得到该泛型的所有实际类型参数。
		Type[] actualTypeArguments= pt.getActualTypeArguments();

		//4、遍历这些实际类型参数
		for(Type t : actualTypeArguments){
			System.out.println(t);
		}
	}

	public static void applyRef(HashMap<String,Integer> hm){

	}
}

注意:

|— 有ArrayList<Date> al = new ArrayList<Date>(); 假如我们要想获得al的实际类型参数,不能通过al.getClass()这样的代码得到ArrayList<>里的参数化(类型擦除),此时必须写一个如applyRef的方法。


留下一个回复

你的email不会被公开。