list移除特定条件参数
曾经和之前公司leader聊过,如何面试1年左右工作经验的java开发。当时我的leader当场说了一个他常用的面试笔试问题,给定list内含有1,2,3,4,5五个值,如何移除值为2的参数。
从java1.6的角度来看,这是一个很容易出坑的问题,毕竟如果你代码写成以下这样,就说明你上套了:
ArrayList<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5); int size = list.size(); for (int i = 0; i < size; i++) { if (list.get(i) == 2) { list.remove(i); } }
这是刚开始写java的时候最容易遇到的坑,因为list执行过remove之后,list本身的size已经变成了4,而当循环内i走到4时,此时list长度为4,是获取不到下标为4的参数的,具体源码如下:
public E get(int index) { rangeCheck(index); return elementData(index); } private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
当然,开发一段时间后有些人可能习惯用foreach,写法如下:
ArrayList<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5); for (Integer i : list) { if(i==2){ list.remove(i); } }
代码量减少了,而且也不报错,万事大吉?no,no,no,这种写法最坑的地方就是正常不报错,一旦移除的值在最后一位,例如i==5,立刻报错,为了便于理解,我们无妨吧foreach循环转换成java编译时自动转换后的样子,如下:
ArrayList<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5); Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()){ Integer i = iterator.next(); if(i==5){ list.remove(i); } }
简单来讲,当list.remove执行的时候了,list内的数组长度变成了4,但是iterator的依旧会尝试取第五位。所以产生了问题(jdk在进入此前,很巧妙的记录了list与iterator的变化次数,并进行对比防止此问题)。但是为什么不移除最后一位的时候就不会有问题,这是因为iterator.hasNext方法会比对当前位是否不等于list长度,当移除中间变量时,走到最后倒数第二位时,下一次循环当前位数为4,数组长度为4,跳出了循环,即5这个参数其实不会参加循环,有兴趣的同学可以再循环内打印i即可观察到(debug当然也可以)。
但其实jdk设计的时候已经考虑过此问题,所以实际上iterator本身提供了remove方法,他会同步修改各参数,iterator相关源码如下
public boolean hasNext() {
// cursor为迭代器当前位置,size为list长度 return cursor != size; } public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } final void checkForComodification() {
// modCount为list改变次数,exceptedModCount为迭代器预期改变次数 if (modCount != expectedModCount) throw new ConcurrentModificationException(); } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
最后,附上1.6年代的有效代码如下:
ArrayList<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5); Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()){ Integer i = iterator.next(); if(i==5){ iterator.remove(); } }
当然,到此还远远不是结束的时候,我们知道java1.8中引入了函数式编程,完美跳过此坑,我们可以使用以下代码
List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);
list= list.stream().filter(e -> e != 5).collect(Collectors.toList());
上述方法的好处是使用stream表达式可以继续进行其他处理,如果单纯只是移除指定值,我们还可以使用jdk1.8的自带方法,如下:
List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);
list.removeIf(e->e==5);
结束前附上jdk的Collections与ArrayList对removeIf的实现,可以看到jdk中ArrayList其实并没有使用迭代器进行remove。
default boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); boolean removed = false; final Iterator<E> each = iterator(); while (each.hasNext()) { if (filter.test(each.next())) { each.remove(); removed = true; } } return removed; } public boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); // figure out which elements are to be removed // any exception thrown from the filter predicate at this stage // will leave the collection unmodified int removeCount = 0; final BitSet removeSet = new BitSet(size); final int expectedModCount = modCount; final int size = this.size; for (int i=0; modCount == expectedModCount && i < size; i++) { @SuppressWarnings("unchecked") final E element = (E) elementData[i]; if (filter.test(element)) { removeSet.set(i); removeCount++; } } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } // shift surviving elements left over the spaces left by removed elements final boolean anyToRemove = removeCount > 0; if (anyToRemove) { final int newSize = size - removeCount; for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) { i = removeSet.nextClearBit(i); elementData[j] = elementData[i]; } for (int k=newSize; k < size; k++) { elementData[k] = null; // Let gc do its work } this.size = newSize; if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } modCount++; } return anyToRemove; }