说明

昨天同事开发的时候遇到了一个奇怪的问题。

使用Guava做缓存,往里面存一个List,为了方便描述,称它为列表A,在另一个地方取出来,再跟列表B中的元素进行差集处理,简单来说,就像是下面这样:

public class ArrayListTest {
// 方便起见,这里用HashMap来做缓存
private Map<String, List<Long>> cache = new HashMap<>(); private void save(){
List<Long> listA = createListA();
cache.put("listA", listA);
} private void get(){
List<Long> listB = createListB();
List<Long> listA = cache.get("listA");
listA.removeAll(listB);
} private List<Long> createListA(){
···
} private List<Long> createListB(){
···
} public static void main(String[] args){
ArrayListTest test = new ArrayListTest();
test.save();
test.get();
}
}

先调用save方法,然后调用get方法,然后就抛出了异常:

Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.remove(AbstractList.java:161)
at java.util.AbstractList$Itr.remove(AbstractList.java:374)
at java.util.AbstractCollection.removeAll(AbstractCollection.java:376)
...

问题探索

究竟是人性的泯灭还是道德的沦丧,一个小小的List竟然也玩不转了,面对突如其来的打击,我跟同事都开始反思,复制粘贴一时爽,debug火葬场。

但作为一名优秀的程序猿,怎么能被这点困难所难倒呢?于是开始了问题排查之旅。

先来验证一下自己对ArrayList是否有什么误解:

@Test
public void testArrayList() {
List<Long> listA = new ArrayList<>();
listA.add(1L);
listA.add(2L);
List<Long> listB = new ArrayList<>();
listB.add(2L);
listB.add(3L);
listA.removeAll(listB);
System.out.println(JSON.toJSONString(listA));
}

输出如下:

[1]

嗯,看来并没有。

再回过头看看,抛出的异常是 UnsupportedOperationException 异常,而且是在 AbstractList 里抛出的,于是打开了 AbstractList的源码。

public E remove(int index) {
throw new UnsupportedOperationException();
}

AbstractList 类对remove方法的默认实现就是直接抛出一个异常,所以如果子类并没有覆盖该方法,就会出现上面的问题。

那么问题应该就出在列表A的创建方式上。

结果一找,发现列表A是通过 Arrays.asList() 创建的,再跟进代码:

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

感觉好像也没哪里不对,这里也是创建一个 ArrayList ,讲道理的话,应该没问题才对,不过等等,ArrayList 好像没有能传入可变长参数的构造函数吧,于是朝着这个ArrayList小手一点,终于发现了问题所在。

原来通过 Arrays.asList() 创建的 List 对象是通过实例化 Arrays 内部类 ArrayList 来创建的,所以这个 ArrayList 并不是我们常用的那个 ArrayList

这个内部类并没有覆盖父类 AbstractListremove 方法,所以调用的时候就会直接调用父类的 remove 方法,于是便发生了上面的异常。

