【源码】ArrayList 深度挖掘

562 阅读3分钟

前言

  • 对于java常用,相对简单的类做一个深度解析,挖掘其实现亮点
  • 阅读源码步骤

数据结构

  • 类都是在操作数据,从数据结构入手
    transient Object[] elementData;   //存放数据
    private int size;                 //记录已存放的元素个数

实现原理&使用细节

初始化

    //static 关键字标识,所有对象公用,省内存
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    //初始化一个空的数组,在第一次add()时才初始化 elementData
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    //预知大数据时指定初始化容量,减少扩容次数
    public ArrayList(int initialCapacity) {
      ...
      this.elementData = new Object[initialCapacity];
    }
  • 延迟初始化的思想,大部分有数组的数据结构,在第一次添加操作时才初始化,分配内存
    例:HashMap StringBuffer ThreadLocal
  • 避免频繁扩容影响性能,知道预期数量则可以指定
    //构造函数如下
    public ArrayList(int initialCapacity) //LinkedList不是数组就没有
    public HashMap(int initialCapacity) 
    public StringBuffer(int capacity)

扩容数据

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //1.5倍扩容 10->15->22->33
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        ...
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    //最终调用 System 类底层方法,浅拷贝数据
    public static native void arraycopy(Object src,  int  srcPos,Object dest, int destPos,int length);
  • Arrays.copyOf 性能比for循环逐个赋值高
  • 最终调用 System 类 arraycopy() C实现方法,浅拷贝数据

批量删除

    private boolean batchRemove(Collection<?> c, boolean complement) {
        ...
        try {
            for (; r < size; r++)
               //内层循环性能关键点,基于Hash查找的HashSet contains() 方法比 List快
                if (c.contains(elementData[r]) == complement)
                    //核心操作数据,jdk美妙之处
                    elementData[w++] = elementData[r];
        } finally {
        ...
        }
        return modified;
    }
    //1.8 lambda 删除方式
    public boolean removeIf(Predicate<? super E> filter) {
    final BitSet removeSet = new BitSet(size);
            for (int i=0; i < size; i++) 
              if (filter.test(element)) {
                  removeSet.set(i);
              }
    }
  • 采用 BitSet 记录需删除索引位,因为采用的long(64 bit位)作为数据结构,最优情况省64倍内存
  • 类似于 redis 的 bitmap,和 Guava 包的 BloomFilter 也有关联

序列化

    transient Object[] elementData;
  • 因为数据可能没有全部填充数组,使用transient关键字,防止自动序列化属性
    private void writeObject(java.io.ObjectOutputStream s){
        ...
        //结束位为size,省 内存|磁盘空间|带宽
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
        ...
    }
    private void readObject(java.io.ObjectInputStream s){
            for (int i=0; i<size; i++) {
                //数据还原
                elementData[i] = s.readObject();
            }
        }
    }

并发异常

ConcurrentModificationException

 * @see     Collection
 * @see     Iterator
 * @see     Spliterator
 * @see     ListIterator
 * @see     Vector
 * @see     LinkedList
 * @see     HashSet
 * @see     Hashtable
 * @see     TreeMap
 * @see     AbstractList
 * it is not generally permissible for one thread to modify a Collection
 * while another thread is iterating over it.
 * In general, the results of the iteration are undefined under these circumstances
  • 我发现java Doc,有必要翻译着看一眼
  • 一个线程在修改Collection,而另一个线程正在迭代它。通常,在这些情况下,迭代的结果是不确定的
  • fail-fast 快速失败思想,如果在使用的数据可能不准确,直接抛出异常
    //涉及到修改 elementData 的方法执行 modCount++;
    public boolean add(E e)
    public E remove(int index) 
    public void replaceAll(UnaryOperator<E> operator) 
    public void sort(Comparator<? super E> c)
    //涉及遍历的方法校验 modCount 值
    public void forEach(Consumer<? super E> action) {
        //遍历前记录修改次数
        final int expectedModCount = modCount;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            ...
        }
        //发现和预期值不同则抛出异常
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    public boolean removeIf(Predicate<? super E> filter)
    Itr.next()
  • 在存在并发的情况下,不需要冒险,采用并发容器

并发 List

Collections#synchronizedList() & Vector

  • 优:SynchronizedList有兼容功能,可以将List的子类转成线程安全的类
    劣:SynchronizedList,进行遍历时要手动进行同步处理

CopyOnWriteArrayList

  • 适合多读少写的场景,读弱一致性,读用volatile无锁方式,写用 ReentrantLock加锁 copy 新的数组

排序 TimSort

    public void sort(Comparator<? super E> c) {
        ...
        //最终调用 TimSort.sort() ,组合 + 插入 排序
        Arrays.sort((E[]) elementData, 0, size, c);
    }
    
    //从小到大排列
    ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
    list.sort(Integer::compareTo);

总结

  • 技术本质是实现动态数组的 CRUD
  • 在实际撸码中,注意初始化指定容量大小提升性能,是线程不安全即可