专栏首页疯狂软件李刚List与List<?>的区别何在

List与List<?>的区别何在

导读

泛型是Java最基础的语法之一,不过这种语法依然有值得一说的地方:如果仅停留在泛型的基本使用上,泛型当然很简单;但如果从字节码层次来看泛型,将会发现更多泛型的本质。

泛型是Java最基础的语法之一,不过这种语法依然有值得一说的地方:如果仅停留在泛型的基本使用上,泛型当然很简单;但如果从字节码层次来看泛型,将会发现更多泛型的本质。

本文并不打算介绍泛型的基本用法,这些内容应该属于普通的使用,如果连简单的在集合类中使用泛型都不熟悉,或泛型类、泛型方法这些基础内容不熟,那么能力不足就要多读书,比如再翻翻手上的《疯狂Java讲义》。

本文讲解的是两个容易混淆的东西:List类型和List<?>之间的区别和联系。

List和List<?>的相似之处

首先要说的是:如果仅从意义上来看,List和List<?>看上去具有一定的相似之处:List代表集合元素可以是任意类型的列表;List<?>似乎也代表集合元素可以任意类型的列表!

事实上呢?并不是如此! List<?>代表集合元素无法确定的列表。

不过它们有相似的地方,由于List完全没有指定泛型,因此程序可以将泛型为任意类型的List(如List<Integer>、List<String>...等)赋值给List类型的变量;类似的,程序也可将泛型为任意类型的List(如List<Integer>、List<String>...等)赋值给List<?>类型的变量。

例如如下程序:

import java.util.*;
public class GenericTest
{
  public static void main(String[] args)
  {
    List<Integer> intList = List.of(2, 3, 10);
    List<String> strList = List.of("java", "swift", "python");
    // 下面两行代码都是正确的
    List list1 = intList;
    List list2 = strList;
    // 下面两行代码也是正确的
    List<?> list3 = intList;
    List<?> list4 = strList;
  }
}

从上面代码可以看到,List<String>、List<Integer>类型的列表可以直接赋值给List、也可直接赋值给List<?>。

如果仅看上面程序,List和List<?>似乎差别不大?真的是这样吗?

原始类型擦除了泛型

首先需要说明一点:早期的Java是没有泛型的——Java 5才加入泛型,对于90后的小朋友来说,Java 5应该是一个古老的传说了。

正因为早期Java没有泛型,因此早期Java程序用List等集合类型时只能写成List,无法写成List<Integer>或List<String>!这样就造成了一个现状:虽然后来Java 5增加了泛型,但Java必须保留和早期程序的兼容,因此Java 5+必须兼容早期的写法:List不带泛型。

换句话来说,使用泛型类不带尖括号、具体类型的用法,其实是一种妥协:为了与早期程序的兼容。

也就是说:对于现在写的程序,谁要是使用泛型类时不填写具体类型,都应该打屁股哦。

注意

现在使用泛型类时,都应该为泛型指定具体的类型。

为了保持与早期程序兼容,Java允许在使用泛型类时不传入具体类型的搞法,被称为”原始类型(raw type)“。

原始类型会导致泛型擦除,这是一种非常危险的操作。例如如下程序:

import java.util.*;
public class GenericErase
{
  public static void main(String[] args)
  {
    List<Integer> intList = new ArrayList<>();
    intList.add(20);
    intList.add(3);
    intList.add(5);

    // 泛型擦除
    List list = intList;  // ①
    // list是List类型,因此可以添加String类型的元素
    list.add("疯狂Java");   // ②

  }
}

上面①号代码使用了原始类型,这样就导致了泛型擦除——擦除了所有的泛型信息,因此程序可以在②号代码处向list集合添加String类型的元素。

那么问题来了,②号代码处是否可以向list集合(其实是List<Integer>集合)添加String类型的元素呢?

如果你不运行这个程序,你能得到正确答案吗?

答案是:完全可以添加进去! ——这是因为原始类型导致泛型信息完全被擦除了。

因此你完全可以在②号代码后使用如下代码来遍历该list集合。

    // 使用Lambda表达式遍历list集合
    list.forEach(System.out::println);

但是,如果你试图使用如下代码来遍历intList集合就会导致错误。

    for (Integer i : intList)
    {
      System.out.println(i);
    }

上面代码编译时没有任何问题——道理很简单,因为intList的类型是List<Integer>,因此编译器会认为它的集合元素都是Integer,因此程序在for循环中声明它的集合元素为Integer类型——这合情合理。

但运行该程序就会导致如下运行时错误。

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer

这就是原始类型问题:原始类型导致了泛型擦除,因此编译器不会执行泛型检查,这样就会给程序引入潜在的问题。

幸运的是,Java编译器非常智能,只要你的程序中包含了泛型擦除导致的潜在的错误,编译器就会提示unchecked警告。

那么问题来了,List<?>是否有这个问题呢?

List<?>不能添加元素

很明显,List<?>是很规范的泛型用法,因此它不会导致泛型擦除,因此将List<Integer>、List<String>赋值给List<?>类型的变量完全不会导致上面的错误。

