您的位置:首页 > 编程语言 > Java开发

java基础加强--泛型(Generic)的应用

2011-02-20 21:19 495 查看
泛型是JAVA 1.5的新特性,引入泛型主要是为了解决数据的类型安全问题。在类的声明时
通过使用一个标识符号(如<T>、<K,V>、<?>等)添加在类名,属性,方法(方法名,参数
以及返回值)上。这样用户在声明或实例化时只要指定好需要的具体类型即可。1.5后集合
类和反射都增加了泛型的支持,先从集合类的一个例子来直观了解泛型的作用和使用方法。
例:

package cn.hxq.generic;

import java.lang.reflect.*;
import java.util.ArrayList;

public class GenericTest {
@SuppressWarnings("unchecked")
public static void main(String[] args) throws Exception{

//1.5以前的集合使用:集合中可以加入多种类型,导致使用时类型出错
ArrayList collection1 = new ArrayList();
collection1.add(1);
collection1.add(1L);
collection1.add("abc");
//如果这样拿出来用就出错了:
//int i = (Integer)collection1.get(1);

//使用泛型改写上面的集合
ArrayList<String> collection2 = new ArrayList<String>();
//此时下面两个内容将不能加入到集合中,保证了集合只能存储String类型的对象
//collection2.add(1);
//collection1.add(1L);
collection2.add("abc");
String element = collection2.get(0);

//反射中也可以使用泛型:
Constructor<StringBuffer> construtor = StringBuffer.class.getConstructor(String.class);
StringBuffer sbr = construtor.newInstance(element);
Method method = sbr.getClass().getMethod("append", String.class);
System.out.println(method.invoke(sbr, element));
}
}


通过上面的例子可以看出,使用泛型后,编译器会通过声明的泛型来限定集合中的元素类型,
用户如果加入错误的类型,就会报错。通过这样的方式就可以保证集合中的类型一致。当用
户在集合中取出一个元素时,也不需要进行强制类型转换了,因为编译器已经知道了集合中
的元素只能是某个用户指定的类型。

泛型的内部作用:
泛型其实是提供给JAVAC编译器使用的,可以限定集合中的输入类型,让编译器挡住源程序
中的非法输入,编译器编译带类型说明的集合时会去掉“类型”信息,使程序运行效率不受
影响,对于参数化的泛型类型,getclass()方法的返回值类型和原始类型一样。由于编译器
编译器产生的字节码会去掉泛型的类型信息,只要能跳过编译器,就可以往某个泛型集合中
加入其它类型的数据。例如,用反射得到集合,再调用其ADD方法。

例2:

import java.util.*;
import java.lang.reflect.*;

public class ReflyectGeneric {
public static void main(String[] args) throws Exception {
List<String> list1 = new ArrayList<String>();
List<Integer> list2 = new ArrayList<Integer>();
//使用反射得到ArrayList集合的字节码,看是否是同一份
System.out.println(list1.getClass() == list2.getClass());
//通过反射调用集合的add方法来往集合中加入不同类型的元素
Method list1Method = list1.getClass().getMethod("add",Object.class);
list1Method.invoke(list2, "abc");
System.out.println(list2.get(0));
}
}


输出结果为abc

但是写成这样就报错了!
例3:

package cn.hxq.generic;

import java.util.*;
import java.lang.reflect.*;

public class ReflyectGeneric {
public static void main(String[] args) throws Exception {
List<String> list1 = new ArrayList<String>();
List<Integer> list2 = new ArrayList<Integer>();
//使用反射得到ArrayList集合的字节码,看是否是同一份
System.out.println(list1.getClass() == list2.getClass());
//通过反射调用集合的add方法来往集合中加入不同类型的元素
Method list1Method = list1.getClass().getMethod("add",Object.class);
list1Method.invoke(list1, 1);
System.out.println(list1.get(0));
}
}


将会报出:Exception in thread "main" java.lang.ClassCastException:
java.lang.Integer cannot be cast to java.lang.String at
cn.hxq.generic.ReflyectGeneric.main(ReflyectGeneric.java:16)

我的猜想:
System.out.println(list1.get(0));
在list1.get(0)这里,由于编译器认为返回的对象类型应该为String,但我因为
使用了反射在集合中添加了Integer类型的对象,list1.get(0)给出的其实
是Integer。于是我再想:System.out.println不是会自动调用toString()方法吗?
后来发现,我的声明是List<String> list1
里面装的肯定是String,get()方法返回的肯定是String,那编译器肯定不能对一个
String类型的对象去调用toString()方法。于是编译器就相当于去做了这么一件事:
Integer i = 1;
String str = i;
那样就肯定报错了,改成以下两句就可以了。
Object obj = list1.get(0);
System.out.println(obj);
我想原来错误就是由于System.out.println语句没法调用toString方法的缘故。
查阅JDK文档我却赫然发现:String类竟然也有toString()方法!那么前面的推断是
错的吗?(String 的toString()方法的解释很搞笑!)

我于是再试了一下这样的方式:

package cn.hxq.generic;

import java.util.*;
import java.lang.reflect.*;

