一、什么是泛型?
“泛型”这个术语的意思就是:适用于多种数据类型。其目的是使类或者方法拥有更广阔的表达能力,通过解耦类或者方法与所用类型之间的约束来实现该目的。通过使用泛型,我们可以极大地提高代码的可复用性,避免冗杂的代码。当我们在编写泛型程序时,应跳出数据类型的约束,把注意力集中在程序本身的数据结构上。
二、Java泛型机制和C++的不同
首先我们先来看一段C++代码(摘自《Java编程思想》(第四版)):
#include<iostream>
using namespace std;
template<class T>
class M{
private:
T object;
public:
M(T x){
obj=x;
}
void manipulate(){obj.f()};
};
class HasF{
public:
void f(){cout << "HasF::f()" << endl}
};
int main(){
HasF hf;
M<HasF> man(hf);
man.manipulate();
}
这段代码没有任何问题,可以通过编译。这是因为C++实例化模板时,编译器会对其进行检查。也就是说当模板被实例化时,该模板就可以知道其模板参数的类型了,因而manipulate()方法可以正确工作。
但将该代码转化为Java代码则不能通过编译。
class HasF{
public void f(){System.out.println("HasF.f()")}
}
class M<T>{
private T object;
public M(T x){
obj=x;
}
//这里会报错
public void manipulate(){obj.f()};
}
public class Test{
public static void main(String[] args){
HasF hf=new HasF();
M<HasF> mm=new M<HasF>(hf);
mm.manipulate();
}
}
之所以有这样的现象,是因为Java泛型是通过擦除来实现的。也就是说当你使用一个Java的泛型程序时,具体的类型信息会被擦除,编译器唯一知道的就是你正在使用一个对象,在泛型代码的内部,无法得知任何有关泛型参数的信息。上述manipulate()方法中,由于擦除,编译器无法得知obj就是一个HasF对象,故而调用f()方法时会报错。
Java泛型类型参数会擦除到它的第一个边界,也就是进行了一种类似于“向上转型”的操作。比如在上述代码中,若将class M< T > 改为 class M< T extends HasF>,就可以在该泛型类内部使用obj安全地调用f()方法了。这是因为编译器实际上会把参数类型替换为它擦除后的类型。
三、类型参数的擦除
首先我们先来看一下下面的代码
class TestList <T>{
List<T> getList(){
return new LinkedList<T>();
}
}
由于有擦除机制,我们知道new LinkedList< T>在运行时不会保留有关T类型的任何信息。所以我们自然而然地想到如果把< T>给去除呢?也就是下面这样:
class TestList <T>{
List<T> getList(){
return new LinkedList();
}
}
这样做的话我们会从编译器得到一个警告。更极端的情况,我们去除掉getList方法中所有类型参数,如下:
public class TestList <T>{
List getList(T t){
List l=new LinkedList();
l.add(t);
l.add("String");
l.add(new TestList());
return l;
}
@Override
public String toString(){
return "I'm TestList";
}
public static void main(String[] args){
TestList<Integer> tl=new TestList<Integer>();
List<Integer> li=tl.getList(1);
System.out.println(li);
}
}
得到输出[1, String, I’m TestList]。在getList返回的List中我们可以看到:我们向LinkedList中插入了Integer、String、TestList三种不同类型的数据。也就是说失去类型参数< T>后,你无法保证插入数据的类型是统一的,这是不被允许的。若改为如下代码:
public class TestList <T>{
List<T> getList(T t){
List<T> l=new LinkedList<T>();
l.add(t);
//这里会报错
l.add("String");
//这里会报错
l.add(new TestList());
return l;
}
@Override
public String toString(){
return "I'm TestList";
}
public static void main(String[] args){
TestList<Integer> tl=new TestList<Integer>();
List<Integer> li=tl.getList(1);
System.out.println(li);
}
}
当你插入不同的数据类型时,编译器会报错。所以我们可以得知:即使编译器无法知道T具体的类型,但它仍可以在编译期保证放置到 l 中的对象具有T类型,使其存储到l中的对象保持内部一致性。
由此我们引入边界的概念。所谓的边界,就是“对象进入和离开方法的地点”。
考虑下面的泛型实例(摘自《Java编程思想(第四版)》):
public class SimpleHolder{
private Object obj;
public void set(Object obj){this.obj=obj;}
public Object get(){return obj};
public static void main(String[] args){
SimpleHolder holder = new SimpleHolder();
holder.set("Item");
String s = (String) holder.get();
}
}
反编译该类:
public void set(java.lang.Object);
0: aload_0
1: aload_1
2: putfield #2; //Field obj:Object
5: return
public java.lang.Object get();
0: aload_0
1: getfield #2; //Field obj:Object
4: areturn
public static void main(java.lang.String[]);
0: new #3; //class SimpleHolder
3: dup
4: invokespecial #4; //Method "<init>" : ()V
7: astore_1
8: aload_1
9: ldc #5;
11: invokevirtual #6; //Method set:(Object;)V
14: aload_1
15: invokevirtual #7; //Method get:()Object
18: checkcast #8; //class java/lang/String
21: astore_2
22: return
从中我们可以看到:向该类取出值的时候会接受检查。
将该类转化为泛型类:
public class GenericHolder<T>{
private T obj;
public T get(){return obj;}
public void set(T t){obj = t;}
public static void main(String[] args){
GenericHolder<String> holder = new GenericHolder<String>();
holder.set("item");
String s=holder.get();
}
}
反编译该类
public void set(java.lang.Object);
0: aload_0
1: aload_1
2: putfield #2; //Field obj:Object
5: return
public java.lang.Object get();
0: aload_0
1: getfield #2; //Field obj:Object
4: areturn
public static void main(java.lang.String[]);
0: new #3; //class SimpleHolder
3: dup
4: invokespecial #4; //Method "<init>" : ()V
7: astore_1
8: aload_1
9: ldc #5;
11: invokevirtual #6; //Method set:(Object;)V
14: aload_1
15: invokevirtual #7; //Method get:()Object
18: checkcast #8; //class java/lang/String
21: astore_2
22: return
得到了一模一样的字节码。通过对SimpleHolder和GenericHolder的比较,我们可以粗略地看出泛型的实现原理。从中我们可以得知 : 在从泛型中取出一个值时,实际上会对泛型传出的值进行一次转型(通常这由编译器自动插入),对于传入泛型的值,会进行一次额外的编译期检查。
Java泛型代码的内部虽然无法得知具体的类型,但通过对传入的值的额外编译期检查和传出值自动转型它依然实现了泛型的功能。
四、擦除带来的不便
由于擦除,泛型代码丧失了某些功能。在泛型代码内部,任何需要知道具体类型信息的操作都会被拒绝。例如:
1.T obj=new T();
2.T[] arr=new T[Size];
3.ele instanceof T
解决方法:
1.当需要在泛型内部创建实例对象时,可以通过传入一个Class对象,再使用newInstance()来创建该类的对象:
class GenericClass<T>{
private T ele;
public GenericClass(Class<T> c){
try {
ele=c.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
该方法有局限性,当该T类没有任何默认构造器时会失效。还有一种方法是使用工厂模式,这里我不再赘述。
2.创建泛型数组时,可以通过传入一个Class对象,并使用Array.newInstance(Class,int)方法,示例:
class GenericClass<T>{
private T[] eles;
public GenericClass(Class<T> c,int size){
eles=(T[])Array.newInstance(c, size);
}
public T[] ret(){
return eles;
}
}
public class GenericArray {
public static void main(String[] args){
GenericClass<String> gc=new GenericClass<String>(String.class,10);
String[] s=gc.ret();
}
}
3.使用动态的isInstance()
class GenericClass<T>{
private Class<T> kind;
public GenericClass(Class<T> c){
kind=c;
}
public boolean judge(Object obj){
return kind.isInstance(obj);
}
}
public class GenericInstance {
public static void main(String[] args){
GenericClass<String> gc=new GenericClass<String>(String.class);
System.out.println(gc.judge("abc"));
System.out.println(gc.judge(123));
}
}