我们知道一般的类和方法在定义的时候,其成员变量、参数、返回值等都必须指明具体的类型,要么是基本的数据类型,要么是自定义的类。如果需要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。而泛型正是为了解决这个问题而产生的,其含义即“可以应用于许多许多类型”,它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型具有更少的限制,因此单个的代码段可以应用于更多的类型上。
public class Container {
private T a;
public Container(T a) { this.a = a;}
public void set(T a) { this.a = a;}
public T get() { return a;}
public static void main(String[] args) {
Container c = new Container("Hello World!");
System.out.println(c.get());
// c.set(250); 无法编译
}
}
在Container的定义中,对象a的类型是T,即a可以被申明为任何类型(T),但是系统在编译Container的时候,实际把a当成的是一个Object对象。在我们运行程序实例化Container后,T就被限定为String类型,当我们想传入一个整型的数据时编译器就报错。
public class MultiArgs {
/**泛型方法**/
public static List f(T...args) {
List result = new ArrayList();
for(T item:args) {
result.add(item);
}
return result;
}
public static void main(String[] args) {
System.out.println(f("a","b","d"));
System.out.println(f(1,3,5,100,23));
}
}
定义泛型方法,只需要将泛型参数列表置于返回值之前,这里MultiArgs并不是泛型的,只是包含一个泛型方法。泛型方法f()传入不定数量、不定类型的参数,返回一个ArrayList。这里有一个需要注意的地方,在使用泛型类时,必须在创建对象的时候指定类型参数的值,如上面Container实例化时指定参数类型为String类型;而使用泛型方法的时候,通常不必指定参数类型,如f(“a”,”b”,”d”),编译会为我们找出具体的类型,这称为类型参数推断。
泛型是自Java SE5引入的,其引入的最主要目的是为了创造容器类,来确保我们存入容器的对象都是同一种类型,因此Java的泛型相对于C++的泛型来说有很大的局限性,我们通过下面的代码来说明:
/***C++中的泛型***/
class People {
void name() { cout << "Tom" << endl; }
}
template<class T> class Test {
T obj;
public:
Test(T t) { obj = t;}
void test() { obj.f(); }
};
int main() {
People p;
Test temp(p); //实例化
temp.test();
}
/***Java中的泛型***/
class People {
void name() { System.out.println("Kimi"); }
}
public class Test<T> {
private T obj;
public Test(T t) { obj = t;}
void test() {
//obj.f() 编译无法通过,因为obj是Object对象,并没有f()方法
}
public static void main(String[] args) {
People p = new People();
Test temp = new Test(p);
p.test();
}
}
可以看到在Java代码中,由于在代码在编译期进行类型检查,编译器无法获知obj是否含有test()方法,因此编译无法通过;而在C++代码中,C++在实例化这个模版时进行检查,Test被实例化的一刻,它看到People拥有一个方法test(),因而程序可以正常运行。
Java泛型是使用“擦除”来实现的,这意味着你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。由于有了擦除,Java编译器无法将test()必须能够在obj上调用f()这一需求映射到People必须有f()这一事实上。也正是因为擦除,像下面这些在运行时需要知道确切类型信息的操作都将无法工作:
public class Erased {
private final int SIZE = 100;
public static void f(Object arg) {
if(arg instanceof T) {} //Error
T var = new T(); //Error
T[] array = new T[SIZE]; //Error
T[] array = (T)new Object[SIZE]; //Unchecked warning
}
}
一种折衷的办法是给泛型参数T指定边界,如
public class Test<T extends People> {
private T obj;
public Test(T t) { obj = t;}
void test() {
obj.f()
}
public static void main(String[] args) {
People p = new People;
Test temp = new Test(p);
p.test();
}
}
在指定参数T继承至People后,代码中的obj对象将不仅仅是Object对象,而是People对象,而People对象必定包含f()方法,因此编译就可以通过了。指定边界其实变相的降低了代码的通用性,这实际就不能算是真正的泛型了,只是一种多态的运用,相当于在凡是需要说明类型的地方,我们都使用基类进行说明。
如果既想保留代码的泛化能力,又能够实现f()的调用,利用Java反射机制也是可以实现的
public class Test {
private T obj;
public Test(T t) { obj = t;}
void test() {
Class c = obj.getClass();
try{
Method m = c.getMethod("f");
m.invoke(obj);
}catch(Exception e) {
System.out.println(c.getSimpleName + " doesn't have f()")
}
}
public static void main(String[] args) {
People p = new People;
Test temp = new Test(p);
p.test();
}
}