public class ReflyectGeneric {
public static void main(String[] args) throws Exception {
List<String> list1 = new ArrayList<String>();
List<Integer> list2 = new ArrayList<Integer>();
//使用反射得到ArrayList集合的字节码,看是否是同一份
System.out.println(list1.getClass() == list2.getClass());
//通过反射调用集合的add方法来往集合中加入不同类型的元素
Method list1Method = list1.getClass().getMethod("add",Object.class);
list1Method.invoke(list2, "abc");
System.out.println(list2.get(0).toString());
}
}


这时我使用了list2,我指定的集合类型是ArrayList<Integer>,通过反射加入
了"abc"这个String对象,从例2来看,直接System.out.println(list2.get(0));
这样可以很好的打印出abc。但这次我显示地加入了toString()方法。让人吃惊的
是,报错了:Exception in thread "main" java.lang.ClassCastException:
java.lang.Integer cannot be cast to java.lang.String at
cn.hxq.generic.ReflyectGeneric.main(ReflyectGeneric.java:16)。

还是和以前一样的异常类型。这样可以证明我前面的推论其实是错的。
想了好久也没想出来,后来请教了群里的同学,同学认为:问题不是出在
toString()是否被调用的前提下,而是在get()这个方法中。

同学的观点:

看看java源代码关于get()方法的定义:
public E get(int index) {
RangeCheck(index);

return (E) elementData[index];
}
这是一个返回值是泛型的方法,编译器在编译的list1.get(0)的时候,因为前面
我声明了List<String> list1,由于去泛型化的原因那么通过list1调用的get()
方法自然就被编译成:
public String get (int index){
return (String) elmentData[index];
}
在运行时,就需要返回一个String类型的值.
而且在return 的时候,会将我加入的Integer强制转换成String类型,于是就报了
java.lang.ClassCastException的错。其实就是类型转换的错误。

新衍生的问题:
那为什么System.out.println(list2.get(0))是正确的,
System.out.println(list2.get(0).toString())却又报错了呢?
如果按照上面的推论:
我声明了List<String> list2,我使用list2调用的get()方法应该会成为这样的形式:
public Integer (int index){
return (Integer) elmentData[index];
}
那么此时将String类型的元素强制转换成Integer的时候却又不报错了呢?再者,为什么一旦显式调用了toString(),却马上报错了?
这些问题实在思考不出来,先丢在一边··。

昨晚没有思考出来,今天早上突然有了灵感。
其实问题是出在get方法的return之后,get()方法本身只是通过return将集合中的引用传递出来
,println()方法接到引用之后,会对接到的引用根据类型去调用编译器在class文件中说明的
toString()方法,因为这个方法是被不同的子类覆写过的。

问题1的解答:
list1.get(0)编译器编译的时候认为它返回的是String值,于是在Class文件中标明了一旦
需要调用toString(),则会使用String类的toString()(实际上如果真是String类,JVM将不会调用)
运行时传递来了一个Integer类的对象,那么就需要调用toString()方法了。
此时JVM就去用一个Integer的对象调用了String类的toString()方法,因此报出类型转换的错误。
Object obj = list.get(0);
这一句很好地说明了get()方法本身内部在进行引用传递时并没有出错。
错在于接受引用的方法或者引用变量。
System.out.println(obj);
此时会调用obj的toString方法肯定是没错的,于是正确打印了。

问题2的解答:
System.out.println(list2.get(0));这样为什么正确呢?
因为println()实际上接到的引用是String类的,JVM没必要再调用编译器在class文件中声明的
Integer类的toString()方法,所以直接就能打印了。
System.out.println(list2.get(0).toString());
这样的显式调用为什么错误?这也是编译器在编译成class文件时造成的,因为
我显式调用了toString(),因为编译器认为我传递出来的是个Integer,所以在class文件
中声明的是Integer类的toString()方法。所以我显示调用时,实际上是用String()类的
对象去调用了Integer类的toString()方法。这样就出错了。

总结如下:
一、
list1Method.invoke(list1, 1);
System.out.println(list1.get(0));

错误:1)、get()方法运行时返回了一个Integer对象,println()方法无法打印。
2)、JVM对Integer对象的处理:去调用toString()方法。
3)、class文件中的信息让它去调用了一个String类的toString()方法。
4)、使用Integer对象调用了String类的toString()方法,出错了。

二、
list1Method.invoke(list2, "abc");
System.out.println(list2.get(0));

正确:1)、get()方法运行时返回了一个String对象,println()方法
接到String类对象。
2)、JVM对String对象的处理:不调用toString()方法。正确了。

三、
list1Method.invoke(list2, "abc");
System.out.println(list2.get(0).toString());

错误:1)、get()方法运行时返回了一个String对象,println()方法
接到String类对象。
2)、此时toString()方法被显式调用,由于class文件得到的类型信息,调用
的是Integer类的toString()方法。
3)、使用String对象去调用Integer类的toString()方法,出错了。

结论是:
使用一个类的对象 去调用了 另一个非父类的toString()方法,导致出错。
如果使用String类,默认是不会调用toString()方法,如果显式调用,会重复上面的
错误。

泛型的应用很广泛,应该牢牢掌握,但如果要深入了解泛型的实现,我想可不是一朝一
夕的事··。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: