Skip to content

Java 泛型列表

🏷️ Java 泛型

泛型的本质是类型参数化,解决不确定具体对象类型的问题。

在泛型定义中,约定成俗的符号包括:

  • EElement :集合中的元素
  • Tthe Type of object :某个类
  • KKey :用于键值对元素
  • VValue :用于键值对元素
  • RReturn :方法的返回值

泛型只是一种编写代码时的语法检查。 在使用泛型元素时,会执行强制类型转换。可以通过查看编译生成的字节码来验证一下。

java
public static void main(String[] args) {
    List<Integer> intList = new ArrayList<>();
    intList.add(1);
    Integer val = intList.get(0);
    System.out.println(val);
}

使用 javac 命令生成字节码文件:

powershell
javac .\ClassName.java

使用 javap -c 命令反汇编字节码文件:

powershell
javap -c .\ClassName.class

生成的 main 方法对应的字节码:

java
  public static void main(java.lang.String[]);
    Code:
       0: new           #7                  // class java/util/ArrayList
       3: dup
       4: invokespecial #9                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: iconst_1
      10: invokestatic  #10                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      13: invokeinterface #16,  2           // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      18: pop
      19: aload_1
      20: iconst_0
      21: invokeinterface #22,  2           // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
      26: checkcast     #11                 // class java/lang/Integer
      29: astore_2
      30: getstatic     #26                 // Field java/lang/System.out:Ljava/io/PrintStream;
      33: aload_2
      34: invokevirtual #32                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      37: return

从泛型列表获取代码的操作是这两行:

java
      21: invokeinterface #22,  2           // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
      26: checkcast     #11                 // class java/lang/Integer

可以看到 List.get 方法返回的是 Object 类型,之后通过 checkcast 指令做了一次强制转换。这就是类型擦除,编译时泛型擦除了泛型的类型。

泛型就是在编译期增加了一道检查而已,目的是促使程序员在使用泛型时安全放置和使用数据。

ListList<Object>List<?> 的区别

  • List:完全没有类型限制和赋值限定。List 并不是泛型,它是原始类型,是为了兼容 JDK5 之前的老代码才保留的。 JDK5 之后应总是使用泛型(编码规约中一般都会有这条规则),以避免运行发生类型转换异常。

    List 类型的危险在于其没有类型检查,而为了向前兼容,编译器允许 List 类型的值赋值给泛型,这就导致了运行时可能会出现强制转换异常。

    java
    List list = new ArrayList();
    list.add(new Object());
    
    List<Integer> intList = new ArrayList<>();
    intList.addAll(list);
    // class java.lang.Object cannot be cast to class java.lang.Integer
    Integer intVal = intList.get(0);

    这段代码可以正常编译,直到运行时才会报错。

  • List<Object>:泛型,可以接受任何类型的元素,但在接受其他类型的列表引用赋值时会编译出错。

    java
    List<Integer> intList = new ArrayList<>();
    // 编译出错:java: 不兼容的类型: java.util.List<java.lang.Integer>无法转换为java.util.List<java.lang.Object>
    List<Object> objList = intList;

    数组可以以类似的方式赋值,因为它是协变的,而列表不是。

  • List<?>:泛型,通配符列表(也叫无界通配符)。可以接受任何类型的列表引用赋值,不能添加任何元素了,但允许删除( remove )和清除( clear )元素)。

    除此之外,还有两种使用通配符的泛型列表:

    • List<? extends E>:上界通配符 表示 E 及其子类型的列表
    • List<? super E>:下界通配符 表示 E 及其父类型的列表
      • 需要注意的是,并不是说列表内只允许放置 E 及其父类型的元素,也可以存放 E 子类型的元素

List<? extends E>List<? super E> 的区别

这两个类型比较难区分,简单来说:

  • List<? extends E>Get First 获取优先 适用于消费集合元素为主的场景
  • List<? super E>Put First 放置优先 适用于生产集合元素为主的场景

