Android开发:细致分析List.remove()使用时出现错误或失效的原因以及最佳的利用场景

我们在开发的过程中很多时候会使用到List进行相关的数据存储工作,主要是因为它的简单方便,使用起来快捷舒服。虽然List中的相关方法使用起来很简单,但是我们可不能掉以轻心,它们实现的背后可是不简单。今天就主要给大家讲解其中remove()方法,在List中它可是大有文章,脾气也是古灵精怪,如果使用不当,便会带来想象不到的后果!

我们平常使用remove()来进行移除List中相应的数据,通常使用remove(Int)传入想要移除位置的下标,就会把相应的数据删除。你可能会碰到这样的情况,在使用remove(Int) 的时候,明明传入了坐标值就是移除不掉,或者干脆曝出错误异常!

错误异常还是挺容易发现的,但是移除不掉就非常坑了,因为这个非常隐蔽,你往往需要花费上几个小时,心焦力悴之后,才不经意间发现原来是remove()失效!但是去解决remove()失效问题时你又会头疼不已,因为你传入的都是正确的下标,你找不到其他的错误!

这里,我将会带领大家一同去探究分析,彻底搞懂失误区域,明白它最佳的使用场景。

这里我们通过源码去讲解,我们知道remove()方法有两个,只不过参数不同,一个是整型,一个是Object,这里我们先分析remove(Int) 方法,它的源码如下:

public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++;
        E oldValue = (E) elementData[index];

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

我们首先可以看到它的返回类型是一个泛型,这里使用泛型很显然易见,毕竟List中储存的数据类型不是固定不变的,可以储存有好多种类型包括自定义。我们接着看方法体,整型参数index是传入的下标值,这里首先进行的第一步就是判断下标是否越界,如果下标值大于List的大小,就会报数组下标越界的错误。可以明确的是,在使用remove()方法报错就是这个下标越界啦。

你可能会有疑问,这里为什么要判断下标是否越界?我们在使用的时候肯定传入的是范围之内的参数!这里先不说明,我们接着往下看。下面对变量modCount进行了加一的操作,这里的modCount变量指的是该List被结构修改的次数,你对List进行移除添加操作,都会改变它的结构,这里的变动次数就被记录下来,这样做的目的是记录下结构修改后可能出现的失误。

往下看,这里定义了一个泛型oldValue,从字面意思上就可以看出,这个oldValue是为了记录下被删除的数据,我们可以看到, 它通过elementData[index]赋值。其中elementData是List的内部数组,List的储存的操作其实都是基于数组的形式。最终oldValue作为返回值被返回了过去。

下面的代码应该可以说就是整个remove()方法的核心了。我们首先看到,代码中定义了一个整形变量numMoved,通过List的大小减去下标再减去一赋值。numMoved变量指的就是需要移动的数据个数。下面判断numMoved变量的大小,如果大于0的话,那么就进行数组复制。System.arraycopy()方法可以实现将一个数组的指定个数元素复制到另一个数组中,在这里System.arraycopy(elementData, index+1, elementData, index, numMoved);的意思就是:将elementData数组里从索引为index+1的元素开始, 复制到数组elementData里的索引为index的位置, 复制的元素个数为numMoved个。

举个例子说明一下:例如,我们在List中存放了五条数据,分别是1,2,3,4,5。现在我们调用remove(3)来移除掉List中4这个数据。首先先计算出需要复制的数据个数,也就是numMoved变量,numMoved=5-3-1=1。需要移动一位数据,然后就开始执行数组赋值函数,从elementData数组中索引为index+1的元素开始,这里就是从数据5开始,复制到数组elementData里的索引为index的位置,这里复制到的位置就是3,也就是数据4,复制的结果就是把数据5复制到数据4的位置上,数组中数据排列变成:1,2,3,5,5。

还别急,我们还剩最后一句代码:

elementData[--size] = null; // clear to let GC do its work

首先进行size减一的操作,size的值变成了4,然后把 elementData数组下标为4 的数据设置为null,即释放掉下标为4的内存。这里的操作就把最后的那个数据5给清除掉了,数组中数据排列变成:1,2,3,5。同时数组的大小变为了4。

这样我们走完了remove的整个流程,我们会发现,如果删除掉List中间某一个数据,则后面相应的数据的下标都会发生变化,下标统统减一!所以这里就是出现问题的关键所在!

