别再这样使用List了,会坑到你哭!

👉本文章全部文字纯原创,若是须要转载,请注明转载出处,谢谢!😘java

哈喽,各位朋友您们好,没想到这么快又跟你们见面了,由于此次写这篇文章又彻底是一个偶然的机会——一次无心的踩坑,这坑倒不是什么特别牛逼深刻的东西,不少大佬们估计都知道,说出来还有点low,可是我以前并不知道......😭因此浪费了很多时间,比较郁闷。再加上我相信确定仍是有一些朋友是不知道的,因此这一切都导致我想写点什么,帮助后来人避免踩坑。git

问题描述

估计不少人都知道Arrays这个工具类吧?顾名思义,这个工具类中都是封装对Java数组对象的操做,好比二分查找、拷贝数组、排序、流操做等很是实用功能,可是我今天要说的是其中的asList()方法。这个方法能够很轻松方便地将一个可变参数(本质就是数组)转换成一个List,相信不少朋友也和我同样这样使用,毕竟这样比先new一个List再往里面添加元素要方便不少。之前我也一直是这样想的,一直这样偷懒着用,舒服~😎常在河边站,哪有不湿鞋,直到昨天......github

昨天闲得蛋疼,无心中写了以下代码,没想到这一写,写出来了个猝不及防......面试

List<String> ls = Arrays.asList("1", "2", "3");
//MetaObject是一个Mybatis里面的一个封装对象的工具类,封装后可使用相似OGNL同样操做对象的属性,这个我从Mybatis里单独提取出来放github上了,有兴趣的能够玩玩。
//这个不是今天的重点,此处能够忽略。
MetaObject metaObject = SystemMetaObject.forObject(ls);
metaObject.add("4");
System.out.println(ls);
复制代码

上面这段代码貌似一看没啥问题,就是往一个List中add一个新元素呗,这能有啥问题啊?结果报错了......😭算法

al-1

报错了没啥稀奇的,学Java的要是没报过几个Bug都很差意思说本身是搞技术的,但坑爹的是,这错有必定的迷惑性。其实看报错提示,若是是细心的人就会产生一个疑问?报错的位置竟然是在AbstractList.add()!?震惊!没错,在AbstractList.add()这里确实啥也没实现,只会报出该异常,该方法是须要由各实现类各自实现的。后端

那为何错误会从这个地方抛出来的?Arrays.asList()这个玩意返回的究竟是个什么List类型啊?设计模式

al-2

深刻源码分析

因而,我天然是绝不犹豫地进入Arrays.asList()方法源码看,以下,奇怪,这不是一个ArrayList吗?但是ArrayList.add()是实现了AbstractList.add()的方法的啊,怎么还会报这个错呢?数组

al-3

就这么一个极其简单,过后回想起来都以为很无语的一个问题,整整困了我半个多小时啊......数据结构

其实这个问题至关的简单,若是这个时候能再往里深刻点一下,就知道其中的坑爹之处,其实此ArrayList非彼ArrayList......尼玛,这里面的这个ArrayList类只是Arrays类中的一个私有静态内部类。这个类确实继承了AbstractList类,可是并无去实现其add()方法,因此会直接抛出UnsupportedOperationException。其实这个问题至关简单,不过既然被这个类坑了,索性咱们就再全面了解一下这个类的内容吧。框架

al-4

其实,这个类中只是实现了AbstractList的一部分方法,并且绝大部分方法都是读取操做,好比上面说的add()操做并未实现,天然也就失去了ArrayList该有的动态伸缩的功能。所以,不可贵出结论,之前为了偷懒一直使用的Arrays.asList()确实能够返回一个List类型,可是这个List长度并不可变,无法像ArrayList那样作到动态伸缩,实现了set()方法能够作修改操做,但也仅限于你传入的数组范围,超事后会像数组同样抛出java.lang.ArrayIndexOutOfBoundsException。因此本质上返回的其实仍是一个数组。

mmp......太坑爹了,那么设计JDK的大佬为什么要设计这么一个坑爹的玩意呢?专坑我这样的菜鸟......😭不过郁闷归郁闷,仍是要搞清楚这样设计的初衷啊不是?其实,Arrays.asList()这个方法的真正意义是在于链接Java中的数组对象和Java集合的API,跟Collection.toArray()是一对兄弟。他们的关系以下图表示。

al-9

因此,若是咱们须要将一个数组(或者可变参数)转成一个List,正确操做姿式是以下代码所示:

List<String> ls = Arrays.asList("1", "2", "3");//或者是new String[]{"1", "2", "3"}
List<String> ls2 = new ArrayList<>(ls);
ls2.add("4");
System.out.println(ls2);
//输出结果:
[1, 2, 3, 4]
复制代码

