当先锋百科网

首页 1 2 3 4 5 6 7

Hello 大家好!上一期我们详细总结了面试中最常用的数据结构,Array 的知识点和常见算法考点,如果你还没有看过,赶快点击链接学习

Techbow大弓科技:面试常用数据结构(一):数组​zhuanlan.zhihu.comeea3a1ef10440d39180f209c45693634.png

今天我们来层层深入探讨一下使用过程中 Array 相关的一个充满坑的 API,Techbow避坑指南带你少写 bug。

如何将 Array 转换为 ArrayList

上次我们讲到了由于 Array 在创建后长度是固定的,无法进行修改,对于『增』和『删』操作只能通过创建一个新的 Array 来进行,因此我们一般对于这种情况会转换为 ArrayList 来进行操作。那么问题来了,怎么将 Array 转变成一个 ArrayList 呢?

比如说我们给定一个数组:

我们如何将该数组转换为我们想要的可自由操作的 『可变长』ArrayList 呢?

撸起袖子就是干!

最简单的方式,那我们就创建一个 ArrayList,然后遍历数组将每一个元素 add 进入 ArrayList 不就行了?

如果我们不想每次写 for 循环呢?因为这样好像显得非常『不优雅』不够『高级』。

好像有个 API 叫 asList()

这时候有同学可能会说,我听过一个 API 叫:

Arrays.asList(),我们可以将 Array 来转换成 List,更加简洁方便。

Arrays 是 Java 中内置的一个 Utility class,提供了 API 可以对 Array 进行一系列的操作,如 sort,binary search,copy,fill 等等。asList() 就是其中的一个 API,传入参数是一个 Array,返回一个该给定数组对应的 List 。

听起来就是我们想要的『高级』,那我们就来瞅一瞅这个方法究竟是否那么 elegant。

asList() 真的是你想的 as list 吗

我们来看下面一段代码:

同学们先停下来想一想,这段代码的结果会是什么呢?

这段代码的返回结果是 4,因为我们创建了一个 String 的 ArrayList,里面包含了 4 个 String 元素,List 的长度是 4。那么再看下一段代码:

这段代码的结果会是什么呢?是 5 吗?

这段代码会报错,无法编译:java: incompatible types: inference variable T has incompatible bounds equality constraints: java.lang.Integer lower bounds: int[]

看起来好像是我们声明的问题,左边声明是一个 Integer 的 List,但是右边是 int,因此 Java 的类型推断无法进行,那么删除左边的 type 声明不就好了吗。我们将代码修改成下面的形式:

这段代码的结果会是什么呢?是 5 吗?还不是,你会发现输出竟然是 1。而且我们如果想从 List 中得到数字 1,我们取 index 为 0 位置的元素时会发现,结果也并不是 1。

为什么呢?因为我们其实创建了一个 int[] 的 List,也就是创建了一个存储数组的 List,即二维结构 List 套内部数组。

因此我们打印 list.get(0) 时,其实得到是一个 int[] 的 object,因此打印出来的是其对应地址计算得到的 hashCode。

那么问题来了,为什么 asList() 不能如我们所愿呢?

第一坑:传入 pritmitive type 数组

通过查看 Java doc,我们可以找到 asList() API 的 function signature:

可以看到传入的参数类型是 T,在 Java 泛型中 T 表示 Type,即数组元素的 Class。我们都知道在 Java 的『面向对象』的特点,任何东西在 Java 中其实就是一个 Object 对象,每一个对象有自己的 Class 类。因此这里传入的 T 就是 Array 中存储的 Object 的 Class,在我们声明 List 时,用于 type inference 类型推导的 Diamond operator <> 可以获得我们需要创建的 List 的存储元素类型再创建对象。

