• Java-ArrayList源码分析


    Java-ArrayList源码分析

    参考

    • JDK 源码

    Overview

    ArrayList是我们非常常用的一个集合,那么ArrayList是如何实现呢?

    从一个小Demo开始分析

    ArrayList<String> arrayList = new ArrayList<>();
    arrayList.add("Hello");
    if (arrayList.contains("Hello")) {
        System.out.println("[Hello] is in ArrayList!");
    }
    arrayList.remove("Hello");
    

    构造函数

    public ArrayList() {
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
        //存储数据的集合
        transient Object[] elementData; // non-private to simplify nested class access
        //....
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    

    从构造函数可以看出,现在我们的集合是一个空集合,真正存储数据的数组也是一个长度为0的数组。

    Add方法

    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
    
    private void add(E e, Object[] elementData, int s) {
        //存储数据的数组已经全部被使用
        if (s == elementData.length)
            //扩张数组
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
    

    一个参数的add方法调用了private的add方法,在该方法中真正完成了向集合中添加元素。在添加元素的时候,如果size==存储数据的数组的长度,那么就表明数组已经存满了,这时候就需要将原本的数组进行扩张,如何扩张数组是通过 grow 方法来实现的。

    Grow方法

    private Object[] grow() {
        return grow(size + 1);
    }
    
    private Object[] grow(int minCapacity) {
        //复制当前数组到一个新的数组中(新的数组长度已经扩张过)
        return elementData = Arrays.copyOf(elementData,
                                           newCapacity(minCapacity));
    }
    
    //计算新的扩张长度
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //计算新的容量
    
        //新容量= 数组的长度+数组的长度向右位移1(例如:10>>1 = 5, 11>>1 = 5)
        //这种新容量的计算方式表明:当集合的长度越大时,集合每一次的扩张的幅度就会越来越大
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //当新容量小于最小容量时,以最小容量为基准
        if (newCapacity - minCapacity <= 0) {
            //当数组中没有元素的时候,会执行这个分支
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                //返回最小容量和默认容量中较大的一个
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);//极端情况,可以不考虑(几乎不会走到这个分支)
    }
    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE)
            ? Integer.MAX_VALUE
            : MAX_ARRAY_SIZE;
    }
    

    Grow 方法是实现ArrayList长度可变的核心,其实现思想是

    当数组存放满了的时候,就扩张数组(当集合中的元素的数量越多的时候,扩张的幅度就越大。)
    而扩张数组是通过,建立新的数组(长度等于扩张后的长度),然后将旧的数组中的元素填充到新数组中,这种方式来实现的。

    Contains方法

    Contains也是ArrayList集合非常常用的一个方法,用来判断集合中是否包含指定的元素。

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
    
    public int indexOf(Object o) {
        return indexOfRange(o, 0, size);
    }
    
    int indexOfRange(Object o, int start, int end) {
        Object[] es = elementData;
        if (o == null) {
            for (int i = start; i < end; i++) {
                if (es[i] == null) {
                    return i;
                }
            }
        } else {
            for (int i = start; i < end; i++) {
                if (o.equals(es[i])) {
                    return i;
                }
            }
        }
        return -1;
    }
    

    contains方法最终调用了调用了indexOfRange方法,indexOfRange做的工作是,在集合指定的范围内判断是否包含指定的元素,如果包含就返回下标,否则就返回-1.

    remove方法

    public boolean remove(Object o) {
        final Object[] es = elementData;
        final int size = this.size;
        int i = 0;
        //搜寻指定元素
        found: {
            if (o == null) {
                for (; i < size; i++)
                    if (es[i] == null)
                        break found;
            } else {
                for (; i < size; i++)
                    if (o.equals(es[i]))
                        break found;
            }
            return false;
        }
        //移除元素
        fastRemove(es, i);
        return true;
    }
    
    private void fastRemove(Object[] es, int i) {
        modCount++;
        final int newSize;
        //如果不是在数组的末尾移除元素
        if ((newSize = size - 1) > i)
            //将i之后所有元素向前移动一个下标
            System.arraycopy(es, i + 1, es, i, newSize - i);
        es[size = newSize] = null;
    }
    

    remove方法的逻辑很清晰:

    1. 首先找到需要移除的元素的下标
    2. 判断下标是否是在集合的中间
    3. 如果下标在集合的中间,那么表明需要将该下标后面的元素全部向前移动一位下标。
    4. 将数组的最后一个元素设置为null

    总结

    ArrayList通过数组实现了可变集合,但是我们从源码中可以看出来,如果要增加删除元素的话,是非常消耗资源和时间的(因为在频繁的操作数组),但是访问的时候是特别的快的(直接通过下标访问即可)。

    可以得出结论: ArrayList不适合频繁的增加删除,但是适合查询。

  • 相关阅读:
    Autodesk Infrastructure Map Server(AIMS)/MapGuide API 培训材料第6章
    安装Vault Professional Server的一些问题
    Autodesk Infrastructure Map Server(AIMS)/MapGuide API 培训材料第2章
    C++的构造函数和析构函数
    一些常用的字符串hash函数
    类的operator new与operator delete的重载
    计算字符串的相似度(编辑距离)
    C++的重载(overload)与重写(override)
    穷举法解24点游戏
    C语言字符串库函数的实现
  • 原文地址:https://www.cnblogs.com/slyfox/p/10836885.html
Copyright © 2020-2023  润新知