Java八股文系列二:集合之ArrayList

本文详细分析了ArrayList的源码,包括成员变量、构造方法、add、get、remove操作,以及ArrayList的扩容机制。同时,文章讨论了ArrayList在并发修改时可能出现的异常,并给出了删除元素的正确做法。此外,还介绍了ArrayList线程安全问题,推荐使用CopyOnWriteArrayList作为线程安全的选择。最后,文章总结了ArrayList的特点和使用注意事项。
摘要由CSDN通过智能技术生成

image

一、ArrayList源码分析

1.1 成员变量

    //默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
    
    //对象数组,存储具体的元素
    transient Object[] elementData;
    
    //元素的个数
    private int size;
    
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    private static final Object[] EMPTY_ELEMENTDATA = {};

1.2 构造方法

    /**
     * 带初始化容量的构造方法
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * 无参构造方法
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 使用指定 Collection 来构造 ArrayList
     */
    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

1.3 add操作

    public boolean add(E e) {
        //确保容量是否够用,不够用的话就扩容
        ensureCapacityInternal(size + 1); 
        //新元素进入数组
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));//calculateCapacity(elementData, minCapacity)等于10或size+1
    }
    
    // 计算容量,如果数组为空则取10和size+1中的较大者,否则就是size+1
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 如果需要的容量大于当前数组中元素的个数就要扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //新容量=旧容量+旧容量的一半,也就是原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果新容量溢出了,那么新容量就是传进来的参数,也就是10或者是size+1
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果新容量比规定的最大的数都大,那么就是最大的数
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //将旧数组的元素copy到新的数组中
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

add流程:

  1. 先计算所需要的容量,如果不够就进入2,否则进入3。
  2. 扩容,扩容后的容量是之前的1.5倍,将旧数组的元素copy到新的数组,进入3。
  3. 将新的元素加入到数组的末尾。
  4. 返回true。

1.4 get操作

    public E get(int index) {
        //检查index是否超过size,超过则抛出异常
        rangeCheck(index);

        return elementData(index);
    }

1.5 remove操作

    //根据数组下标删除元素
    public E remove(int index) {
        //检查index是否超过size,超过则抛出异常
        rangeCheck(index);

        //修改的次数加1
        modCount++;
        E oldValue = 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;
    }
    
    //根据具体元素删除元素
    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;
    }
    
    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
    }

根据下标删除元素的流程:

  1. 先判断下标是否小于size。
  2. 如果要删除的元素不是最后一个元素,调用System.arraycopy()方法,将要删除元素后面所有的元素复制到前一位上。
  3. 将最后一个元素置为空。

根据元素删除元素的流程:

  1. 如果要删除的元素为null,遍历数组,用等于号来判断元素是否相等,如果相等则按照上面的方式删除。
  2. 如果要删除的元素不为null,遍历数组,用equals方法来判断元素是否相等,如果相等则按照上面的方式删除。

1.6 缩容操作

    //如果size小于length则可以进行缩容操作
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

二、ArrayList常见面试问题

2.1 ArrayList的默认初始长度是多少?

ArrayList的默认初始长度是10。

2.2 ArrayList如何扩容?

一般扩容后的容量是原来的1.5倍,如果超过了整型的最大值,那么就是Integer.MAX_VALUE。容量确定后底层调用System.arraycopy()方法将旧的数组复制到新数组中。

2.3 遍历删除问题

删除ArrayList中所有"b"元素:

    public static void main(String[] args) {
        //ArrayList 中有{"a","b","b","c","d","b","e","e","e",}
        //删除所有"b"
        ArrayList<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("b");
        list.add("e");
        list.add("e");
        list.add("e");
        System.out.println("删除操作之前:"+list);
        for(int i = 0; i < list.size(); i++) {
            if ("b".equals(list.get(i))){
                list.remove(i);
            }
        }
        System.out.println("删除操作之后:"+list);
    }
