我是石头,,一个在程序圈混迹多年,主业 Java,另外 Python、React 也玩儿的 6 的斜杠开发者。
请看下面的代码,谁能看出它有什么问题吗?
Stringa ="古时的";
Stringb ="石头";
List stringList = Arrays.asList(a,b);
stringList.add("!!!");
这是一个小白程序员问我的问题。
他说:石头,帮我看看这代码有什么问题吗,为什么报错呢,啥操作都没有啊?
我:看上去确实没什么问题,但是我确实没用过 Arrays.asList这个方法,报什么错误?
他:异常信息是
java.lang.UnsupportedOperationException,是调用 add 方法时抛出的。
恩,我大概明白了,这可能是 ArrayList的又一个坑,和 subList应该有异曲同工之妙。
Arrays.asList
Arrays.asList 方法接收一个变长泛型,最后返回 List,好像是个很好用的方法啊。
有了它,我们总是说的 ArrayList 初始化方式是不是就能更优雅了,既不用{{这种双括号方式,也不用先 new ArrayList,然后再调用 add方法一个个往里加了。但是,为啥没有提到这种方式呢?
虽然问题很简单,但还是有必要看一下原因的。于是,写了上面这 4 行代码做个测试,运行起来确实抛了异常,异常如下:
直接看源码吧,定位到 Arrays.asList 方法看一看。
publicstaticListasList(T... a){
returnnewArrayList<>(a);
}
咦,是 new 了一个 ArrayList出来呀,怎么会不支持 add操作呢,不仔细看还真容易被唬住,此ArrayList非彼ArrayList,这是一个内部类,但是类名也叫 ArrayList,你说坑不坑。
privatestaticclassArrayListextendsAbstractList
implementsRandomAccess,java.io.Serializable{
privatestaticfinallongserialVersionUID = -2764017481108945198L;
privatefinalE[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
publicintsize(){
returna.length;
}
@Override
publicObject[] toArray() {
returna.clone();
}
@Override
@SuppressWarnings("unchecked")
public T[] toArray(T[] a) {
intsize = size();
if(a.length < size)
returnArrays.copyOf(this.a, size,
(Class) a.getClass());
System.arraycopy(this.a,0, a,0, size);
if(a.length > size)
a[size] =null;
returna;
}
@Override
publicEget(intindex){
returna[index];
}
@Override
publicEset(intindex, E element){
E oldValue = a[index];
a[index] = element;
returnoldValue;
}
@Override
publicintindexOf(Object o){
E[] a =this.a;
if(o ==null) {
for(inti =0; i < a.length; i++)
if(a[i] ==null)
returni;
}else{
for(inti =0; i < a.length; i++)
if(o.equals(a[i]))
returni;
}
return-1;
}
@Override
publicbooleancontains(Object o){
returnindexOf(o) != -1;
}
@Override
publicSpliteratorspliterator(){
returnSpliterators.spliterator(a, Spliterator.ORDERED);
}
@Override
publicvoidforEach(Consumer action){
Objects.requireNonNull(action);
for(E e : a) {
action.accept(e);
}
}
@Override
publicvoidreplaceAll(UnaryOperator<E> operator){
Objects.requireNonNull(operator);
E[] a =this.a;
for(inti =0; i < a.length; i++) {
a[i] = operator.apply(a[i]);
}
}
@Override
publicvoidsort(Comparator c){
Arrays.sort(a, c);
}
}
里面定义了 set、get等基本的方法,但是没有重写add方法,这个类也是继承了 AbstractList,但是 add方法并没有具体的实现,而是抛了异常出来,具体的逻辑需要子类自己去实现的。
publicvoidadd(intindex, E element){
thrownewUnsupportedOperationException();
}
所以说,Arrays.asList方法创建出来的 ArrayList 和真正我们平时用的 ArrayList只是继承自同一抽象类的两个不同子类,而 Arrays.asList创建的 ArrayList 只能做一些简单的视图使用,不能做过多操作,所以 ArrayList的几种初始化方式里没有 Arrays.asList这一说。
subList 方法
上面提到了那个问题和 subList的坑有异曲同工之妙,都是由于返回的对象并不是真正的 ArrayList类型,而是和 ArrayList集成同一父类的不同子类而已。
坑之一
所以会产生第一个坑,就是把当把 subList返回的对象转换成 ArrayList 的时候
List stringList =newArrayList<>();
stringList.add("我");
stringList.add("是");
stringList.add("石头");
List subList = (ArrayList) stringList.subList(0,2);
会抛出下面的异常:
java.lang.ClassCastException: java.util.ArrayList$SubListcannot be cast to java.util.ArrayList
原因很明了,因为这俩根本不是一个对象,也不存在继承关系,如果真说有什么关系,顶多算是兄弟关系,因为都继承了 AbstractList 嘛 。
坑之二
当你在 subList 中操作的时候,其实就是在操作原始的 ArrayList,不明所以的同学以为这是一个副本列表,然后在 subList 上一顿操作猛如虎,最后回头一看原始 ArrayList已然成了二百五。
例如下面这段代码,在 subList 上新增了一个元素,然后又删除了开头的一个元素,结果回头一看原始的 ArrayList,发现它的结果也发生了变化。
List stringList =newArrayList<>();
stringList.add("我");
stringList.add("是");
stringList.add("石头");
List subList = stringList.subList(0,3);
subList.add("!!!");
subList.remove(0);
System.out.println("------------------");
System.out.println("修改后的 subList");
System.out.println("------------------");
for(String s : subList) {
System.out.println(s);
}
System.out.println("------------------");
System.out.println("原始 ArrayList");
System.out.println("------------------");
for(String a : stringList) {
System.out.println(a);
}
以上代码的输出结果:
------------------
修改后的 subList
------------------
是
石头
!!!
------------------
原始 ArrayList
------------------
是
石头
!!!
为什么会发生这样的情况呢,因为 subList的实现就是这样子啊,捂脸。我们可以看一下 subList 这个方法的源码。
publicListsubList(intfromIndex,inttoIndex){
subListRangeCheck(fromIndex, toIndex, size);
returnnewSubList(this,0, fromIndex, toIndex);
}
看到它内部是 new 了一个 SubList 类,这个类就是上面提到的 ArrayList的子类,看到第一个参数 this了吗,this就是当前的 ArrayList 原始列表,之后的增删改其实都是在 this上操作,最终也就是在原始列表上进行的操作,所以你的一举一动最后都会诚实的反应到原始列表上,之后你再想用原始列表,对不起,已经找不到了。
坑之三
如果你使用 subList 方法获取了一个子列表,这之后又在原始列表上进行了新增或删除的操作,这是,你之前获取到的 subList 就已经废掉了,不能用了,不能用的意思就是你在 subList 上进行遍历、增加、删除操作都会抛出异常,没错,连遍历都不行了。
例如下面这段代码
List stringList =newArrayList<>();
stringList.add("我");
stringList.add("是");
stringList.add("石头");
List subList = stringList.subList(0,3);
// 原始列表元素个数改变
stringList.add("!!!");
// 遍历 subList
for (String s : subList) {
System.out.println(s);
}
// get 元素
subList.get(0);
// remove 元素
subList.remove(0);
//增加元素
subList.add("hello");
遍历、get、remove、add 都会抛出以下异常
其实与二坑的原因相同,subList 其实操作的是原始列表,当你在 subList 上进行操作时,会执行 checkForComodification方法,此方法会检查原始列表的个数是否和最初的相同,如果不相同,直接抛出
ConcurrentModificationException异常。
privatevoidcheckForComodification(){
if(ArrayList.this.modCount !=this.modCount)
thrownewConcurrentModificationException();
}
最后
没有在项目中踩过 JDK 坑的程序员,不足以谈人生。所以,各位同学在使用一些看似简单、优雅的方法时,一定要清楚它的特性和原理,不然就离坑不远了。
壮士且慢,先给点个赞吧,总是被白嫖,身体吃不消!
作者:古时的风筝
链接:https://juejin.im/post/5ed066fe6fb9a047a8622134