前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >List与List<?>的区别何在

List与List<?>的区别何在

作者头像
疯狂软件李刚
发布2020-06-24 17:01:49
1.6K0
发布2020-06-24 17:01:49
举报

导读

泛型是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
}

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

本文结束

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 疯狂软件李刚 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档