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!"自然可以存进去。