下面通过一些示例代码来加深理解。

创建了四个类:

  • 三个依次继承的类(爷爷类 GrandpaClass、 父亲类 FatherClass、 儿童类 ChildClass
  • 一个对比用的其他类 AnotherClass
java
private static class GrandpaClass {}

private static class FatherClass extends GrandpaClass {}

private static class ChildClass extends FatherClass {}

private static class AnotherClass {}

分别定义四种类型对应的列表和实例变量:

java
ArrayList<GrandpaClass> grandpaList = new ArrayList<>();
ArrayList<FatherClass> fatherList = new ArrayList<>();
ArrayList<ChildClass> childList = new ArrayList<>();
ArrayList<AnotherClass> anotherList = new ArrayList<>();

GrandpaClass grandpa = new GrandpaClass();
FatherClass father = new FatherClass();
ChildClass child = new ChildClass();
AnotherClass another = new AnotherClass();

grandpaList.add(grandpa);
fatherList.add(father);
childList.add(child);
anotherList.add(another);

分别定义一个 EFatherClasssuperextends 列表:

java
List<? extends FatherClass> extendsList = new ArrayList<>();
List<? super FatherClass> superList = new ArrayList<>();

列表引用赋值

将四种类型的列表的引用直接赋值给 superextends 列表:

java
// 编译出错:java: 不兼容的类型: java.util.ArrayList<GrandPaClass>无法转换为java.util.List<? extends FatherClass>
extendsList = grandpaList;
// 允许 E 类型列表的引用赋值
extendsList = fatherList;
// 允许 E 子类型列表的引用赋值
extendsList = childList;
// 编译出错:java: 不兼容的类型: java.util.ArrayList<AnotherClass>无法转换为java.util.List<? extends FatherClass>
extendsList = anotherList;

// 允许 E 父类型列表的引用赋值
superList = grandpaList;
// 允许 E 类型列表的引用赋值
superList = fatherList;
// 编译出错:java: 不兼容的类型: java.util.ArrayList<ChildClass>无法转换为java.util.List<? super FatherClass>
superList = childList;
// 编译出错:java: 不兼容的类型: java.util.ArrayList<AnotherClass>无法转换为java.util.List<? super FatherClass>
superList = anotherList;

根据编译结果可以得出如下结论:

  • List<? extends E> :允许 E 及其子类型列表的引用赋值
  • List<? super E> :允许 E 及其父类型列表的引用赋值
    相比于 extends 个人感觉 super 更难理解一些。
    到这里为止可以看到 super 列表中可以保存 E 及其父类型的元素。

添加元素

调用 add()addAll() 添加元素:

java
// 允许添加 null 值
extendsList.add(null);

// 不允许添加 null 以外的元素到 `<? extends E>` 列表,以下均会编译出错
extendsList.add(grandpa);
extendsList.addAll(grandpaList);
extendsList.add(father);
extendsList.addAll(fatherList);
extendsList.add(child);
extendsList.addAll(childList);
extendsList.add(another);
extendsList.addAll(anotherList);

// 允许添加 null 值
superList.add(null);

// 编译出错:java: 不兼容的类型: GrandPaClass无法转换为capture#1, 共 ? super FatherClass
superList.add(grandpa);
// 编译出错:java: 不兼容的类型: java.util.ArrayList<GrandpaClass>无法转换为java.util.Collection<? extends capture#2, 共 ? super FatherClass>
superList.addAll(grandpaList);

// 允许添加 E 类型元素到列表
superList.add(father);
superList.addAll(fatherList);

// 允许添加 E 子类型元素到列表
superList.add(child);
superList.addAll(childList);

// 编译出错:java: 不兼容的类型: AnotherClass无法转换为capture#3, 共 ? super FatherClass
superList.add(another);
// 编译出错:java: 不兼容的类型: java.util.ArrayList<AnotherClass>无法转换为java.util.Collection<? extends capture#4, 共 ? super FatherClass>
superList.addAll(anotherList);

两者都允许添加 null 值。

  • List<? extends E> :不允许添加 null 以外的元素
    这应该是 extends 理解为主要适用于消费场景的原因。消费时一般只删除元素,不添加元素。
  • List<? super E> :允许添加 E 及其子类型元素
    通过 add()addAll() super 列表也可以保存 E 子类型的元素。
    承接上面的引用赋值部分,此时 super 列表中可以保存 E 类型、E 父类型及 E 子类型的元素,即 可以保存 E 继承树上的所有类型的元素

获取元素

java
for (FatherClass item : extendsList) {
    // 除 null 外, extendsList 只可能包含 E 类型及 E 子类型的元素
}

for (Object item : superList) {
    // 由于并不知道列表里元素的类型,元素类型强制为 Object 类型
}
  • List<? extends E> :取出的元素强制转换为 E 类型
    通过前面的示例可知,除了 null 值外,extends 列表只能通过引用赋值设置元素,也即只可能包含 E 及其子类型的元素。
    所以可以将元素强制转化为 E 类型
  • List<? super E> :取出的元素强制转换为 Object 类型
    通过前面的示例可知,super 列表中可能存在 E 类型继承树上的所有类型的元素,所以取出来时无法确定元素具体的类型,只能强制转换为 Object 类型。

移除和清理元素

调用 remove()removeAll()clear() 移除和清理元素:

java
// 以下均不会编译出错

extendsList.remove(grandpa);
extendsList.remove(father);
extendsList.remove(child);
extendsList.remove(another);

extendsList.removeAll(grandpaList);
extendsList.removeAll(fatherList);
extendsList.removeAll(childList);
extendsList.removeAll(anotherList);

superList.remove(grandpa);
superList.remove(father);
superList.remove(child);
superList.remove(another);

superList.removeAll(grandpaList);
superList.removeAll(fatherList);
superList.removeAll(childList);
superList.removeAll(anotherList);

extendsList.clear();
superList.clear();

两者均可以正常执行 remove()removeAll()clear() 操作。

因为 remove()removeAll() 的形参分别为 ObjectCollection<?> 类型,所以即使是和 E 无任何关系的 AnotherClass 类型的实例也不会报错。

参数传递

定义消费( consume )和生产( produce )方法:

java
private static void consume(List<? extends FatherClass> fatherList) {
    for (FatherClass item : fatherList) {
        // do something
    }
    fatherList.clear();
}

private static void produce(List<? super FatherClass> fatherList) {
    fatherList.clear();
    fatherList.add(new FatherClass());
    fatherList.add(new ChildClass());
}

分别传递对应的泛型列表的实参到这两个方法:

java
// 'consume(java.util.List<? extends FatherClass>)' 无法应用于 '(java.util.ArrayList<GrandpaClass>)'
consume(grandpaList);
consume(fatherList);
consume(childList);
// 'consume(java.util.List<? extends FatherClass>)' 无法应用于 '(java.util.ArrayList<AnotherClass>)'
consume(anotherList);

produce(grandpaList);
produce(fatherList);
// 'produce(java.util.List<? super FatherClass>)' 无法应用于 '(java.util.ArrayList<ChildClass>)'
produce(childList);
// 'produce(java.util.List<? super FatherClass>)' 无法应用于 '(java.util.ArrayList<AnotherClass>)'
produce(anotherList);

从编译结果看方法参数的传递同引用赋值是类似的。

  • List<? extends E> :允许传递 E 及其子类型列表的实参
  • List<? super E> :允许传递 E 及其父类型列表的实参

总结

从上面示例中的消费和生产方法的使用,个人猜测下为什么 superextends 列表要如此设计。

  • 消费方法( consume ):形参类型设置为 List<? extends E> 类型
    • 可以使其接受 E 及其子类型列表的实参
    • 在消费处理中确保其元素为 E 或其子类型,使元素总是可以当作 E 类型来处理
    • 不允许添加非 null 元素可以避免在消费时添加元素
    • 允许移除和清空元素可以使列表移除已消费的元素
  • 生产方法( produce ):形参类型设置为 List<? super E> 类型
    • 可以使其接受 E 及其父类型列表的实参
    • 在方法体内向列表中添加元素时,确保新加入的元素是 E 或其子类型
    • 根据上面的两点,可以确保在之后处理实参中的元素时,总是可以将其当作 E 类型来处理
    • 从形参中获取元素时其类型为 Object 类型,隐藏了元素的类型,以此来限制在生产方法中对元素的使用
    • 如果类比消费方法,生产列表元素的过程中应该不允许移除和清空元素才对,但其实是可以的

List<?>List<? extends Object> 的区别

关于这两个类型,我以为是一样的,但是根据这篇博客的内容,发现两者还是稍有不同的。

可重构类型是指那些在编译时未被擦除的类型。换句话说,一个不可重构类型,运行时将比编译时表达的信息更少,因为其中一些信息会被擦除。

一般来说,参数化类型是不可重新定义的。比如 List<String>Map<Integer,String> 就不可重新定义。编译器会擦除它们的类型,并将它们分别视为列表和映射。

这个准则的唯一例外是无界通配符类型。也就是说, List<?> 以及 Map<?, ?> 是可重写的。

另外,List<? extends Object> 不可重写。虽然微妙,但这是一个显著的区别。

不可重构的类型在某些情况下不能使用,例如在 instanceof 运算符或作为数组的元素。

不过这篇博客中的示例代码我这边实际运行的效果和文章中并不一样(应该是 JDK 版本不一致导致的,这里试了几个版本,区别见示例代码中的备注)。

java
public static void main(String[] args) {
    List<?> oneList = new ArrayList<>();
    System.out.println(oneList instanceof List<?>); // print true
    // 反编译的 .class 文件:System.out.println(oneList instanceof List);

    List<? extends Object> anotherList = new ArrayList<>();
    // OpenJDK 1.8 编译会出错:instanceof 的泛型类型不合法
    // Corretto 17.0.3 编译不会出错(原博客中 anotherList 的类型为 List ,这里分别使用了 List 、List<?> 和 List<? extends Object> 都可以正确编译并总是打印 true)
    // OpenJDK 18.0.2 编译不会出错
    System.out.println(anotherList instanceof List<? extends Object>); // print true
    // 反编译的 .class 文件:System.out.println(anotherList instanceof List);

    List<?>[] arrayOfList = new List<?>[1];
    // 反编译的 .class 文件:List<?>[] arrayOfList = new List[1];

    List<? extends Object>[] arrayOfAnotherList;
    arrayOfAnotherList = new List<?>[1];
    // 反编译的 .class 文件:List<? extends Object>[] arrayOfAnotherList = new List[1];
    System.out.println(arrayOfAnotherList.length); // print 1

    // OpenJDK 1.8 编译会出错:创建泛型数组
    // Corretto 17.0.3 编译会出错:创建泛型数组
    // OpenJDK 18.0.2 编译不会出错:虽然在 IDEA 的问题窗口中会有个 *创建泛型数组* 的错误,但是可以正确编译并运行
    arrayOfAnotherList = new List<? extends Object>[2];
    // 反编译的 .class 文件:arrayOfAnotherList = new List[2];
    System.out.println(arrayOfAnotherList.length); // print 2
}

另外 IntelliJ IDEA 中在使用 List<? extends Object> 类型时会提示:移除冗余的 'extends Object' ,并且在问题窗口可以看到一个警告:通配符类型实参 '?' 显式扩展 'java.lang.Object'

参考


以上仅为个人理解,若有谬误,欢迎评论指摘。