哈哈,这时你获得的就是一个真正的ArrayList了,天然全部List相关操做都能正常执行了,其实这里面能够引伸出一种设计模式——适配器模式,这一点在阿里巴巴的《Java开发手册》中也有所说起。固然这边不是讲设计模式,不展开,后续会开启一系列关于设计模式的文章,但愿到时你们期待。这边客户端须要使用当前的接口(List)去对原有不兼容的接口(数组)操做,而Arrays.asList()刚好充当了适配器的角色,为数组和集合的转换提供了一个桥梁。实际上是本身用错该方法,错怪设计JDK API的大佬了......🤦‍♂️

知识延伸

既然说到了List,不妨再给你们引伸个内容,咱们发现Arrays$ArrayList除了继承了AbstractList,以及实现了咱们熟悉的java.io.Serializable,还另外实现了一个RandomAccess接口,我就顺便聊聊这玩意,毕竟我之前没了解过这玩意,估计大部分人也和我同样。点进去会发现,这个接口是个标记接口,里面啥东西都没有,JavaDoc却是写了不少。基本大意是说,这个标记接口用来声明能够快速随机访问的List。这边就引伸出2个问题,

什么是随机访问的List?用这个接口声明为随机访问的List有什么卵用?

学过List的都知道,经常使用的List主要有两个:ArrayList和LinkedList。这二者的区别相信不用我再赘述了,这也是面试题的高频选择。这二者最大的区别就是底层实现。ArrayList采用数组实现,而LinkedList采用链表。有点数据结构基础的人都知道数组在内存中是连续分配的,全部元素紧密排列,咱们可使用数组索引快速定位到该数组中任意一个元素。这种结构特色致使数组结构查询效率极高,时间复杂度O(1)。链表就恰好相反,每一个元素使用节点保存,节点和节点之间采用指针链接,因此要查询链表中某一元素,必须从表头元素依次向后遍历,致使其时间复杂度最差O(n)。

就是由于ArrayList和LinkedList的实现本质存在如此大的差别,因此聪明的JDK设计者在JDK API设计有关他们的算法时就会区别对待,这中间就是使用到了RandomAccess接口。

咱们先来看看ArrayList和LinkedList的源码,细心的人会发现,ArrayList实现了RandomAccess,而LinkedList并无,这是为何呢?这就跟我前面说的二者的差别性有关系了。

al-5

al-6

所谓的随机访问,就是指像数组这种结构,不管须要查找其中哪一个元素,这个元素出于什么位置,都是O(1)复杂度,效率很是高。这类List称之为能够随机访问的List,如ArrayList。而LinkedList则显然不是。

那用这个接口声明为随机访问的List到底有什么卵用呢?这只是一个空接口而已啊?

这里先跟你们聊一下标记接口。标记接口也叫空接口,顾名思义,这些接口不包含任何方法和属性,仅仅做为标记使用,那究竟是标记啥?标记它属于某一个特定的类型。Java中最典型的标记接口有*java.io.Serializablejava.lang.Cloneable。标记接口的惟一目的就是后续在一些算法中可使用instanceof进行类型查询。固然在JDK引入注解以后,这个功能也能够被标记注解替代了。同理,标记注解不包含成员。标记注解的惟一目的就是标记声明,后续可使用isAnnotationPresent()*方法进行查询。

RandomAccess接口到底在哪里有使用到?

Collections是JDK中一个很是实用的集合操做工具类,里面封装实现了大量对Java集合框架的操做,包括查找、排序、遍历、比较等功能。在这个类中,咱们能够发现有大量RandomAccess接口的身影。咱们以其中的binarySearch()方法为例,其他的方法你们有兴趣能够本身研究。

al-7

这是一个二分搜索方法。能够清晰的看出,这边使用RandomAccess接口来区别不一样的List类型,并执行不一样的算法。具体的算法实现不是咱们这边的重点。不可贵出结论,当一个List若是是实现了RandomAccess(好比ArrayList)或者元素个数<5000,就会采用索引方式遍历查找。不然会采用迭代器从头至尾遍历方式搜索。

其实RandomAccess接口的JavaDoc也提到了,若是是实现了RandomAccess接口的List,通常状况下采用索引遍历的性能会比直接用迭代器好一些,固然这是理论上的支持,具体实际测试状况,你们有兴趣能够本身性能测试一下结果,这边就再也不赘述。

结束

从下次开始,我将会开始写GOF23全部设计模式一整个系列文章,其实以前我也作过一些笔记放在github上,不过有读者反映笔记太过精简,看不懂,并且我的以为记笔记终归比不上文章来的深刻全面,因此此次打算将其丰满成一系列文章,为保证质量,保持在一周到十天左右一篇的频率,敬请期待。😊


  • 今天的技术分享就分享到这里,感谢您百忙抽出这么长时间阅读个人文章😊。
  • 另外,个人笔记还有文章也会在个人github上更新。
相关文章
相关标签/搜索