然而,int 是一个例外。Java 中的 primitive type 原生数据类型是没有 Class 这个属性的,如 int,short,long,char 等,因此我们直接来创建 List 的时候,asList() 需要传入参数为 Object 或者 Object Array,这里 Primitive Type 不是 Object, 但是 Primitive Type 的 Array 是一个 Object,因此 asList() 所传入的参数就不是数组中的元素,而是数组对象本身。因此就创建了一个长度为 1 的存 int[] 对象的 List。我们前面的第一个例子是一个 String 数组,String 是一个 object,因此没有任何逻辑错误。

那么,我们该怎么创建一个存数字的 List 呢?

方法 1:使用 Primitive type 的 wrapper

我们无法创建需要的 List 的原因是我们传入的参数 int 不是 Object,那我们可以去使用 Primitive type 的 wrapper class 包装类呀。

在 Java 中,对于每一个 primitive type 都有一个对应的 wrapper class,这些 wrapper 都是一个 Object,也因此都具有 Class 属性,如:int → Integer

char → Character

long → Long

short → Short

......

从 primitive type 到 wrapper class 的过程称为『boxing』,从 wrapper class 到 primitive type 的过程称为『unboxing』。Java 提供了这样一个自动开箱装箱的方式来实现 primitive type 和 object 之间的转换,因此我们可以将 int[] 改为 Integer[] ,那么自然而然可以成功创建了:

方法 2:通过边创建 Array 来边创建 List

有没有别的办法呢?来看这一段代码,结果会是什么呢?

这个的结果仍然是 5,也就是说我们成功创建了一个装有所有整数的 List。为什么呢?前面不是说 primitive type 不是 object 吗?

其实这里是 Java 里的一个『autoboxing』的机制,为了理解,我们先看下面一段我们常用的代码:

没有任何违和感对不对,但是我们深入思考一下,这不对啊,我们声明了 list 是一个装 Integer 的 List,后面我们却每次 add 的是 int,不是说它不是 object 嘛,怎么又可以装到 list 中呢?

Java 中对于这种情况,不会报错编译错误,编译器会自动识别,将 1 创建为一个 Integer object,也就是一个自动 boxing 为 wrapper class 的过程。在实际运行时,上面的代码就等效于:

那这样问题就来了,这样不就很混乱,Java 到底有多智能可以自动处理这些 boxing 问题?实际上,Java 只会在两种情况下对 primitive type 进行 auto-boxing 操作:primitive type 被作为参数传入到一个需要对应 wrapper class object 的 method 中,比如上面的 list.add()

赋值给一个声明为 wrapper class 的 object,比如 Integer x = 1

因此我们就理解了这种第二种方式为什么可以成功了:

由于左边声明为一个存 Integer object 的 list,因此右边 asList() 传入参数 int 的时候就将其认为是 Integer,所以进行了 autoboxing 操作,可以等效为向 asList() 传入了一个 Integer[] 的参数得到我们的结果。

第二坑:asList() 后修改元素

我们将 Array 转变为 ArrayList 的原因就是为了可以高效操作修改元素,那么我们来看看效果如何?

这段代码的运行结果是什么呢?是 6 吗,并不是。这段代码会报错:

java.lang.UnsupportedOperationException

这就很奇怪了啊,我们就是为了将 Array 转变成一个 ArrayList 便于修改,现在却不能修改?

Java 中的 Arrays.asList() API

我们来追根溯源看一下这个 API,官方文档对 Arrays 类有一个说明:This class also contains a static factory that allows arrays to be viewed as lists.

也就是对于 asList(),我们的传入参数是一个 Array,返回的实际上是一个给定数组的 List View。什么叫 View 呢?我们可以看一下官方文档对 asList() 的说明:Returns a fixed-size list backed by the specified array. (Changes to the returned list "write through" to the array.) This method acts as bridge between array-based and collection-based APIs.

也就是说我们返回的是给定数组的一个『List 形式的 View』,这个 List 的内部还是 backed by 我们所传入的数组,只此一份,修改 List 会修改 Array,反之亦然。同时我们看到了,这个 list 是一个 fixed-size 的 list,因此无法进行修改。