Arrays.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, in
* combination with {@link Collection#toArray}. The returned list is
* serializable and implements {@link RandomAccess}.
*
* <p>This method also provides a convenient way to create a fixed-size
* list initialized to contain several elements:
* <pre>
* List&lt;String&gt; stooges = Arrays.asList("Larry", "Moe", "Curly");
* </pre>
*
* @param <T> the class of the objects in the array
* @param a the array by which the list will be backed
* @return a list view of the specified array
*/

从说明可以发现,有这么几点需要注意:

1、该方法返回的是一个固定长度的列表

所以它的长度是不能被改变的,也就不能对它进行添加和删除元素的操作,从它的内部类ArrayList的方法列表也可以看出,并没有覆盖add和remove方法,因此对这两个方法的调用都会导致抛出异常。

虽然不能改变列表的长度,但是可以改变列表中的元素,以及元素的位置。比如通过set方法来重新设值,通过replaceAll方法来批量替换,通过sort方法来排序等等。

2、任何对列表的改动都会回写到原来是数组

也就是说对返回的列表进行的任何修改操作,都会导致原数组的改变。可以通过一个Test来测试一下:

@Test
public void testArrays() {
Long[] longs = {1L,2L,4L,3L};
List<Long> longList = Arrays.asList(longs);
System.out.println("longList:" + JSON.toJSONString(longList) + "longs:" + JSON.toJSONString(longs)); longList.set(1, 5L);
System.out.println("longList:" + JSON.toJSONString(longList) + "longs:" + JSON.toJSONString(longs)); longList.replaceAll(a -> a + 1L);
System.out.println("longList:" + JSON.toJSONString(longList) + "longs:" + JSON.toJSONString(longs)); longList.sort(Long::compareTo);
System.out.println("longList:" + JSON.toJSONString(longList) + "longs:" + JSON.toJSONString(longs)); longs[2] = 7L;
System.out.println("longList:" + JSON.toJSONString(longList) + "longs:" + JSON.toJSONString(longs));
}

输出如下:

longList:[1,2,4,3]longs:[1,2,4,3]
longList:[1,5,4,3]longs:[1,5,4,3]
longList:[2,6,5,4]longs:[2,6,5,4]
longList:[2,4,5,6]longs:[2,4,5,6]
longList:[2,4,7,6]longs:[2,4,7,6]

注意最后一个输出,我们修改原数组的元素,也会导致列表元素的改变,究其原因,当然是因为列表只是将数组封装了起来而已,最终指向的都是同一个内存地址,因此修改自然也是同步的。

3、不能使用基本数据类型数组来作为参数

举个栗子:

@Test
public void testArrays2() {
int[] ints = { 1, 2, 3 };
List list = Arrays.asList(ints);
System.out.println(list.size());
}

这里并不会报错,而是会输出1。为什么呢?

再回过头去看下说明:

@param <T> the class of the objects in the array

参数的类型T指的是数组中的元素类型,如果数组中元素类型是基本类型,就会把整个数组当成一个元素,我们把上面的栗子稍微修改一下就清楚了。

@Test
public void testArrays2() {
int[] ints = { 1, 2, 3 };
System.out.println(ints.getClass());
List list = Arrays.asList(ints);
System.out.println(JSON.toJSONString(list));
}

输出如下:

class [I
[[1,2,3]]

注意第二行的输出是一个二维数组。变长参数本质上就是一个对象数组,所以如果传入一个Integer数组,就能正常接收:

@Test
public void testArrays2() {
Integer[] ints = { 1, 2, 3 };
System.out.println(ints.getClass());
List list = Arrays.asList(ints);
System.out.println(list.size());
}
class [Ljava.lang.Integer;
3

总结

至此,关于 Arrays.asList() 的探索之旅就结束了,遇到问题一般跟一跟源码就差不多能解决了,但对于常用的类,如果对其内部的运行机制不熟悉的话,代码就会容易出现一些不符合预期的行为,报错的异常并不可怕,因为可以根据异常很快定位,最怕的就是不报错,能正常运行,但是数据处理却是错误的,等到真正发现的时候,可能已经造成了难以挽回的损失。

看来主动阅读源码还是相当有必要的,其实Arrays.asList()并不难使用,推而广之,就像Guava、fastjson这些模块,或者spring、redis、dubbo之类,学习使用并不难,但如果不熟悉内部运行机制,仅仅当成一个黑盒的话,无法探索内部的精妙设计,遇到问题也比较难处理,如果只是把功能框定在其设定的能力范围之内,就没有办法进行定制化的改造。

嗯,看来我的历练路程还很长啊。最后用荀子的一句话来共勉吧。

“路虽弥,不行不至,

事虽小,不做不成。”

【问题总结】万万没想到,竟然栽在了List手里的更多相关文章

  1. 头条编程题 万万没想到之抓捕孔连顺 JavaScript

    [编程题] 万万没想到之抓捕孔连顺 时间限制:1秒 空间限制:131072K 我叫王大锤,是一名特工.我刚刚接到任务:在字节跳动大街进行埋伏,抓捕恐怖分子孔连顺.和我一起行动的还有另外两名特工,我提议 ...

  2. 字节跳动:[编程题]万万没想到之聪明的编辑 Java

    时间限制:1秒 空间限制:32768K 我叫王大锤,是一家出版社的编辑.我负责校对投稿来的英文稿件,这份工作非常烦人,因为每天都要去修正无数的拼写错误.但是,优秀的人总能在平凡的工作中发现真理.我发现 ...

  3. 万万没想到!ModelArts与AppCube组CP了

    摘要:嘘,华为云内部都不知道的秘密玩法,我悄悄告诉您! 双"魔"合璧庆双节 ↑开局一张图,故事全靠编 华为云的一站式开发平台ModelArts和应用魔方AppCube居然能玩到一起 ...

  4. go 学习笔记之万万没想到宠物店竟然催生出面向接口编程?

    到底是要猫还是要狗 在上篇文章中,我们编撰了一则简短的小故事用于讲解了什么是面向对象的继承特性以及 Go 语言是如何实现这种继承语义的,这一节我们将继续探讨新的场景,希望能顺便讲解面向对象的接口概念. ...

  5. 万万没想到,Spring Boot 竟然这么耗内存!

    Spring Boot总体来说,搭建还是比较容易的,特别是Spring Cloud全家桶,简称亲民微服务. 但在发展趋势中,容器化技术已经成熟,面对巨耗内存的Spring Boot,小公司表示用不起. ...

  6. 万万没想到,面试中,连 ClassLoader类加载器 也能问出这么多问题…..

    1.类加载过程 类加载时机 「加载」 将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存上创建一个java.lang.Class对象用来封装类在方法区内的数据 ...

  7. 万万没想到,JVM内存区域的面试题也可以问的这么难?

    二.Java内存区域 1.Java内存结构 内存结构 程序计数器 当前线程所执行字节码的行号指示器.若当前方法是native的,那么程序计数器的值就是undefined. 线程私有,Java内存区域中 ...

  8. N皇后求解。万万没想到,只用一个一维数组就搞定了。还体现了回溯。

    一.啥是N皇后?先从四皇后入手 给定一个4x4的棋盘,要在棋盘上放置4个皇后.他们的位置有这样的要求,每一列,每一行,每一对角线都能有一个皇后. 你可能会对这个对角线有疑惑,其实就是每一个小正方形的对 ...

  9. 万万没想到,3D打印居然可以做这些逆天设计

    3D打印一直被冠以“高科技”头衔,似乎离我们的日常生活还很遥远.其实不然,随着技术的创新,3D打印技术逐渐深入各个领域,工业生产.商业.医学.建筑.艺术等领域都能看到3D打印技术的影子.它将会改变我们 ...

随机推荐

  1. 【JAVA学习】struts2的action中使用session的方法

    尊重版权:http://hi.baidu.com/dillisbest/item/0bdc35c0b477b853ad00efac 在Struts2里,假设须要在Action中使用session.能够 ...

  2. ios之编码规范具体说明

    iOS代码规范: 所有代码规范所有遵循苹果sdk的原则,不清楚的请訪问苹果SDK文档或下载官方Demo查看. 1.project部分: 将项目中每一个功能模块相应的源文件放入同一目录下,使用虚拟目录. ...

  3. SAP-财务知识点

    [转自 http://blog.itpub.net/195776/viewspace-1023912/] SAP FI/CO Reading RepositorySAP财务成本知识库 目 录前言.一. ...

  4. 打开蓝牙debug hci log

    Android4.2之前抓取hci log都是通过hcidump命令完成的,但是Android4.2 Bluetooth引入了Bluedroid,这是一个新的蓝牙协议栈.所以抓取hci log的方法也 ...

  5. python的类型

    弱类型是可以自由转换的,如js,字符串和数字能相加 强类型不能自由转换,如python,要加上函数转成相同的类型

  6. okhttp 请求list数据实例

    public class DataBean { /** * id : 61684 * movieName : <猜火车2>先导预告片 * coverImg : http://img31.m ...

  7. HDU - 1160 FatMouse's Speed 【DP】

    题目链接 http://acm.hdu.edu.cn/showproblem.php?pid=1160 题意 给出一系列的 wi si 要找出一个最长的子序列 满足 wi 是按照升序排列的 si 是按 ...

  8. 2 《锋利的jQuery》jQuery选择器

    tip1:jquery检查某个元素是否存在:if($("#tt").length>0){}或者if($("#tt")[0]){} 先说css选择器有: 标 ...

  9. 20145239 《Java程序设计》实验三 实验报告

    详见我的parter20145224的博客:http://www.cnblogs.com/20145224kevs/p/5428892.html 感想:这次的实验看似容易,但很多点都需要注意,比如开源 ...

  10. 《CSS权威指南(第三版)》---第二章 选择器

    本章的主要内容是,怎么获取文档中的元素给予渲染: 1.元素选择器: 2.ID选择器: 3.CLSSS选择器: 4.通配选择器:*; 5.属性选择器:selector[] 6.部分属性选择器: sele ...