概述

什么是泛型?为什么需要泛型?如何使用?是什么原理?如何改进?

什么是泛型

泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

为什么需要泛型

Java中引入泛型最主要的目的是将类型检查工作提前到编译时期,将类型强转(cast)工作交给编译器,从而让你在编译时期就获得类型转换异常以及去掉源码中的类型强转代码。例如
没有泛型前:

  1. private static void genericTest() {
  2. List arrayList = new ArrayList();
  3. arrayList.add("总有刁民想害朕");
  4. arrayList.add(7);
  5. for (int i = 0; i < arrayList.size(); i++) {
  6. Object item = arrayList.get(i);
  7. if (item instanceof String) {
  8. String str = (String) item;
  9. System.out.println("泛型测试 item = " + str);
  10. }else if (item instanceof Integer)
  11. {
  12. Integer inte = (Integer) item;
  13. System.out.println("泛型测试 item = " + inte);
  14. }
  15. }
  16. }

如上代码所示,在没有泛型之前类型的检查和类型的强转都必须由我们程序员自己负责,一旦我们犯了错(谁还能不犯错?),就是一个运行时崩溃等着我们。

有了泛型后:

  1. private static void genericTest2() {
  2. List<String> arrayList = new ArrayList<>();
  3. arrayList.add("总有刁民想害朕");
  4. arrayList.add(7); //..(参数不匹配:int 无法转换为String)
  5. ...
  6. }

如上代码,编译器在编译时期即可完成类型检查工作,并提出错误(其实IDE在代码编辑过程中已经报红了)

泛型作用的对象

泛型有三种使用方式,分别为:泛型类、泛型接口和泛型方法。

泛型类

在类的申明时指定参数,即构成了泛型类,例如下面代码中就指定T为类型参数,那么在这个类里面就可以使用这个类型了。例如申明T类型的变量name,申明T类型的形参param等操作。

  1. public class Generic<T> {
  2. public T name;
  3. public Generic(T param){
  4. name=param;
  5. }
  6. public T m(){
  7. return name;
  8. }
  9. }

那么在使用类时就可以传入相应的类型,构建不同类型的实例,如下面代码分别传入了String,Integer,Boolean3个类型:

  1. private static void genericClass() {
  2. Generic<String> str=new Generic<>("总有刁民想害朕");
  3. Generic<Integer> integer=new Generic<>(110);
  4. Generic<Boolean> b=new Generic<>(true);
  5. System.out.println("传入类型:"+str.name+" "+integer.name+" "+b.name);
  6. }

输出结果为:传入类型:总有刁民想害朕 110 true

如果没有泛型,我们想要达到上面的效果需要定义三个类,或者一个包含三个构造函数,三个取值方法的类。

泛型接口

泛型接口与泛型类的定义基本一致

  1. public interface Generator<T> {
  2. public T produce();
  3. }

泛型方法

这个相对来说就比较复杂,当我首次接触时也是一脸懵逼,抓住特点后也就没有那么难了。

  1. public class Generic<T> {
  2. public T name;
  3. public Generic(){}
  4. public Generic(T param){
  5. name=param;
  6. }
  7. public T m(){
  8. return name;
  9. }
  10. public <E> void m1(E e){ }
  11. public <T> T m2(T e){ }
  12. }

重点看public void m1(E e){ }这就是一个泛型方法,判断一个方法是否是泛型方法关键看方法返回值前面有没有使用<>标记的类型,有就是,没有就不是。这个<>里面的类型参数就相当于为这个方法声明了一个类型,这个类型可以在此方法的作用块内自由使用。
上面代码中,m()方法不是泛型方法,m1()与m2()都是。
值得注意的是m2()方法中声明的类型T与类申明里面的那个参数T不是一个,也可以说方法中的T隐藏了类型中的T。下面代码中类里面的T传入的是String类型,而方法中的T传入的是Integer类型。

  1. Generic<String> str=new Generic<>("总有刁民想害朕");
  2. str.m2(123);

泛型的使用方法

如何继承一个泛型类

如果不传入具体的类型,则子类也需要指定类型参数,代码如下:

  1. class Son<T> extends Generic<T>{}

如果传入具体参数,则子类不需要指定类型参数

  1. class Son extends Generic<String>{}

如何实现一个泛型接口

  1. class ImageGenerator<T> implements Generator<T>{
  2. @Override
  3. public T produce() {
  4. return null;
  5. }
  6. }

如何调用一个泛型方法

和调用普通方法一致,不论是实例方法还是静态方法。

通配符

通配符?

?代表任意类型,例如有如下函数:

  1. public void m3(List<?>list){
  2. for (Object o : list) {
  3. System.out.println(o);
  4. }
  5. }

其参数类型是?,那么我们调用的时候就可以传入任意类型的List,如下

  1. str.m3(Arrays.asList(1,2,3));
  2. str.m3(Arrays.asList("总有刁民","想害","朕"));