public static void main(String[] args) {
        //ArrayList 中有{"a","b","b","c","d","b","e","e","e",}
        //删除所有"b"
        ArrayList<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("b");
        list.add("e");
        list.add("e");
        list.add("e");
        System.out.println("删除操作之前:"+list);
        for (String str : list) {
            if (str.equals("b")) {
                list.remove(str);
            }
        }
        System.out.println("删除操作之后:"+list);
    }

结果:

删除操作之前:[a, b, b, c, d, b, e, e, e]
删除操作之后:[a, b, c, d, e, e, e]
删除操作之前:[a, b, b, c, d, b, e, e, e]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at com.example.demo.thread.ArrayListTest.main(ArrayListTest.java:22)

现象:普通for循环元素b没有删除干净,foreach循环报错。

原因:remove()方法会使元素的位置向前挪动一位,如果存在相同且相邻的元素,后面相同的元素会代替它前面一位已经删除的元素,此时继续遍历的话就会判断下一位元素。

解决方法

  1. 普通循环倒叙遍历删除。因为后面元素的位移不影响前面的元素。
    for (int i = list.size() - 1; i > 0; i--) {
        if ("b".equals(list.get(i))) {
            list.remove(i);
        }
    }
  1. 用迭代器解决。
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        String next = iterator.next();
        if (next.equals("b")) {
            iterator.remove();
        }
    }
  1. removeIf()方法解决。
    list.removeIf(a -> a.equals("b"));

2.4 ArrayList线程安全吗?有线程安全的list吗?

ArrayList线程不安全,CopyOnWriteArrayList是线程安全的list。

三:补充

ArrayList里的迭代器

思考:为什么在2.3中使用foreach会抛出异常呢?

源码:

    public Iterator<E> iterator() {
        return new Itr();
    }

    
    private class Itr implements Iterator<E> {
        
        int cursor;       // 下一个元素的下标
        int lastRet = -1; // 上一个元素的下标,如果没有的话是-1
        int expectedModCount = modCount; // 对 ArrayList 修改次数的期望值

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        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];
        }

        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();
            }
        }
        
        //省略

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
  • hasNext

    如果下一个元素的下标达到了数组的size,说明到最后了。

  • next

    先判断expectedModCount和modCount是否相等,注意expectedModCount是Itr类独有的成员变量而
    modCount是整个ArrayList的成员变量,如果不相等就直接抛异常,也就是说通过迭代器的修改次数和通过ArrayList的修改次数不一致就会抛出异常;如果相等就继续判断cursor是否超出了size和length,超出则抛异常,没有超出则将cursor赋值给lastRet并取出相应的元素。

  • remove

    先判断lastRet是否小于0,然后再检查expectedModCount和modCount是否相等,如果相等则调用
    ArrayList的remove()方法删除下标为lastRet的元素,然后将lastRet赋值给cursor,lastRet重新设置为-1,并将modCount重新赋值给expectedModCount,因为调用ArrayList的remove()方法会修改modCount。

现在说一下2.3中使用foreach这种循环方法删除ArrayList中的元素会抛出ConcurrentModificationException这个异常的原因:

首先foreach这种语法糖底层是iterator,也就是说使用的迭代器进行遍历,但是在迭代器的遍历中使用的却是ArrayList的remove()方法,也就是说modCount更新了但是迭代器里的expectedModCount没有更新,所以在迭代器调用next()方法进行遍历时,首先调用的checkForComodification()这个方法判断expectedModCount和modCount是否相等,发现不相等就抛出了ConcurrentModificationException这个异常。解决的方法就是使用迭代器遍历,并且调用迭代器的remove()方法,这个方法会将modCount同步给expectedModCount。

四:总结

  • ArrayList是动态扩容的数组,按照元素的进入顺序,元素可重复,增删慢,查询快,初始长度是10,扩容后一般是原来的1.5倍,线程不安全。
  • 最好对ArrayList指定初始大小,这样可以减少频繁扩容带来的开销。
  • 最好使用迭代器来进行遍历删除的操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值