xiwenAndlejian / my-blog

Java基础学习练习题
1 stars 0 forks source link

Java 泛型学习笔记 #14

Open xiwenAndlejian opened 6 years ago

xiwenAndlejian commented 6 years ago

Java 泛型学习笔记

内容均出自:Java泛型教程

泛型的关系

public void boxTest(Box<Number> n) { ... }

上述方法接受一个类型为Box<Number>的参数,但是并不能传递Box<Integer>Box<Double> 虽然我们已知IntegerNumber子类,但是Box<Integer>不是Box<Number>的子类型。

注意:给定两个具体类型 A 和 B(如:Number 和 Integer),MyClass 与 MyClass 无关,无论 A 和 B是否相关。MyClass 和 MyClass 的公共父类是 Object。 如果需要创建类似子类型关系的信息,需要使用通配符和子类型

通配符

通配符范围 通配符格式 解释
有上限的通配符 ? extends Class 类型需要是Class类型或其子类(<=)
无界通配符 ? 不限制类型
有下界的通配符 ? super Class 类型需要是Class类型或其父类(>=)

注意:

  1. 您可以指定通配符的上限,也可以指定下限,但不能同时指定两者
  2. 多边界写法 <? extentds A & B & C>
  3. 使用多边界时,如果其中一个边界是类,则必须首先指定它。(详见下文)
class A { ... }
interface B { ... }
interface C { ... }
// 正确写法
class D <T extands A & B & C> { ... }
// 错误写法,无法通过编译
class D <T extands B & A & C> { ... }

子类型

一个类或接口的类型参数与另一个类或参数的类型参数之间的关系由 extends 和 implements 子句确定。

ArrayList<E> 实现(extends) List<E>。List<E> (implements) Collection<E>。
因此:
ArrayList<String> 是 List<String> 的子类型,且 List<String> 是 Collection<String> 的子类型。
只要不改变类型参数,就会在类型之间保留子类关系

我们如下定义一个接口:PayloadList


interface PayloadList<E,P> extends List<E> {
  void setPayload(int index, P val);
  ...
}

下面这些 PayloadList 都是 List<String>的子类型

  • PayloadList<String, String>
  • PayloadList<String, Integer>
  • PayloadList<String, Exception>

泛型擦除

泛型被引入到 Java 语言中,以便在编译时提供更严格的类型检查并支持泛型编程。 为了实现泛型,Java 编译器类型擦除应用于:

  • 将泛型类型中的所有类型参数替换为其边界,如果类型参数无界,则替换为 Object。因此生成的字节码仅包含普通的类,接口和方法。
  • 如果有必要,插入类型转换以保持类型安全
  • 生成桥接方法以保留扩展泛型类型中的多态性 类型擦除确保不为参数化类型创建新类;因此,泛型不会产生运行时开销。

擦除通用类型

Java 编译器将擦除所有类型参数,并在类型参数有界时将其替换为第一个绑定,如果无界,则替换为 Object。

无界类型参数

源码:


public class Node<T> {
    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() {
        return data;
    }
    // ...
}

由于类型参数T无界,因此编译器使用Object替换:

类型擦除后:


public class Node {
    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() {
        return data;
    }
    // ...
}
有界类型参数

源码:


public class Node<T extentds Comparable<T>> {
    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() {
        return data;
    }
    // ...
}

类型参数有界时,编译器使用第一个边界类:Comparable替换类型参数:

类型擦除后:


public class Node {
    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() {
        return data;
    }
    // ...
}

擦除通用方法

无界

public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

T是无界的,因此使用Object替换它:

类型擦除后:


public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}
有界

public static <T extends Shape> void draw(T shape) { /*...*/ } T是有界的,因此采用第一个边界类型:Shape替换它。

类型擦除后: public static void draw(Shape shape) { /*...*/ }

类型擦除和桥方法

在类型擦除时,编译器有时会创建一个被称为桥接方法的合成方法,作为类型擦除的一部分。

给予以下两个类:


public class Node<T> {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

考虑以下代码:


MyNode mn = new MyNode(5);
Node n = mn;            // ⚠️编译器抛出一个未检查警告
n.setData("Hello");     
Integer x = mn.data;    // ❗️运行时抛出类型转换异常(ClassCastException)

类型擦除后的代码:


MyNode mn = new MyNode(5);
Node n = (MyNode) mn;    
n.setData("Hello");     
Integer x = (String) mn.data;   
教程解释(与实际情况有出入)

代码解释:

