Utils.safeForeach 遍历回调时出现NullPointerException: Attempt to invoke interface method 或数组越界

近期在工作时发现了一个有趣的bug,切换语言时,概率性出现crash ,查看相关log,发现是调用到

  @VisibleForTesting
    protected void fireConfigChanged(ZenModeConfig config) {
        Utils.safeForeach(mCallbacks, c -> c.onConfigChanged(config));
   }

使用lamda表达式去迭代回调数组时,发生了NullPointerException,mCallback是一个回调对象的ArrayList,就查看了safeForeach方法的实现,发现一个有趣的地方

    /**
     * Allows lambda iteration over a list. It is done in reverse order so it is safe
     * to add or remove items during the iteration.
     */
    public static <T> void safeForeach(List<T> list, Consumer<T> c) {
        for (int i = list.size() - 1; i >= 0; i--) {
            c.accept(list.get(i));
        }
    }

谷歌工程师通过比较反向遍历ArrayList的方式,去避免迭代期删减条目,导致数组越界的问题。但是,我们都知道,ArrayList是线程不安全的,这一点看源码就可以知道

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    // Android-note: Also accessed from java.util.Collections
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;    

 

ArrayList的实现主要就是用了一个Object的数组,用来保存所有的元素,以及一个size变量用来保存当前数组中已经添加了多少元素。接下来在看一下add是如何实现的:

        public void add(int index, E e) {
            if (index < 0 || index > this.size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
            parent.add(parentOffset + index, e);
            this.modCount = parent.modCount;
            this.size++;
        }

由此看到get元素的时候,会比较index和当前的size,而这里的size会因为其他线程的add、remove发生改变,故出现文章最开始的IndexOutOfBoundsException。

那么回过头来看safeForeach,尽管使用倒序,但它在迭代过程中,仍然可能会出现被其他线程修改的情况。无法保证size是否发生改变,因此无法保证线程安全,该bug该如何解决呢:

其实直接使用我们常用的foreach 即可,同时,使用

CopyOnWriteArrayList来替代ArrayList,因为它是线程安全的,

而由于foreach对于数组,是普通的for循环,但对于集合,却使用了Iterater迭代器。因此,把代码改写为:

    @VisibleForTesting
    protected void fireConfigChanged(ZenModeConfig config) {
        for(Callback cb : mCallbacks){
            cb.onConfigChanged(config));
        }
    }

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值