我是风筝,公众号「古时的风筝」,一个不只有技术的技术公众号,一个在程序圈混迹多年,主业 Java,另外 Python、React 也玩儿的 6 的斜杠开发者。

Spring Cloud 系列文章已经完成,可以到 我的github 上查看系列完整内容。也可以在公众号内回复「pdf」获取我精心制作的 pdf 版完整教程。

请看下面的代码,谁能看出它有什么问题吗?

  1. String a = "古时的";
  2. String b = "风筝";
  3. List<String> stringList = Arrays.asList(a,b);
  4. stringList.add("!!!");

这是一个小白程序员问我的问题。

他说:成哥,帮我看看这代码有什么问题吗,为什么报错呢,啥操作都没有啊?

我:看上去确实没什么问题,但是我确实没用过 Arrays.asList这个方法,报什么错误?

他:异常信息是 java.lang.UnsupportedOperationException,是调用 add 方法时抛出的。

恩,我大概明白了,这可能是 ArrayList的又一个坑,和 subList应该有异曲同工之妙。

Arrays.asList

Arrays.asList 方法接收一个变长泛型,最后返回 List,好像是个很好用的方法啊,有了它,我们总是说的 ArrayList 初始化方式是不是就能更优雅了,既不用{{这种双括号方式,也不用先 new ArrayList,然后再调用 add方法一个个往里加了。但是,为啥没有提到这种方式呢?

虽然问题很简单,但还是有必要看一下原因的。于是,写了上面这 4 行代码做个测试,运行起来确实抛了异常,异常如下:

直接看源码吧,定位到 Arrays.asList 方法看一看。

  1. public static <T> List<T> asList(T... a) {
  2. return new ArrayList<>(a);
  3. }

咦,是 new 了一个 ArrayList出来呀,怎么会不支持 add操作呢,不仔细看还真容易被唬住,此ArrayList非彼ArrayList,这是一个内部类,但是类名也叫 ArrayList,你说坑不坑。

  1. private static class ArrayList<E> extends AbstractList<E>
  2. implements RandomAccess, java.io.Serializable {
  3. private static final long serialVersionUID = -2764017481108945198L;
  4. private final E[] a;
  5. ArrayList(E[] array) {
  6. a = Objects.requireNonNull(array);
  7. }
  8. @Override
  9. public int size() {
  10. return a.length;
  11. }
  12. @Override
  13. public Object[] toArray() {
  14. return a.clone();
  15. }
  16. @Override
  17. @SuppressWarnings("unchecked")
  18. public <T> T[] toArray(T[] a) {
  19. int size = size();
  20. if (a.length < size)
  21. return Arrays.copyOf(this.a, size,
  22. (Class<? extends T[]>) a.getClass());
  23. System.arraycopy(this.a, 0, a, 0, size);
  24. if (a.length > size)
  25. a[size] = null;
  26. return a;
  27. }
  28. @Override
  29. public E get(int index) {
  30. return a[index];
  31. }
  32. @Override
  33. public E set(int index, E element) {
  34. E oldValue = a[index];
  35. a[index] = element;
  36. return oldValue;
  37. }
  38. @Override
  39. public int indexOf(Object o) {
  40. E[] a = this.a;
  41. if (o == null) {
  42. for (int i = 0; i < a.length; i++)
  43. if (a[i] == null)
  44. return i;
  45. } else {
  46. for (int i = 0; i < a.length; i++)
  47. if (o.equals(a[i]))
  48. return i;
  49. }
  50. return -1;
  51. }
  52. @Override
  53. public boolean contains(Object o) {
  54. return indexOf(o) != -1;
  55. }
  56. @Override
  57. public Spliterator<E> spliterator() {
  58. return Spliterators.spliterator(a, Spliterator.ORDERED);
  59. }
  60. @Override
  61. public void forEach(Consumer<? super E> action) {
  62. Objects.requireNonNull(action);
  63. for (E e : a) {
  64. action.accept(e);
  65. }
  66. }
  67. @Override
  68. public void replaceAll(UnaryOperator<E> operator) {
  69. Objects.requireNonNull(operator);
  70. E[] a = this.a;
  71. for (int i = 0; i < a.length; i++) {
  72. a[i] = operator.apply(a[i]);
  73. }
  74. }
  75. @Override
  76. public void sort(Comparator<? super E> c) {
  77. Arrays.sort(a, c);
  78. }
  79. }

里面定义了 setget等基本的方法,但是没有重写add方法,这个类也是继承了 AbstractList,但是 add方法并没有具体的实现,而是抛了异常出来,具体的逻辑需要子类自己去实现的。

  1. public void add(int index, E element) {
  2. throw new UnsupportedOperationException();
  3. }

所以说,Arrays.asList方法创建出来的 ArrayList 和真正我们平时用的 ArrayList只是继承自同一抽象类的两个不同子类,而 Arrays.asList创建的 ArrayList 只能做一些简单的视图使用,不能做过多操作,所以 ArrayList的几种初始化方式里没有 Arrays.asList这一说。

subList 方法

上面提到了那个问题和 subList的坑有异曲同工之妙,都是由于返回的对象并不是真正的 ArrayList类型,而是和 ArrayList集成同一父类的不同子类而已。

坑之一

所以会产生第一个坑,就是把当把 subList返回的对象转换成 ArrayList 的时候

  1. List<String> stringList = new ArrayList<>();
  2. stringList.add("我");
  3. stringList.add("是");
  4. stringList.add("风筝");
  5. List<String> subList = (ArrayList) stringList.subList(0, 2);

会抛出下面的异常:

  1. java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList

原因很明了,因为这俩根本不是一个对象,也不存在继承关系,如果真说有什么关系,顶多算是兄弟关系,因为都继承了 AbstractList 嘛 。

坑之二

当你在 subList 中操作的时候,其实就是在操作原始的 ArrayList,不明所以的同学以为这是一个副本列表,然后在 subList 上一顿操作猛如虎,最后回头一看原始 ArrayList已然成了二百五。

例如下面这段代码,在 subList 上新增了一个元素,然后又删除了开头的一个元素,结果回头一看原始的 ArrayList,发现它的结果也发生了变化。

  1. List<String> stringList = new ArrayList<>();
  2. stringList.add("我");
  3. stringList.add("是");
  4. stringList.add("风筝");
  5. List<String> subList = stringList.subList(0, 3);
  6. subList.add("!!!");
  7. subList.remove(0);
  8. System.out.println("------------------");
  9. System.out.println("修改后的 subList");
  10. System.out.println("------------------");
  11. for (String s : subList) {
  12. System.out.println(s);
  13. }
  14. System.out.println("------------------");
  15. System.out.println("原始 ArrayList");
  16. System.out.println("------------------");
  17. for (String a : stringList) {
  18. System.out.println(a);
  19. }

以上代码的输出结果:

  1. ------------------
  2. 修改后的 subList
  3. ------------------

  4. 风筝
  5. !!!
  6. ------------------
  7. 原始 ArrayList
  8. ------------------

  9. 风筝
  10. !!!

为什么会发生这样的情况呢,因为 subList的实现就是这样子啊,捂脸。我们可以看一下 subList 这个方法的源码。

  1. public List<E> subList(int fromIndex, int toIndex) {
  2. subListRangeCheck(fromIndex, toIndex, size);
  3. return new SubList(this, 0, fromIndex, toIndex);
  4. }

看到它内部是 new 了一个 SubList 类,这个类就是上面提到的 ArrayList的子类,看到第一个参数 this了吗,this就是当前的 ArrayList 原始列表,之后的增删改其实都是在 this上操作,最终也就是在原始列表上进行的操作,所以你的一举一动最后都会诚实的反应到原始列表上,之后你再想用原始列表,对不起,已经找不到了。

坑之三

如果你使用 subList 方法获取了一个子列表,这之后又在原始列表上进行了新增或删除的操作,这是,你之前获取到的 subList 就已经废掉了,不能用了,不能用的意思就是你在 subList 上进行遍历、增加、删除操作都会抛出异常,没错,连遍历都不行了。

例如下面这段代码

  1. List<String> stringList = new ArrayList<>();
  2. stringList.add("我");
  3. stringList.add("是");
  4. stringList.add("风筝");
  5. List<String> subList = stringList.subList(0, 3);
  6. // 原始列表元素个数改变
  7. stringList.add("!!!");
  8. // 遍历 subList
  9. for (String s : subList) {
  10. System.out.println(s);
  11. }
  12. // get 元素
  13. subList.get(0);
  14. // remove 元素
  15. subList.remove(0);
  16. //增加元素
  17. subList.add("hello");

遍历、get、remove、add 都会抛出以下异常

其实与二坑的原因相同,subList 其实操作的是原始列表,当你在 subList 上进行操作时,会执行 checkForComodification方法,此方法会检查原始列表的个数是否和最初的相同,如果不相同,直接抛出 ConcurrentModificationException异常。

  1. private void checkForComodification() {
  2. if (ArrayList.this.modCount != this.modCount)
  3. throw new ConcurrentModificationException();
  4. }

最后

没有在项目中踩过 JDK 坑的程序员,不足以谈人生。所以,各位同学在使用一些看似简单、优雅的方法时,一定要清楚它的特性和原理,不然就离坑不远了。


壮士且慢,先给点个推荐吧,总是被白嫖,身体吃不消!

我是风筝,公众号「古时的风筝」。一个兼具深度与广度的程序员鼓励师,一个本打算写诗却写起了代码的田园码农!你可选择现在就关注我,或者看看历史文章再关注也不迟。

读了这一篇,让你少踩 ArrayList 的那些坑的更多相关文章

  1. 搭载Dubbo+Zookeeper踩了这么多坑,我终于决定写下这篇!

    大家好,我是melo,一名大二上软件工程在读生,经历了一年的摸滚,现在已经在工作室里边准备开发后台项目啦. 这篇文章我们不谈数据结构了,来谈谈入门分布式踩过的坑.感觉到了分布式这一层,由于技术更新迭代 ...

  2. Redis上踩过的一些坑

    来自: http://blog.csdn.net//chenleixing/article/details/50530419 上上周和同事(龙哥)参加了360组织的互联网技术训练营第三期,美团网的DB ...

  3. 安装python爬虫scrapy踩过的那些坑和编程外的思考

    这些天应朋友的要求抓取某个论坛帖子的信息,网上搜索了一下开源的爬虫资料,看了许多对于开源爬虫的比较发现开源爬虫scrapy比较好用.但是以前一直用的java和php,对python不熟悉,于是花一天时 ...

  4. [原创]阿里云RocketMQ踩过的哪些坑

    由于公司的最近开始使用RocketMQ来做支付业务处理, 便开启了学习阿里云RocketMQ的学习与实践之路, 其中踩了不少的坑, 大部份是由于没有仔细查看阿里云的技术文档而踩的坑. 但是有一个非常大 ...

  5. [转帖]美团在Redis上踩过的一些坑-5.redis cluster遇到的一些问题

    美团在Redis上踩过的一些坑-5.redis cluster遇到的一些问题 博客分类: redis 运维 redis clustercluster-node-timeoutfailover  转载请 ...

  6. [转帖]美团在Redis上踩过的一些坑-4.redis内存使用优化

    美团在Redis上踩过的一些坑-4.redis内存使用优化 博客分类: 运维 redis redisstringhash优化segment-hash  转载请注明出处哈:http://carlosfu ...

  7. [转帖]美团在Redis上踩过的一些坑-1.客户端周期性出现connect timeout

    美团在Redis上踩过的一些坑-1.客户端周期性出现connect timeout 博客分类: redis 运维 jedisconnect timeoutnosqltcp  转载请注明出处哈:http ...

  8. 三分之一的程序猿之社交类app踩过的那些坑

    三分之一的程序猿之社交类app踩过的那些坑 万众创新,全民创业.哪怕去年陌生人社交不管融资与否都倒闭了不知道多少家,但是依然有很多陌生人社交应用层出不穷的冒出来.各种脑洞大开,让人拍案叫起. 下面我们 ...

  9. 【Fine原创】JMeter分布式测试中踩过的那些坑

    最近因为项目需要,研究了性能测试的相关内容,并且最终选用了jmeter这一轻量级开源工具.因为一直使用jmeter的GUI模式进行脚本设计,到测试执行阶段工具本身对资源的过量消耗给性能测试带来了瓶颈, ...

随机推荐

  1. 图论--BFS总结

    1.关于BFS的Key_word: ①hash或状态压缩记录状态  ②状态剪枝 ③反向BFS ④双向BFS ⑤特殊初始化VIS数组 ⑥动态图的搜索 ⑦优先队列优化搜索 ⑧数位搜索 下面是一一讲解: 1 ...

  2. spring的bean的属性注入

    一.设置注入 设置注入要求: 要求属性在实体类中必须有getter 和setter方法,然后在spring的工厂中就可以使用property标签进行设值注入. 二.构造注入 通过类的构造方法的方式注入 ...

  3. 微信小程序-swiper(轮播图)抖动问题

    ps:问题 组件swiper(轮播图)真机上不自动滚动 一直卡在那里抖动 以前遇到这个问题,官方一直没有正面回复.就搁置了,不过有大半年没写小程序了也没去关注,今天就去看了下官方文档,发觉更新了点好东 ...

  4. MySQL基础总结(二)

    数据表的完整性约束条件 AUTO_INCREMENT (自增长) 注意事项: 1.一个表中只能有一个自增长字段 2.必须配合主键使用 方法1: 方法2: 方法3: 指定自增长初始值的方法: 修改自增长 ...

  5. Scrapy模块使用出错,出现builtins.ImportError: DLL load failed: 找不到指定的程序

    问题描述:初次学习scrapy,使用scrapy官方文档创建爬虫项目出错, 出现builtins.ImportError: DLL load failed: 找不到指定的程序, ImportError ...

  6. X Error:BadDrawable (individ Pixmap or Window parameter 9)

    #描述 平台:aarch64 系统:ubuntu16.04.02 Qt Version:4.8.7 Qt程序可以正常运行,界面渲染出现问题以及乱码,控制提示错误内容: "unable to ...

  7. elasticsearch kibana的安装部署与简单使用(二)

    介绍一下elasticsearch和kibana的简单使用 es其实我理解为一个数据库,一个数据库无非就是增删改查, Delete  PUT GET POST 这些接口关键字完美对应 比如,我想查一张 ...

  8. [UVA Live 12931 Common Area]扫描线

    题意:判断两个多边形是否有面积大于0的公共部分 思路:扫描线基础. #pragma comment(linker, "/STACK:10240000") #include < ...

  9. Kubernetes管理员手边必备的9个kubectl命令

    导语:将这9个关键的kubectl命令放在手边,它们可以帮您快速排除故障并管理Kubernetes集群. Kubernetes是当今基础架构的主导技术,这意味着系统管理员需要熟悉其管理.多年来,笔者一 ...

  10. RunLoop总结

    参考学习了YY大神的http://blog.ibireme.com/2015/05/18/runloop/#mode RunLoop: 看做是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一 ...