List<?>怎么处理的呢?Java的泛型规定:List<?>不允许添加任何类型的元素!

List<?>相当于上限是Object的通配符,因此List<?>完全相当于List<? extends Object>,这种指定通配符上限的泛型只能取出元素,不能添加元素。

注意

这种指定通配符上限的用法被称为泛型协变,关于泛型协变的深入介绍可参考《疯狂Java讲义》9.3节或参考《Effective Java》。

实际上,Google推荐的Android开发语言:Kotlin在处理泛型协变时更加简单粗暴,它不再搞什么上限、下限,而是直接用in、out来修饰泛型——out代表泛型协变、泛型协变只能出不能进;in代表泛型逆变,泛型逆变只能进不能出。相比之下,Kotlin在处理泛型型变、逆变时具有更好的可读性。

备注

如需了解Kotlin的泛型型变、逆变的内容,可参考《疯狂Kotlin讲义》。

对于如下程序:

import java.util.*;
public class GenericWildcard
{
  public static void main(String[] args)
  {
    List<Integer> intList = new ArrayList<>();
    intList.add(20);
    intList.add(3);
    intList.add(5);

    // 泛型通配符,此处的本质就是泛型协变
    List<?> list = intList;  // ①
    // list是List类型,因此可以添加String类型的元素
    list.add("疯狂Java");    // ②

  }
}

上面程序中①号代码将List<Integer>类型的变量赋值给List<?>变量,此时的本质就是泛型协变。

由于List<?>代表元素不确定类型的List集合,因此程序无法向 List<?>类型的集合中添加任何元素——因此Java编译器会禁止向list添加任何元素,故程序②号代码报错。

上面程序编译就会报错,这样程序就健壮多了。

List和List<?>的本质是一样的

需要说明的是,泛型类并不存在!

泛型只是一种编译时的检查,因此List和List<?>的本质是一样。

例如如下使用原始类型的程序:

import java.util.*;
public class RawTypeTest
{
  public static void main(String[] args)
  {
    List<Integer> inList = new ArrayList<>();
    List rList = inList;
    
  }
}

用javap分析上面程序的字节码,可看到如下输出:

public class RawTypeTest {
  public RawTypeTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: astore_2
      10: return
}

对于如下使用通配符的程序:

import java.util.*;
public class WildcardTest
{
  public static void main(String[] args)
{
    List<Integer> inList = new ArrayList<>();
    List<?> wList = inList;

  }
}

用javap分析上面程序的字节码,同样可看到如下输出:

public class WildcardTest {
  public WildcardTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: astore_2
      10: return
}

从上面字节码可以看到,泛型检查的主要工作是在编译阶段完成,编译之后生成的字节码并没有太大的差别。

本文结束

本文分享自微信公众号 - 疯狂软件李刚(fkbooks),作者:疯狂软件李刚

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java泛型的协变与逆变

    泛型是Java最基础的语法之一,众所周知:出于安全原因,泛型默认不能支持型变(否则会引入危险),因此Java提供了通配符上限和通配符下限来支持型变,其中通配符上...

    疯狂软件李刚
  • 关于Python函数装饰器最简单的说明

    本文是关于Python函数装饰器最简单的介绍,没有废话,没有套路,赤裸裸的一句话就掌握Python函数装饰器。

    疯狂软件李刚
  • 使用pygame开发合金弹头(5)

    Python的强大超出你的认知,Python的功能不止于可以做网络爬虫,数据分析,Python完全可以进行后端开发,AI,Python也可进行游戏开发,本文将会...

    疯狂软件李刚
  • Java工具集-集合(CollectionUtils)

    cwl_java
  • java每日一题20201006

    大家好,我是向同学,从今天继续每日一题,旨在为提高大家的基础知识。话说干了这么多年的开发,只知道会用,怎么用,用什么,隐约也知道了为什么用,但为啥JAVA总像一...

    用户7656790
  • JAVA 泛型

    命名类型参数 推荐的命名约定是使用大写的单个字母名称作为类型参数。这与 C++ 约定有所不同(参阅 附录 A:与 C++ 模板的比较),并反映了...

    用户1688446
  • Scala入门学习笔记四--List使用

    前言 本篇将介绍一个和Array很相似的集合List,更多内容请参考:Scala教程 本篇知识点概括 List的构造 List与Array的区别 Lis...

    用户1174963
  • Why to do,What to do,Where to do 与 Lambda表达式!

    最近我做一个“四象限”图表控件,其中有一个比较复杂的“坐标变换”问题,即是如何让一组数据放到有限的一个区间内,例如有一组数据 List[4,5,6,7,8],要...

    用户1177503
  • Android高德之旅(17)出行路线规划废话简介总结

    今天这篇来记录一下地图SDK中非常重要的一个功能:出行路线规划。我相信高德地图使用最多的也就是这个功能了,当然,我们今天的内容可能还做不到高德地图那么丰富的效果...

    大公爵
  • Java 集合深入理解:List 接口

    注意:上述使用了 ArrayList 的转换构造函数: public ArrayList(Collection

    张拭心 shixinzhang

扫码关注云+社区

领取腾讯云代金券