• java集合类型源码解析之ArrayList


    前言

    作为一个老码农,不仅要谈架构、谈并发,也不能忘记最基础的语言和数据结构,因此特开辟这个系列的文章,争取每个月写1~2篇关于java基础知识的文章,以温故而知新。

    如无特别之处,这个系列文章所使用的java版本都是1.8.0。

    第一篇当然谈ArrayList了,因为这是java最常用的list集合类型,它内部使用数组作为存储空间,在增加元素时能够自动增长。总体来说,ArrayList的实现比较简单,这里不罗列它的全部代码,只看一些有意思的地方。

    成员变量

        private static final int DEFAULT_CAPACITY = 10;
        private static final Object[] EMPTY_ELEMENTDATA = {};
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
        
        transient Object[] elementData; 
        private int size;
    
    
    • DEFAULT_CAPACITY 常量,初始话的时候如果没有指定capacity,使用这个值;
    • EMPTY_ELEMENTDATA 空数组,所有空的ArrayList的elementData可以共享此值,避免使用null;
    • DEFAULTCAPACITY_EMPTY_ELEMENTDATA 也是elementData的值,代表初始化容量为默认值(非0)的存储空间,但是暂时还没有实际元素,等到需要增长存储空间时,与EMPTY_ELEMENTDATA的策略不一样;
    • elementData 提供存储空间的数组;
    • size list长度,list的元素存储在elementData[0~size)。

    PS:说实话,DEFAULTCAPACITY_EMPTY_ELEMENTDATA的使用到底带来了什么好处,我没想明白。

    初始化

    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;
    }
    
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
    
    • 第一种,指定初始容量;
    • 第二种,使用默认容量,此时DEFAULTCAPACITY_EMPTY_ELEMENTDATA派上用场;
    • 第三种,使用另一个集合来初始化,使用了集合的toArray方法,值得注意的是,集合的toArray返回的不一定是Object[]类型。

    空间增长

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
    
        ensureExplicitCapacity(minCapacity);
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
        
    

    这是三个私有方法

    • ensureCapacityInternal,在增长容量前加了一层检查,如果elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,确保容量不小于默认容量;
    • ensureExplicitCapacity,检查数组的容量是否确实需要扩充;
    • grow,执行数组扩容,先尝试扩容一半,看是否满足需求;也就是说空间的扩容,不是严格按照输入参数来的。

    这里有一个疑问,ensureExplicitCapacity方法的那句modCount++为什么会在if语句外执行.

    增删改查

    基本操作方法有很多,这里只列几个有代表性的。

    1、查找:

    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }
    

    ArrayList查找元素的过程就是遍历数组,而且null也是可以查找的。

    2、插入:

    public void add(int index, E element) {
        rangeCheckForAdd(index);
    
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
    

    插入前做了范围检查,并确保容量,最后把插入位置之后的元素全部再往后挪一个位置;移动内存使用了System.arraycopy,这是一个native方法

    值得注意的是,add方法并没有直接调用modCount++,因为ensureCapacityInternal里面调用了,所以我猜测modCount++放在ensureCapacityInternal里面纯碎就是为了更方便,即使有些没有修改数组的场景也会导致modCount被修改。

    3、删除:

    public E remove(int index) {
        rangeCheck(index);
    
        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;
    }
    

    把删除位置之后的元素全部往前挪一个位置,注意的是,最后空出来的那个内存位置要置成null,否则会内存泄露

    4、批量删除

    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }
    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }
    
    private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }
    

    removeAll删除集合参数里的所有元素,反过来retainAll删除不在集合参数里面的所有元素,二者都通过batchRemove来实现。

    batchRemove使用两个游标w,r来执行连续的元素移动,w代表写入位置,r代表读取位置,读取一个元素后判断是否应该删除,如果是那么继续读下一个,否则写入w位置。 这样,到了最后w位置就是list的末尾,w~r之间的位置需要置成null。

    值得注意的是,最后的收尾逻辑放到finally里面,保证了一定程度的异常安全性。如果发生异常,那么剩余未扫描的元素(r位置之后的元素),要拷贝到w之后;这样一来,batchRemove可能执行了一半而失败,但ArrayList的状态并没有乱掉。

    迭代器

    ArrayList支持两种迭代器,Iterator和ListIterator,后者是前者的增强版本,可以向前移动、插入元素、返回元素索引。

    这里就解读下Iterator实现,ListIterator是差不多的。

    1、Iterator成员变量

    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
    
        public boolean hasNext() {
            return cursor != size;
        }
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    

    Iterator实现为ArrayList的非静态内部内,因此可以直接访问ArrayList的成员字段,它只需要记住当前位置(cursor)即可。lastRet指向上一次next操作返回的元素位置,这个是非常有必要的,因为在迭代器上,如果要对元素做一些操作,都是针对这个位置的元素。

    expectedModCount是ArrayListmodCount的快照,防止在迭代过程中,意外对list执行修改。

    2、next操作

    @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];
    }
    

    很简单,几乎没啥可解释的。

    3、remove操作

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

    remove操作会更新expectedModCount,这是一个允许的、意料之中的修改操作。

    4、modCount
    modCount定义在基类AbstractList当中,使用来追踪list修改的,自身的值没有实际意义。

    modCount主要最用,就是在发生意外的修改时,快速失败,而不是等到出现难以追踪的数据状态错误时才失败。思路是,如果在执行一个操作的过程中,如果不期望ArrayList被其他操作所修改,那么可以在开始时记录一下modCount快照,在执行操作的过程中,通过比较ArrayList的modCount和这个快照来判定ArrayList是否被修改了,然后抛出ConcurrentModificationException。

    ConcurrentModificationException看起来像是多线程相关的,但实际上这里和多线程一点关系都没有,ArrayList不是线程安全的,modCount的设计也没有针对多线程的意思。

    SubList

    ArrayList的subList可以直接通过游标锁定原ArrayList的一个区段,从而避免了copy。
    由于SubList要实现List的全部接口,全部源码比较多,这里讲解一下get方法的实现,其他的都是类似的。

    private class SubList extends AbstractList<E> implements RandomAccess {
        private final AbstractList<E> parent;
        private final int parentOffset;
        private final int offset;
        int size;
    
        SubList(AbstractList<E> parent,
                int offset, int fromIndex, int toIndex) {
            this.parent = parent;
            this.parentOffset = fromIndex;
            this.offset = offset + fromIndex;
            this.size = toIndex - fromIndex;
            this.modCount = ArrayList.this.modCount;
        }
        
        public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
        }
        public E set(int index, E e) {
            rangeCheck(index);
            checkForComodification();
            E oldValue = ArrayList.this.elementData(offset + index);
            ArrayList.this.elementData[offset + index] = e;
            return oldValue;
        }
    }
    

    1、首先,构造函数可以看出来,SubList实际就是通过索引来操作原ArrayList的数据,只不过加一个偏移(offset)和长度限制(size)。

    2、get方法检查modCount,说明在SubList的生命周期内,期望parent ArrayList不会被修改。这也说明SubList只适合做临时变量,不适合长期存活,除非原ArrayList是不变的。

    AbstractList

    AbstractList是ArrayList的基类,它也实现了Iterator,ListIterator,SubList的一个版本。不过AbstractList并不清楚存储的实现细节,因此只能基于list的公共接口来实现,因此效率肯定不佳。

    比如AbstractList的Iterator的next方法是这样实现的:

    public E next() {
        checkForComodification();
        try {
            int i = cursor;
            E next = get(i);
            lastRet = i;
            cursor = i + 1;
            return next;
        } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
        }
    }
    

    获取元素用的是list.get方法: E next = get(i)

  • 相关阅读:
    data-icon=""图片还可以是自定义的?
    orientationchange不管用啊
    menu({postion:{my:"left top"},at:"right bottom"})里的my与at会冲突吗
    关于position的疑惑
    Linux
    C++
    MATLAB
    SCE
    SFLA混合蛙跳算法
    memetic algorithm
  • 原文地址:https://www.cnblogs.com/longhuihu/p/11123095.html
Copyright © 2020-2023  润新知