Java泛型
1 为什么使用泛型
为什么使用泛型?首先,我们必须知道什么是泛型。泛型,简单来说,就是将类型参数化,即用一个参数来代表类型。比方说,在学习泛型之前,我们定义的变量都是指明了具体类型的,如String str定义了字符串类型的str, Integre num;定义了整型的num等(包括Object类型)。那么学习了泛型之后,我们就可能会定义类似T a;这种不指明具体类型的变量。a的类型随着传入参数的不同而变化。
比方说Java集合类List<E>就是一个泛型类(准确说是接口),里面不仅可以装Integer类型的数据,也可以装String类型的数据,以及用户自定义的对象等等。
因此,编写泛型程序意味着:1.我们的代码可以被不同类型的对象所重用;2.比随意地使用Object变量具有更好的安全性(比如避免java.lang.ClassCastException)和可读性(消除源代码中的许多强制类型转换,所有的强制转换都是自动和隐式的);3.性能较高,泛型代码可以为java编译器和虚拟机带来更多的类型信息,这些信息对java程序做进一步优化提供条件。
2 泛型类
一个泛型类是具有一个或多个类型变量的类。类型变量使用大写形式。在Java库中,T(需要时还可以用临近的字面U和S)表示任意类型。在我们写泛型类时,用T、U等只是习惯用法,其它的A、B、C等都是可以的。
比如,下面代码定义了具有一个类型变量T的泛型类。
public class Computer<T> {
private T data;
public Computer() {
}
public Computer(T data) {
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
下面代码定义了具有两个类型变量T和U的泛型类:
public class Computer<T,U> {
private T data;
private U com;
public Computer() {
}
public Computer(T data, U com) {
this.data = data;
this.com = com;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public U getCom() {
return com;
}
public void setCom(U com) {
this.com = com;
}
}
类型变量用<>括起来,放在类名的后面。类型变量可以有一个或多个,如果有多个时,中间用逗号隔开。
在实际的程序当中,类型参数用在什么地方呢?泛型类中的类型变量,主要用在三个地方,一是指定方法的返回类型,二是指定域的类型,三是指定局部变量的类型。
指定方法的返回类型:比如前面程序里的getXxx方法。
指定域的类型:比如前面程序变量的定义T xxx。
指定局部变量的类型:比如前面程序setXxx和getXxx方法里的参数。
用具体的类型替换类型变量就可以实例化泛型类型。
我们仍用上面的Computer类来说明。
Computer<String> com = new Computer<String>();
Computer<String ,String> computer = new Computer<String ,String>("abc","def");
或Computer<String ,String> computer = new Computer("asd","bcv"); //后面构造函数的<String ,String>可以不写。
3 泛型方法
泛型方法时一个带有类型参数的简单方法。泛型方法可以定义在普通类中,也可以定义在泛型类中。类型变量放在修饰符的后面,返回类型的前面。
下面这个例子展示了如何在普通类中定义泛型方法:
public class Phone {
//定义泛型方法
public static <T> String fun(T t){
return t.toString();
}
public static void main(String[] args) {
//当调用一个泛型方法时,在方法名前的<>中放入具体的类型
String str1 = Phone.<Integer>fun(8);
System.out.println(str1);
//实际上大多数情况下,也是编译器推荐的方式,方法名前的<>是省略的,如下方式
String str2 = Phone.fun("hello");
System.out.println(str2);
}
}
4 类型变量的限定
像上面这样,我们会写一个泛型类、泛型方法,以及调用使用他们,基本在日常的开发中就没有什么问题了。当然,如果想要了解泛型的更多东西,还可以继续往下看。
这里,我们说一下类型变量的限定。所谓类型变量的限定,就是给类型变量加上约束,比如类型参数必须实现某个接口,继承某个类等,而不是让类型参数可以任意取值。
我们一泛型方法为例,比如:
定义一个Father类
public class Father {
public void hunt(){
System.out.println("I'm good at hunting");
}
}
定义一个儿子类:
public class Son {
public static <T> void fun(T t){
//下面这句编译器会报错
t.hunt();
}
}
如上,在Father类中,有一个hunt()方法。在Son类里,有一个泛型方法fun,并调用了hunt()方法。程序显然是错误的,因为我们不知道T是什么类型,它有没有hunt()方法,直接t.hunt()肯定不行,而且编译器也会直接给我们报错。
可是,如果我们必须要让t具有hunt()函数,即函数fun(T t);必须以t。hunt()的方式调用hunt函数,那该怎么办呢?很简单,因为hunt()方法在Father类中定义,我们只要让传入的参数t是Father类型或其子类类型就可以了,也就是,我们传入的这个参数t,不能再是任意类型,必须让它继承Father类。
所以,上述代码改成下面这样:
public class Son {
//<T>改为<T extends Father>
public static <T extends Father> void fun(T t){
t.hunt();
}
}
这样,我们就可以知道,如果你想调用Son里的fun(T t);函数,那么T必须extends Father,即传入的参数必须的Father及其子类型,不然没法调用。这样我们就不用担心t.hunt()的调用会出错了。
一个类型变量可以有多个限定,例如:
T必须同时继承(或实现)A、B 、C三个类(或接口):<T extends A & B & C>
T必须同时继承(或实现)A、B 、C三个类(或接口),U必须同时继承(或实现)D、E两个类(或接口):<T extends A & B & C, U extends D & E>
限定类型用 & 分隔,类型变量用逗号分隔。
需要注意的是:
(1)无论的类型变量需要继承某个父类还是实现某个接口,统统用关键字extends,而不能用implements。
(2)类型参数可以指定多个限定接口,但只能指定一个限定类,如果有限定类,限定类必须放在限定列表的第一个。比方说,Father和Animal类是我们自定义的两个类,Comparable和List是Java自带的接口。
<T extends Father & List & Comparable>是可以的,完全没问题,
<T extends Father & Animal>是不可以的,因为限定类只能有一个,不能是Father和Animal两个,
<T extends List & Comparable & Father>是不可以的,因为List和Comparable是接口,Father是类,类必须放在限定列表的第一个。
另外,
<? extends T>表示包括T在内的任何T的子类,属于子类型限定。
<? super T>表示包括T在内的任何T的父类,属于超类型限定。
5 擦除
参考文章 http://blog.csdn.net/lonelyroamer/article/details/7868820
使用泛型的时候加上的类型参数,编译器在编译时会去掉,在生成的Java字节码中是不包含泛型中的类型信息的。这个过程就称为类型擦除。
因此,事实上,虚拟机并不知道泛型,无论是Computer<String>,还是Computer<Integer>,在JVM的眼里,统统是Computer。所有的泛型在编译阶段就已经被处理成了普通类和方法,即我们所说的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定类型的变量用Object)。
比如我们写的第一个泛型类Computer<T>的原始类型如下:
public class Computer {
//因为T是一个无限定的类型变量,所以直接替换为Object
private Object data;
public Computer() {
}
public Computer(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
结果就是普通的类,和引入泛型之前,我们使用Object一样。
类型变量如果没有限定,就用Object替换,如果有限定类型,就替换为限定类型,如果限定类型有多个,就替换为第一个限定类型。
其实,我们还可以手动验证一下类型擦除。
为了方便,我直接使用Java自带的泛型类List,代码如下:
public static void main(String[] args) {
ArrayList<String> stringList = new ArrayList<String>();
ArrayList<Integer> integerList = new ArrayList<Integer>();
System.out.println(stringList.getClass()==integerList.getClass());
}
运行这个main方法,控制台会打印true。这说明,JVM在运行的时候,类型变量已经擦除了,它所知道的只有List。
我们还可以以另一种方式验证:
public static void main(String[] args) {
ArrayList<Integer> arrayList=new ArrayList<Integer>();
arrayList.add(1);//这样调用add方法只能存储整形,因为泛型类型的实例为Integer
try {
//通过反射获取类的运行时信息可以存储字符串
arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, "Hello!");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
System.out.println(arrayList);
}
运行这个main函数,控制台会打印:[1, Hello!]
简直震惊!我们竟然在一个整型数组列表ArrayList<Integer>里存进了一个字符串"Hello!"。不用惊讶,事实本该如此。因为类型擦除,类型变量被替换为Object,字符串"Hello!"自然可以存进去。
禅语:一直以来我们找不到对的人,是因为我们不能改变错误的自己!