看起来好像我们找到答案了,但是我们再想一想,在上一篇文章中我们讲了 ArrayList 就是一个内部实现了自动扩容方法的 List,怎么就变成 fixed-size 了呢?我们辛辛苦苦为了『高级感』,怎么最后白忙活了。

为了找到答案,我们再深入一层,去看 JDK 源码中 asList() 方法的实现(下图只精简列出来了讨论相关的代码)。会发现其实现方法是通过 Abstract Factory Pattern 抽象工厂设计模式进行构造,ArrayList 的 constructor 构造函数内部 maintain 了一个 final 修饰的 Array reference a。

终于我们找到了 root cause,List View 的谜底揭晓了,这个 backed by 确实是真正的 backed by。我们得到的 ArrayList 实际上内部实现就是一个无法 modify 的 final 修饰的 array reference 指向我们传入的 array object,并在内部进行 wrapper 实现了一系列 ArrayList 的 API。因此这个 method 相当于是一个从 Array 类 API 到 Collection 类对象 API 的连接转换桥梁,并不是我们所想的 to be as list 的 asList。

如何优雅地创建可操作的 List?

我们上面已经看到了,asList() 返回的是一个 ArrayList 的 View,并不是一个真正的 ArrayList,因此不能扩容。那么,我们创建一个真正的 ArrayList 不就行了。

这时候发现一开始的 for 循环方法还真是『大道至简』,没有这些坑就解决了问题。不过我们就是想要优雅,怎么做?

可以看到的是我们通过 asList() 可以将 Array 转变为一个 Collection 的形式,我们通过这个 Collection 作为传入参数来创建一个 ArrayList。

这样我们的 realList 就是一个创建出来的实打实的 ArrayList object,这样就可以对 List 进行修改操作了。

OK,这样就解决我们问题了。那么,如果我就是想显得我很高级,直接通过 int[] 来创建一个 List呢?

int[] 一步到位 List

上面我们已经 dive deep 解决了前面的两个坑,最后我们再回到最初的问题。我们有一个给定的 primitive type 的 int[] 数组,我就是想把它直接转变成一个 List ,有什么办法吗?

我们已经知道了 root cause 就是 int 不是一个 object 不具有 class 属性,因此对于 int 数组 Java 无法直接对整个数组进行 unboxing 操作得到一个 Integer[]。如果有 API 可以将所有的 int 进行 boxing 操作变成 Integer,那么就完事了。好在 Java SE 8 之后开始支持了这个功能:

这是 Java 提供的一个新的 Stream 类,正如其名可以将 Array 转变为一个 streaming 输入流,再调用 Stream 的 boxed() 方法一个个进行 boxing 操作,最后调用 collect() 方法选择需要什么形式的 Collection 结果。这里我们调用的是 toList() 即可得到最后的 ArrayList。

同理,如果我们需要反过来呢,如果面试官就非让你返回一个 Array,需要将 Collections 容器类转换成 Array 类,如从 List 得到 int[] Array,也可以使用同样的方法,将 list 转换为 stream 形式再逐个进行手动 unboxing。因此我们需要一个 method 来将 Integer 这个 wrapper class 对象转换为 primitive 的 int 值,Stream 提供了 mapToInt() 的方法,下面几种写法都可以成功:

Summary

今天我们一步一步 root cause 研究了 toList() API 的几个坑,也帮助大家掌握了几个 primitive Array和对应 wrapper class ArrayList 的转换方法,希望对大家有所帮助。欢迎有兴趣的同学阅读源码和 Oracle Doc 掌握这些 Java 程序语言设计中的一些坑,帮助自己更好地使用这些 API。八哥讲算法,我们下周见~

如果想要更系统地学习 Java 知识和提高算法编程能力,欢迎联系 Techbow 导师进行了解。