Java 泛型

处理泛型的方式:

通常情况下,一个编译器处理泛型有两种方式:

  1. C#中的泛型无论在源码中、还是运行期的 CLR 都是切实存在的,ListList就是两个不同类型,会产生两份目标代码,有它们自己的虚方法表和类型数据,这种实现称为类型膨胀
  2. 还有就是 Java 泛型,只在源码中存在,在编译后的字节码文件,就已经替换为原生类型,并且在相应地方插入了强制转型代码,Java 泛型实现方法称为类型擦除,是 Java 的语法糖,因此也是伪泛型。每个泛型类都只生成唯一的目标代码,该泛型类的所有实例都映射到这份代码上。

泛型基本使用过程中,注意泛型方法是可以定义在普通类中的。

class ArrayAlg {
    public static  T getMiddle(T... a) {
        return a[a.length / 2];
    }
}
// 调用时
String middle = ArrayAlg.getMiddle("]ohnM", "Q.", "Public");

验证类型擦除

Pair.java源代码如下:

public class Pair {

    public static void main(String[] args) {

        Pair pair = new Pair<>();
        pair.setFirst("one");
        pair.setSecond("two");
        System.out.println(pair.getFirst());
        System.out.println(pair.getSecond());

    }

    private T first;
    private T second;

    public Pair(T a, T b) {
        this.first = a;
        this.second = b;
    }

    public Pair() {
    }

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public T getSecond() {
        return second;
    }

    public void setSecond(T second) {
        this.second = second;
    }
}
 分别执行命令:
 javac Pair.java 
 javap -c Pair.class 
 得到反编译后内容(仅贴出部分):
 public class com.webtest.test.generic.Pair {
  public static void main(java.lang.String[]);
    Code:
       0: new           #1                  // class com/webtest/test/generic/Pair
       3: dup
       4: invokespecial #2                  // Method "":()V
       7: astore_1
       8: aload_1
       9: ldc           #3                  // String one
      11: invokevirtual #4                  // Method setFirst:(Ljava/lang/Object;)V
      14: aload_1
      15: ldc           #5                  // String two
      17: invokevirtual #6                  // Method setSecond:(Ljava/lang/Object;)V
      20: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      23: aload_1
      24: invokevirtual #8                  // Method getFirst:()Ljava/lang/Object;
      27: checkcast     #9                  // class java/lang/String
      30: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      33: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      36: aload_1
      37: invokevirtual #11                 // Method getSecond:()Ljava/lang/Object;
      40: checkcast     #9                  // class java/lang/String
      43: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      46: return

  public com.webtest.test.generic.Pair(T, T);
    Code:
       0: aload_0
       1: invokespecial #12                 // Method java/lang/Object."":()V
       4: aload_0
       5: aload_1
       6: putfield      #13                 // Field first:Ljava/lang/Object;
       9: aload_0
      10: aload_2
      11: putfield      #14                 // Field second:Ljava/lang/Object;
      14: return
}

可以从 Pair 构造函数中看出 first/second 成员编译后为 Object 类型;
另 main() 中 Pair 实例对象调用方法,也都是得到 Object 类型,其中 getFirst()、getSecond() 还牵扯了 检验类型转换,
由 27 和 40 数字序列看出 checkcast 指令。

通配符限定

关于 ? super A 和 ?extends A 的使用,存在容易混淆的点,搞不清楚如何正确使用。

先拿 ? super A 说明(因为我觉得从 super 角度更容易让人恍然大悟,至少我之前是如此),在 List 中,? super A 限定的是类型参数而已,不是直接限制的元素
过程是,先令类型参数 T 满足 ? super A,然后得到 List ,而 List 才是对元素做出限制的。
以上两句重要的话,读三遍!!!

举例:

class Q {
}
class W extends Q{
}
class E extends W{
}
class R extends E{
}

List listE = new ArrayList<>();
? super E 限定了类型参数 T 必须为 E 类或者 E 类的父类
假设 T 为 E 类,得到 List,因此集合内可以添加 E 类和 E 类的子类的元素;
假设 T 为 E 类的父类,同理集合内可以添加 E 类和 E 类的子类的元素;
因此可以有:
listE.add(new E()); // 允许
listE.add(new R()); // 允许
listE.add(new W()); // 错误,不能添加。
listE.add(new Q()); // 错误,不能添加。
错误原因是万一 ? super E 具体的类型参数 T 是 E 类呢,List 就肯定不能添加 E 类的父类对象;
同理万一 ? super E 具体的参数类型 T 是 W  类呢,List 就肯定不能添加 Q 类对象。

从以上例子可以看出,正确的理解是:? super E 是限定了参数类型 T,T 为 E 类或者 E 类的父类,因而可以添加 E 类或者 E 类子类对象到列表中。
错误的理解是:直观上的感觉 ? super E 是限定了添加到列表的元素,必须是 E 类的父类的对象。

再深入,带有超类型限定的通配符,可以为方法提供参数,但是不能使用返回值。你会发现以上的例子中 listE调用获取元素的方法,仅会得到 Object 对象,Object object = listE.get(0);。这是因为不能保证返回对象的类型,只能把它赋给一个 Object 对象。后面会引出 PECS 原则

接下来再拿 ? extends A 做说明,在 List 中,? extends A 限定的是类型参数而已,不是直接限制的元素。过程是,先令类型参数 T 满足 ? extends A,然后得到 List ,而 List 才是对元素做出限制的。

举例:

// Number 类是 Integer 和 Double 的父类
List numberArray = new ArrayList();  // Number 是 Number 类型的
List numberArray = new ArrayList(); // Integer 是 Number 的子类
List numberArray = new ArrayList();  // Double 是 Number 的子类
以上三个操作都是合法的,但是对于 List numberArray 对象:

我们不能添加 Number 到 numberArray 中, 因为 numberArray 有可能是List 类型
我们不能添加 Integer 到 numberArray 中, 因为 numberArray 有可能是 List 类型
我们不能添加 Double 到 numberArray 中, 因为 numberArray 有可能是 List 类型

我们无法确定 List 对象实际是个什么类型,毕竟都有可能,因此就不能确定添加的元素是否能够和 List 匹配。
但是读取元素的操作,我们是能确定返回类型的。
Number number = numberArray.get(0); 

List 类型如果是 List 类型,那么返回 Number 类型元素,
那么读取 Number 类型对象给 Number 类型引用是合法的;
List 类型如果是 List 类型,那么返回 Integer 类型元素,
那么读取 Integer 类型对象给 Number 类型引用是合法的;
List 类型如果是 List 类型,那么返回 Double 类型元素,
那么读取 Double 类型对象给 Number 类型引用是合法的;

PECS 原则

如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)

如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)

如果既要存又要取,那么就不要使用任何通配符。

需要记住有关 Java 泛型转换的事实:

  1. 虚拟机中没有泛型, 只有普通的类和方法。
  2. 所有的类型参数都用它们的限定类型替换。
  3. 桥方法被合成来保持多态。
  4. 为保持类型安全性, 必要时插人强制类型转换。

参考:
Java的类型擦除
Java 之泛型通配符 ? extends T 与 ? super T 解惑
深入理解 Java 虚拟机:JVM 高级特性与最佳实践/周志明著

你可能感兴趣的:(Java)