但是说实话,单独一个?意义不大,因为大家可以看到,从集合中获取到的对象的类型是Object 类型的,也就只有那几个默认方法可调用,几乎没什么用。如果你想要使用传入的类型那就需要强制类型转换,这是我们接受不了的,不然使用泛型干毛。其真正强大之处是可以通过设置其上下限达到类型的灵活使用,且看下面分解重点内容。

假设我们有如下两个类

  1. public class Parent {
  2. }
  3. public class Son extends Parent {
  4. }

通配符上界

通配符上界使用<? extends T>的格式,意思是需要一个T类型或者T类型的子类,一般T类型都是一个具体的类型,例如下面的代码。

  1. public void testExtends(List<? extends Parent> list) {
  2. for (Parent parent : list) {
  3. }
  4. // list.add("这里无法传入具体的类型,因为我们不知道有哪些类继承了Parent");
  5. // <? extends Parent> list只能提供值,我们不能消费list,即不能修改list
  6. }

这个意义就非凡了,无论传入的是何种类型的集合,我们都可以使用其父类的方法统一处理。

通配符下界

通配符下界使用<? super T>的格式,意思是需要一个T类型或者T类型的父类,一般T类型都是一个具体的类型,例如下面的代码。

  1. public void testSuper(List<? super Son> list) {
  2. for (Object o : list) {
  3. // o 只能是Object类型,所以可以认为list不能提供值,只能被消费,即修改list
  4. }
  5. list.add(new Son());
  6. }

通过如下方法可以测试

  1. public void test() {
  2. List<Son> sonList = new ArrayList<>();
  3. testExtends(sonList);
  4. List<Parent> parentList = new ArrayList<>();
  5. testSuper(parentList);
  6. }

至于什么时候使用通配符上界,什么时候使用下界,在 《Effective Java》
中有很好的指导意见:遵循PECS原则,即producer-extends,consumer-super. 换句话说,如果参数化类型表示一个生产者,就使用 <? extends T>;如果参数化类型表示一个消费者,就使用<? super T>。

泛型在静态方法中的问题

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数,例如下面的代码编译失败

  1. public class Test<T> {
  2. public static T one; //编译错误
  3. public static T show(T one){ //编译错误
  4. return null;
  5. }
  6. }

因为静态方法和静态变量属于类所有,而泛型类中的泛型参数的实例化是在创建泛型类型对象时指定的,所以如果不创建对象,根本无法确定参数类型。但是静态泛型方法是可以使用的,我们前面说过,泛型方法里面的那个类型和泛型类那个类型完全是两回事。

  1. public static <T>T show(T one){
  2. return null;
  3. }

Java泛型原理解析

为什么人们会说Java的泛型是伪泛型呢,就是因为Java在编译时擦除了所有的泛型信息,所以Java根本不会产生新的类型到字节码或者机器码中,所有的泛型类型最终都将是一种原始类型,那样在Java运行时根本就获取不到泛型信息。

擦除

Java编译器编译泛型的步骤:
1.检查泛型的类型 ,获得目标类型
2.擦除类型变量,并替换为限定类型(T为无限定的类型变量,用Object替换)
3.调用相关函数,并将结果强制转换为目标类型。

  1. ArrayList<String> arrayString=new ArrayList<String>();
  2. ArrayList<Integer> arrayInteger=new ArrayList<Integer>();
  3. System.out.println(arrayString.getClass()==arrayInteger.getClass());

上面代码输入结果为 true,可见通过运行时获取的类信息是完全一致的,泛型类型被擦除了!

如何擦除:
当擦除泛型类型后,留下的就只有原始类型了,例如上面的代码,原始类型就是ArrayList。擦除类型变量,并替换为限定类型(T为无限定的类型变量,用Object替换),如下所示

擦除之前:

  1. //泛型类型
  2. class Pair<T> {
  3. private T value;
  4. public T getValue() {
  5. return value;
  6. }
  7. public void setValue(T value) {
  8. this.value = value;
  9. }
  10. }

擦除之后:

  1. //原始类型
  2. class Pair {
  3. private Object value;
  4. public Object getValue() {
  5. return value;
  6. }
  7. public void setValue(Object value) {
  8. this.value = value;
  9. }
  10. }

因为在Pair中,T是一个无限定的类型变量,所以用Object替换。如果是Pair,擦除后,类型变量用Number类型替换。

与其他语言相比较

相比较Java,其模仿者 C# 在泛型方面无疑做的更好,其是真泛型。

C#泛型类在编译时,先生成中间代码IL,通用类型T只是一个占位符。在实例化类时,根据用户指定的数据类型代替T并由即时编译器(JIT)生成本地代码,这个本地代码中已经使用了实际的数据类型,等同于用实际类型写的类,所以不同的封闭类的本地代码是不一样的。其可以在运行时通过反射获得泛型信息,而且C#的泛型大大提高了代码的执行效率。

那么Java为什么不采用类似C#的实现方式呢,答案是:要向下兼容!