  • n.setData("Hello");:调用 MyNode.setData(Object data)。(从 Node 继承而得)
  • setData(Object data)中:n 引用的对象的 data 字段被分配给 "Hello"。
  • 可以访问 mn 引用的同一对象的 data 字段,并期望它是整数(因为 mn 是 MyNode,它是 Node<Integer>)。
  • 尝试将 String 分配给 Integer 会导致 Java 编译器在赋值时类型转换中抛出异常(ClassCastException)。
实际情况

JDK Version:1.8.0_121

关于代码:


MyNode mn = new MyNode(5);
Node   n  = mn;
n.setData("Hello");// ⚠️编译器抛出一个未检查警告,❗️运行时抛出类型转换异常 ClassCastException
Integer x = mn.data;

分析: 我们先通过命令javap -c Class.class查看编译后的 .class 文件:

// javap -c Node.class
public class com.dekuofa.Node<T> {
  public T data;

  public com.dekuofa.Node(T);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #2                  // Field data:Ljava/lang/Object;
       9: return

  public void setData(T);
    Code:
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String Node.setData
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: aload_0
       9: aload_1
      10: putfield      #2                  // Field data:Ljava/lang/Object;
      13: return
}
// javap -c MyNode.class
public class com.dekuofa.MyNode extends com.dekuofa.Node<java.lang.Integer> {
  public com.dekuofa.MyNode(java.lang.Integer);
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #1                  // Method com/dekuofa/Node."<init>":(Ljava/lang/Object;)V
       5: return

  public void setData(java.lang.Integer);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String MyNode.setData
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: aload_0
       9: aload_1
      10: invokespecial #5                  // Method com/dekuofa/Node.setData:(Ljava/lang/Object;)V
      13: return

  public void setData(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #6                  // class java/lang/Integer
       5: invokevirtual #7                  // Method setData:(Ljava/lang/Integer;)V
       8: return
}

// 调用代码
    Code:
       0: new           #2                  // class com/dekuofa/MyNode
       3: dup
       4: iconst_5
       5: invokestatic  #3                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       8: invokespecial #4                  // Method com/dekuofa/MyNode."<init>":(Ljava/lang/Integer;)V
      11: astore_1
      12: aload_1
      13: astore_2
      14: aload_2
      15: ldc           #5                  // String Hello
      17: invokevirtual #6                  // Method com/dekuofa/Node.setData:(Ljava/lang/Object;)V
      20: aload_1
      21: getfield      #7                  // Field com/dekuofa/MyNode.data:Ljava/lang/Object;
      24: checkcast     #8                  // class java/lang/Integer
      27: astore_3
      28: return

通过注解我们可以看出,反编译后的调用代码与之前代码行数大致对应关系:

  • MyNode mn = new MyNode(5);// 对应 0~8
  • n.setData("Hello");// 对应 15~21
  • Integer x = mn.data;// 对应 24~27

我们可以看出,实际 MyNode 中确实生成了桥方法(setData(Object)),但是生成的代码中: 2: checkcast #6 // class java/lang/Integer 校验了 data 类型是否是 Integer,因此才在n.setData("Hello")执行时抛出了异常。

不可再生类型

可再生类型:其类型信息在运行时完全可用,即可以获取到实际类型。其中包括基元、非泛型类型、原始类型和未绑定通配符的调用。 不可再生类型:编译时通过类型擦除,删除了类型信息的类型,即未定义为无界通配符的泛型类型的调用。不可再生类型在运行时没有提供所有信息。示例为:List<String>List<Number>;JVM 无法在运行时区分这些类型,因此无法在某些情况中使用。参考下文的泛型限制。

可再生类型示例

  • 基元:byte、bool、char、short、int、long、float、double
  • 非泛型类型:Object、Integer、String
  • 原始类型:new Array(),注意:此处省略了实际的类型参数,因此创建原始类型。(绕过了泛型检查,可能在运行时抛出异常,应尽量避免使用原始类型)
  • 未绑定通配符:定义class Myclass<OtherClass> { /*...*/ },调用MyClass obj = new MyClass<OtherClass>,obj 即为原始类型。

堆污染

当参数化类型的变量引用不是该参数类型化的对象时,会发生堆污染。例如,在混合原始类型和参数化类型时,或者在执行未经检查的强制转换时,会发生堆污染。

示例

ArrayBuilder:


public class ArrayBuilder {

  public static <T> void addToList (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  public static void faultyMethod(List<String>... l) {
    Object[] objectArray = l;     // Valid
    objectArray[0] = Arrays.asList(42);
    String s = l[0].get(0);       // ClassCastException thrown here
  }

}

HeapPollutionExample:


public class HeapPollutionExample {

