面向对象相对于面向过程而言,是软件领域的一个重大进步。面向对象的多态特性,使得系统具有了较好的扩展性,通常我们使用父类来代替具体的类型,在实际运行时,却可以使用子类的对象。Java又更进了一步,提倡面向接口的编程,我们指定接口而不是具体的实现类。这样的约束有时候还是太强,我们希望编写更通用的代码,使代码能够用运行于“某种不具体的类型”,而不是具体的接口或类。
泛型的概念
假设我们要实现一个类,这个类持有一个属性,为了灵活,我们希望这个属性可以是任何类型,如果这样的话,这个属性就应该定义为Object类型的,其实现代码如下:
public class Holder {
private Object obj;
public void setObj(Object obj) {
this.obj = obj;
}
public Object getObj() {
return obj;
}
}
在实际使用的时候,不管传递进来的参数是什么类型,在Holder类里都被向上转型为Object类型了;在希望获得持有的属性时,也仅仅能得到Object类型,必须要使用类型转换
Holder holder = new Holder();
holder.setObj("cxy");
//显式的类型转换
String name = (String)holder.getObj();
我们可以看到,get()返回的是Object类型,但其实我们希望得到更具体的类型,这个时候就要使用类型转换。问题是,类型转换的任务需要有程序员来完成,这意味着程序要需要知道Holder实际持有的是什么类型的。如果在类型转换时发生了错误,必须要等到运行期才能发现。
holder.set(new Integer(1));
//错误的类型转换
String name = (String)holder.get();
我们希望错误发现的越早越好。泛型就是用来保证类型的正确性的,在定义类时,使用一个占位符,代表类型信息,通常使用T来表示。上面的代码如果使用泛型来写:
public class Holder<T> {
private T obj;
public void set(T t) {
obj = t;
}
public T get() {
return obj;
}
}
在使用的时候,只需要用实际的类型来代替T,编译器会确保传递和获得时的类型正确性。如下面的代码所示:
//只能持有String类型
Holder<String> holder = new Holder<String>();
holder.set("cxy");
//编译器会确保返回的是String类型
String name = holder.get();
//如果试图传入非String类型的参数,编译无法通过
//holder.set(new Integer(1));
就是这么简单,类型的正确性由编译器来处理,但其实Java中的泛型远比想象中的要复杂,上面只是泛型最简单的用法,泛型出了可以和普通类配合使用之外,还可以和接口、内部类一起使用,可以写一个没有实际意义的代码来演示一下:
//泛型接口
public interface GenericIn<T> {
public void f();
}
//泛型匿名内部类
public class Outer<T> {
public GenericIn<T> getGeneric() {
return new GenericInt<T>() {
public void f() {
do something
}
};
}
}
泛型方法
泛型类在定义时使用一个占位符来表示类型,这个类型只有在具体构造该类的实例时才会知道,所以static方法和static字段都不能使用泛型类的类型参数,如果static想要拥有泛型能力,必须要借助于其他的机制。还有,有时候我们仅仅希望某个方法拥有泛型能力,而不是整个类,这样更加方便灵活,不用为了局部的泛型需要,而必须要在所有使用该的地方都使用泛型。出于以上两个原因,泛型方法就被提出来了。泛型方法的定义很简单,就是把类型参数放到方法的返回值之前:
public <T> void f();
public <T> T f(T t);
这样在方法f内部就可以使用类型T了。泛型方法是独立于泛型类而产生变化的,不是泛型类也可以拥有泛型方法;泛型方法可以使用额外的类型信息:
public class Generic<T> {
//泛型方法使用额外的类型信息
public <K> K f(K k) {
return k;
}
}
在创建泛型类时必须要显式的指定类型信息,而在使用泛型方法时则不用,编译器会自动找出具体的类型,这称为
类型推断(type argument inference)。比如
//编译器会自动推断出类型是String
String value = xxx.f("value");
但是类型推断只有在赋值时有效,在其他的时候,类型参数并不起作用,有如下代码:
public static <K,V> Map<K,V> map() {
return new HashMap<K,V>();
}
static void f(Map<String, String> map){}
如果这样调用是通不过的:
f(map());
因为不是赋值操作,所以编译器会认为map()返回的是Map<Object,Object>。这个时候可以使用显式的类型说明来解决此问题,显式的类型说明就是在方法调用者和方法之间指定类型信息,如果是在类内部调用实例方法,需要使用this,如果是类方法,需要使用类名,其形式如下:
f(ClassName.<Map<String,String>>map());
以上只是泛型的基本用法,Java的泛型远比想象的要复杂,Java的泛型并不是真正的泛型,而是使用擦除来实现的,泛型所有的动作都是由编译器来完成的,在实际运行时,虚拟机根本不知道泛型的存在,具体内容可以参考《泛型-擦除实现的Java泛型》。
转载请注明:喻红叶《泛型-Java泛型基础》