昨天同事开发的时候遇到了一个奇怪的问题。java
使用Guava作缓存,往里面存一个List,为了方便描述,称它为列表A,在另外一个地方取出来,再跟列表B中的元素进行差集处理,简单来讲,就像是下面这样:redis
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方法,而后就抛出了异常:spring
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火葬场。shell
但做为一名优秀的程序猿,怎么能被这点困难所难倒呢?因而开始了问题排查之旅。json
先来验证一下本身对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]
嗯,看来并无。dom
再回过头看看,抛出的异常是 UnsupportedOperationException
异常,并且是在 AbstractList
里抛出的,因而打开了 AbstractList
的源码。ide
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
。
这个内部类并无覆盖父类 AbstractList
的 remove
方法,因此调用的时候就会直接调用父类的 remove
方法,因而便发生了上面的异常。
为了更好的使用这里方法,咱们先来看看它的注释说明:
/** * 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<String> 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 */
从说明能够发现,有这么几点须要注意:
一、该方法返回的是一个固定长度的列表
因此它的长度是不能被改变的,也就不能对它进行添加和删除元素的操做,从它的内部类ArrayList的方法列表也能够看出,并无覆盖add和remove方法,所以对这两个方法的调用都会致使抛出异常。
虽然不能改变列表的长度,可是能够改变列表中的元素,以及元素的位置。好比经过set方法来从新设值,经过replaceAll方法来批量替换,经过sort方法来排序等等。
二、任何对列表的改动都会回写到原来是数组
也就是说对返回的列表进行的任何修改操做,都会致使原数组的改变。能够经过一个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]
注意最后一个输出,咱们修改原数组的元素,也会致使列表元素的改变,究其缘由,固然是由于列表只是将数组封装了起来而已,最终指向的都是同一个内存地址,所以修改天然也是同步的。
三、不能使用基本数据类型数组来做为参数
举个栗子:
@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之类,学习使用并不难,但若是不熟悉内部运行机制,仅仅当成一个黑盒的话,没法探索内部的精妙设计,遇到问题也比较难处理,若是只是把功能框定在其设定的能力范围以内,就没有办法进行定制化的改造。
嗯,看来个人历练路程还很长啊。最后用荀子的一句话来共勉吧。
“路虽弥,不行不至,
事虽小,不作不成。”