  public static void main(String[] args) {

    List<String> stringListA = new ArrayList<String>();
    List<String> stringListB = new ArrayList<String>();

    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
    ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");

    List<List<String>> listOfStringLists = new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists,stringListA, stringListB);

    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  }
}

ArrayBuilder.addToList 方法定义产生以下警告: warning: [varargs] Possible heap pollution from parameterized vararg type T 当编译器遇到 varargs 方法时,它会将 varargs 形式参数转为数组。但是, Java 不允许创建参数化类型的数组。 在该方法中,编译器将 T ... elements 转换为 T[] elements,经过类型擦除,编译器将参数转化为Object[] elements。因此可能存在堆污染。

ArrayBuilder.faultyMethod 方法中: Object[] objectArray = l; 上述语句可能会引入堆污染。原本类型参数 List<String>... l,经过类型擦除后得到 List[] l,是 Object[] 的子类型。 因此语句objectArray[0] = Arrays.asList(42);不会有警告或错误。 而在语句String s = l[0].get(0);中,将原本为 Integer 类型的对象转换为 String 时失败,抛出 ClassCastException 异常。

泛型的限制

  • 无法使用基元类型参数实例化通用类型
  • 无法创建类型参数的实例
  • 无法声明类型为类型参数的静态字段
  • 无法使用具有参数化类型的强制转换或 instanceof
  • 无法创建参数化类型的数组
  • 无法创建、捕获或抛出参数化类型的对象
  • 无法重载每个重载形式参数类型擦除到相同原始类型的方法。

无法使用基元类型参数实例化通用类型

无法创建如:List<int> list = new ArrayList<>(); 只能使用基本类型的装箱类型,例如:List<Integer> list = new ArrayList<>();

注:在 list.add(1); 中,编译器将 1 自动装箱 => list.add(Integer.valueOf(1));

无法创建类型参数的实例


// 错误方法
public static<E> void append(List<E> list) {
    E elem = new E();// 编译报错
    list.add(elem);
}

// 正确使用方法
public static<E> void append(List<E> list, Class<E> clazz) throws Exception {
    E elem = clazz.newInstance();
    list.add(elem);
}

// 调用

List<String> ls = new ArrayList<>();
append(ls, String.class);

无法声明类型为类型参数的静态字段

参考如果存在以下声明:


public class MobileDevice<T> {
    public static T os;
    //...
}

那么当允许类型参数的静态字段时,以下代码将混淆:


MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager>      pager = new MobileDevice<>();
MobileDevice<TabletPC>   pc    = new MobileDevice<>();

由于字段 os 是共享在 MobileDevice 类的对象之间的,所以 os 的实际类型到底是什么?

无法使用具有参数化类型的强制转换或 instanceof

instanceof

由于泛型擦除的原因,所以无法验证运行时使用泛型类型的参数化类型:


public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {// 编译错误
        // ...
    }
}

运行时不跟踪类型参数,因此无法区分ArrayList<Integer>ArrayList<String>。最多可以使用无界匹配符来验证 list 是否是 ArrayList 类型。


public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<?>) {// instanceof 需要一个可再生的类型
        // ...
    }
}
强制转换

通常,除非通过无界通配符对其进行参数化,否则无法强制转换为参数化类型。eg:


List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li;// 编译时错误

但是某些情况下,编译器知道类型参数始终有效并允许强制转换。eg:


List<String> l1= ...;
ArrayList<String> l2 = (ArrayList<String>) l1;// 通过编译

无法创建参数化类型的数组

如下代码无法编译:


List<Integer>[] arrayOfList = new List<Integer>[2];// 编译时错误

参考一下代码:


Object[] strings = new String[2];
strings[0] = "hi";// ok
strings[1] = 100;// throw ArrayStoreException

Object[] stringLists = new List<String>[];  // 编译错误,但是我们假装允许此处执行
stringLists[0] = new ArrayList<String>();   // 执行通过
stringLists[1] = new ArrayList<Integer>();  // 同样的 ArrayStoreException 异常应该被抛出,但是运行时无法检测到它

因为泛型被擦除,我们无法区分ArrayList<String>ArrayList<Integer>,因此,允许参数化列表数组时,上述代码无法抛出我们需要的异常。

无法创建、捕获或抛出参数化类型的对象

泛型类不能直接或者间接扩展 Throwable 类。eg:


// 间接扩展
class MathException<T> extends Exception { /* ... */ }    // 编译错误

// 直接扩展
class QueueFullException<T> extends Throwable { /* ... */ // 编译错误

方法无法捕获类型参数的实例:


public static <T extends Exception, J> void execute(List<J> jobs) {
    try {
        for(J job: jobs) {
            //...
        }
    } catch(T e) {//编译时错误
        //...
    }
}

但是可以在 throws 子句中使用类型参数:


class Parser<T extends Exception> {
    public void parse(File file) throws T {//编译通过
        //...
    }
}

无法重载每个重载形式参数类型擦除到相同原始类型的方法

一个类不能有两个在类型擦除后具有相同签名的重载方法。


public class Example {
    public void print(Set<String> strSet) {}
    public void print(Set<Integer> strSet) {}
}