zoukankan      html  css  js  c++  java
  • Java集合001 --- ArrayList源码解析

    前言

    List是一种有序、可以重复的数据结构,底层实现是动态数组或者链表;常用的List结构有:ArrayList、LinkedList、CopyOnWriteList还有古老的Vector和Stack。

    先带着如下几个问题去学习ArrayList的源码:

    1、ArrayList是怎么扩容的?

    2、ArrayList的插入、删除、查询的时间复杂度?

    3、怎么求两个集合的交集、并集、差集?

    4、ArrayList怎么实现序列化和反序列化?

    5、集合的toArray方法有什么问题?

    ArrayList属性

    // 默认容量
    private static final int DEFAULT_CAPACITY = 10;
    
    // 空数组, 创建list时容量为0或者初始化集合为空时使用
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    // 空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    // 数组元素
    transient Object[] elementData; // non-private to simplify nested class access
    
    // 当前数组中有几个元素, 区别容量
    private int size;

    构造方法

    ArrayList提供了如下三个构造方法:

    1、指定初始容量

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

    注意一点:可以指定初始容量为0,此时数组元素为EMPTY_ELEMENTDATA

    2、无参构造器

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    初始容量为0,第一次添加元素时,指定容量,默认是10

    3、入参为集合类型构造器

    public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
    elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
    // replace with empty array.
    this.elementData = EMPTY_ELEMENTDATA;
    }
    }

    流程为:先将输入集合转换成数组,然后赋值;这里为什么要判断elementData是不是Object类呢、或者说什么时候elementData不是Object类?

    答案是在重写了ArrayList的toArray方法的类可能出现上述场景

    添加单个元素方法

    ArrayList提供了如下两个添加元素的方法:

    1、添加单个元素到数组的最后(时间复杂度为O(1))【返回boolean类型】

    public boolean add(E e) {
        // 先做容量检查, 容量不够, 扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++; // 序列化时使用
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return 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);
    }
    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }

    流程为:计算所需的最小容量,容量不够,做扩容处理,最后添加元素到末尾;这里重点说下扩容;

    扩容机制为:先扩容1.5倍,如果还是不够分配需要的容量;如果大于Integer最大数值-8,则按照超大数组流程处理,防止整数溢出

    2、添加单个元素到指定索引(涉及元素拷贝,时间复杂度为O(n))【无返回值】

    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++;
    }
    
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    流程:1、验证索引,超上界or低于下界,抛IndexOutOfBoundsException异常(注意和ArrayIndexOutOfBoundsException的区别,ArrayIndexOutOfBoundsException为数组越界异常,IndexOutOfBoundsException为索引越界异常)

               2、扩容处理

               3、做元素拷贝:将index开始到结尾的元素拷贝到index+1到结尾

               4、元素复制&&元素个数加一

    移除元素方法

    1、移除指定索引的元素(时间复杂度为O(n))【返回值为被删除的元素】

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

    2、移除指定元素(时间复杂度为O(n))【返回值为删除结果boolean类型】

    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 remove method that skips bounds checking and does not
     * return the value removed.
     */
    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
    }

    这里注意的是:按需重写list对象的equals方法,否则默认的equals方法比较的是两个对象的地址

    获取&修改元素方法

    1、获取指定索引的元素(时间复杂度为O(1))

    public E get(int index) {
        rangeCheck(index);
    
        return elementData(index);
    }

    2、修改指定索引的元素为指定值(时间复杂度为O(1))【返回值为被修改的元素】

    public E set(int index, E element) {
        rangeCheck(index);
    
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

    求两个集合的并集

    实际上就是在指定list中批量添加元素,提供了如下两个方法:

    1、在末尾批量添加元素(时间复杂度为O(n))【返回值为新添加元素个数是否为0(boolean)】

    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray(); // 转换为数组
        int numNew = a.length;
        ensureCapacityInternal(size + numNew); // 扩容处理
        System.arraycopy(a, 0, elementData, size, numNew); // 元素拷贝到原数组末尾
        size += numNew;
        return numNew != 0;
    }

    2、在指定索引批量添加元素(时间复杂度为O(n))【返回值为新添加元素个数是否为0(boolean)】

    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index); // 上界&下界校验
    
        Object[] a = c.toArray(); // 转换为数组
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // 扩容处理
    
        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved); // 先将原数组从index开始, 预留位置
    
        System.arraycopy(a, 0, elementData, index, numNew); // 将元素批量插入到index位置到index+numNew
        size += numNew;
        return numNew != 0;
    }

    求两个集合的交集(返回值为boolean类型, 表示原数组是否有改变)

    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 {
            // 理论上是r一定等于size的, 除非contains抛异常
            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;
    }

    求两个集合的差集(返回值为boolean类型, 表示原数组是否有改变)

    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }

    ArrayList序列化

    从前面的属性中可以看到ArrayList的elementData由transient修饰的,是不参与序列化的,那么ArrayList怎么序列化呢?答案是ArrayList的writeObject方法和readObject方法

    ArrayList通过这两个方法自己控制序列化

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // 记录list修改次数, 防止序列化期间list变化
        int expectedModCount = modCount;
        s.defaultWriteObject(); // 写入非静态 & 非transient属性
    
        s.writeInt(size); // 写入数据个数
    
        // 写入数组所有元素
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
    
        // 序列化期间list变化, 抛出异常
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA; // 数组初始化
        // 去取非静态 & 非transient属性 & size
        s.defaultReadObject();
    
        // 读取数据个数, 没有卵用, 只是为了按照序列化步骤反序列化
        s.readInt(); // ignored
    
        if (size > 0) {
            // 计算容量
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size); // 扩容
    
            Object[] a = elementData;
            // 依次读取数据
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

    为什么要这样序列化呢?我的理解,ArrayList存在容量远远大于size的情况(比如创建list时指定超大初始容量),按需序列化会大大减小内存占用

    前面问题的答案

    1、ArrayList是怎么扩容的?

          先扩容1.5倍,如果容量还是不够,按照需要的容量扩容 --> 如果是超大容量(Integer最大值-8),检查是否可能整数溢出

    2、ArrayList的插入、删除、查询的时间复杂度?

          插入:插入尾部O(1),插入到指定索引O(n)

          删除:指定index和指定值都为O(n),涉及数组拷贝

          查询:O(1)

    3、怎么求两个集合的交集、并集、差集?

          交集:addAll(collection)、addAll(index, collection)

          并集:   remainAll()

          差集:   removeAll()

    4、ArrayList怎么实现序列化和反序列化?

          自己实现私有的writeObject、readObject方法,按照实际元素个数序列化

    5、集合的toArray方法有什么问题?

  • 相关阅读:
    关于LEA指令(单周期就可以做简单的算术计算)
    说说字符集和编码(非常形象,非常有意思)
    类依赖项的不透明性和透明性
    构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(6)-Unity 2.x依赖注入by运行时注入[附源码]
    大端和小端
    Google C++测试框架系列:入门
    随机森林中的数学基础之大数定律
    K-means Algorithm
    JavaScript插件——按钮
    分布式系统设计(8)
  • 原文地址:https://www.cnblogs.com/sniffs/p/12911706.html
Copyright © 2011-2022 走看看