这里给出两个错误使用remove(Int)方法场景:

错误场景一:

        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);

        list.remove(3);
        list.remove(4);
        list.remove(5);

上面的伪代码中,我定义了一个list,在他里面我添加了6个数据,分别是1,2,3,4,5,6,然后我分别调用了remove(3),remove(4),remove(5),以期望删除掉数据4,5,6。那么开始运行这份代码,会出现什么样的问题呢?

答案是会报数组下标越界的错误。为什么?下标3,4,5都在list数组的范围之内啊!

这里我们来分析一下:首先数组中数据的排列顺序为:1,2,3,4,5,6,接着我们调用了remove(3)方法,移除掉下标3的数据4,根据上面我们分析的移除规则,移除后,数组的结构变为:1,2,3,5,6,数组的大小由之前的6变成了5!下面接着删除下表为4的数据,我们期望删除的数据是5,但是由于上一次的移除操作已经改变的数组结构,所以这里移除的是6,因为数据6的下标此时为4。下面再接着删除下标为5的数据,此时,数组的结构已经变为:1,2,3,5.数组大小由之前的5变为了4,这时你却要删除下标为5的数据,根据源码中的代码:

if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

5大于4,就会报出数组下标越界错误!这下明白为什么会报出错误了把!

错误场景二:

 如果说错误场景一起码还有一个错误信息,那么错误场景二是一点错误信息都没有,可谓是杀人不见血,任你找Bug找的头破血流也发现不了!代码如下:

        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);

        list.remove(1);
        list.remove(2);
        list.remove(3);

这里我还是使用上面的伪代码,只不过修改了移除下标。根据上面的移除规则我们可以分析出:

执行过 list.remove(1)后数组中数据的排列顺序为:1,3,4,5,6,数组的大小为5;

执行过list.remove(2)后数组中数据的排列顺序为:1,3,5,6,数组的大小为4;

执行过list.remove(3)后数组中数据的排列顺序为:1,3,5,数组的大小为3。执行完毕。

我们会发现原本我们期望移除掉的是2,3,4,结果移除的是2,4,6!

这里不会报错,因为数组并没有越界,所以这样的Bug是隐藏最深的,一般不会被轻易发现。

总结以上的两种错误场景,我们发现导致错误的原因都是我们连续调用了多次remove()方法,没有考虑到当执行过一次remove()方法后,数组的结构已经发生了变化!所以remove(int)的正确使用场景只能是单一的删除list中某一个数据,而不能用于连续删除多个数据。

 但是在我们平时的开发过程,总会遇到需要删除List中多条数据的情况,还是上面的伪代码,这里我们就需要清除掉list中2,3,4三个数据,这里我们该怎么办?

还是有办法的,谷歌毕竟不会难为我们开发者,还记得开始时提到的另外一个remove()方法吗?这个remove()方法传入的参数为Object类型,我们去看一下这个remove(Object)方法的源码:

public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

首先我们可以看到,返回类型是不相同的,这个remove返回类型是布尔型,这个布尔值代表的是是否移除成功,移除失败返回false,移除成功返回true。我们接下来看具体方法的实现:

首先判断传入的参数是否为null。如果参数为空,那么就会进行遍历数组,如果发现其中的数据有空值,那么就会执行fastRemove方法,传入的参数为空值的下标。我们看一下fastRemove()方法:

private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

是不是很眼熟?没错这里还是进行一个数组移除的操作。

返回我们的remove()方法中,如果传入的参数不为空,首先还是进行数组遍历,如果在数组中找到了传入的参数值,那么就会执行数组移除操作,传入相应的下标值,返回true。

我们对比两个remove()方法,发现remove(Object)最大的不同就是,remove(Object)是自主寻找需要移除的数据的下标,会首先进行遍历寻找需要移除的数据,找到之后传入下标进行数组移除,如果找不到需要移除的数据,那么就会返回false。

remove(Object)解决了只能单一删除一次操作的场景,可以连续进行删除操作。但是性能方面比remove(Int) 方法稍逊一筹,毕竟remove(Object)方法中每一次移除之前都要进行for循环遍历查找,而remove(Int) 方法直接就行移除操作。

所以当只需要进行一次移除操作的时候,使用remove(Int) 方法是最佳的选择;当需要进行多个移除操作时,使用remove(Object)是最佳的选择!

本文到此结束,需要引用的地方请标明出处,谢谢!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值