java数据结构源码解读——ArrayList

说道最常用的数据结构,莫过于ArrayList和LinkedList了,有一点数据结构知识的人都知道一个是变长数组,另一个是链表形式的数组。但是它们究竟是在底层如何运行的呢?我们来看看。

 

首先我们观察这个类的定义:

public class ArrayList extends AbstractList
        implements List, RandomAccess, Cloneable, java.io.Serializable
{
 //...
}

啊我们知道了这是AbstractList的子类,然后继承了,List,RandomAccess,Conable,Serializable接口。

这样我们知道,ArrayList是可以随机访问,克隆(复制),序列化的。

public interface RandomAccess {
}

这是RandomAccess接口的定义。

public interface Cloneable {
}

这是Cloneable的。。。

public interface Serializable {
}

这是Serializable的。。。

有人说,这仨接口咋都是空的呢?其实这几个接口都是一个标识符而已,这个标识符表明了这个类是否有这样的能力去克隆,序列化,随机访问。

接下来看看List接口,我们用同样的方法打开,然后发现一堆备注,但是我们只看方法,整理结果如下:

int size();
boolean isEmpty();
boolean contains(Object o);
Iterator iterator();
Object[] toArray();
 T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection c);
boolean addAll(Collection c);
boolean addAll(int index, Collection c);
boolean removeAll(Collection c);
boolean retainAll(Collection c);
void clear();
boolean equals(Object o);
int hashCode();
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator listIterator();
ListIterator listIterator(int index);
List subList(int fromIndex, int toIndex);

default void replaceAll(UnaryOperator operator) {
        Objects.requireNonNull(operator);
        final ListIterator li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }

default void sort(Comparator c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

default Spliterator spliterator() {
        if (this instanceof RandomAccess) {
            return new AbstractList.RandomAccessSpliterator<>(this);
        } else {
            return Spliterators.spliterator(this, Spliterator.ORDERED);
        }
    }


static  List of() {
        return ImmutableCollections.emptyList();
    }

static  List of(E e1) {
        return new ImmutableCollections.List12<>(e1);
    }

static  List of(E e1, E e2) {
        return new ImmutableCollections.List12<>(e1, e2);
    }

 static  List of(E e1, E e2, E e3) {
        return new ImmutableCollections.ListN<>(e1, e2, e3);
    }

 static  List of(E e1, E e2, E e3, E e4) {
        return new ImmutableCollections.ListN<>(e1, e2, e3, e4);
    }
 static  List of(E e1, E e2, E e3, E e4, E e5) {
        return new ImmutableCollections.ListN<>(e1, e2, e3, e4, e5);
    }

static  List of(E e1, E e2, E e3, E e4, E e5, E e6) {
        return new ImmutableCollections.ListN<>(e1, e2, e3, e4, e5,
                                                e6);
    }
static  List of(E e1, E e2, E e3, E e4, E e5, E e6, E e7) {
        return new ImmutableCollections.ListN<>(e1, e2, e3, e4, e5,
                                                e6, e7);
    }
static  List of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8) {
        return new ImmutableCollections.ListN<>(e1, e2, e3, e4, e5,
                                                e6, e7, e8);
    }

static  List copyOf(Collection coll) {
        return ImmutableCollections.listCopy(coll);
    }

这List接口里面真的是,一坨方法啊,但是我们按照方法名猜测一波,为了方便,我们一组一组猜测,以备注形式给出:

int size();//获取大小
boolean isEmpty();//是否为空
boolean contains(Object o);//是否包含某个元素
Iterator iterator();//生成一个迭代器
Object[] toArray();//转化为Object数组
 T[] toArray(T[] a);//根据传入类型转化为对应类型数组

上面的显然是一些最基本的方法

boolean add(E e);//添加元素
boolean remove(Object o);//移除某个元素,如果移除就返回true
boolean containsAll(Collection c);//是否包含所有
boolean addAll(Collection c);//添加所有集合元素
boolean addAll(int index, Collection c);//从index开始添加所有集合元素
boolean removeAll(Collection c);//移除所有集合元素
boolean retainAll(Collection c);//???
void clear();//清除元素
boolean equals(Object o);//是否相等(指两个ArrayList)

boolean equals(Object o);//俩ArrayList是否相符
int hashCode();//返回hashcode
E get(int index);//按下标获取
E set(int index, E element);//按下标set一个元素
void add(int index, E element);
E remove(int index);//按下标删除
int indexOf(Object o);//获取某元素第一次出现下标
int lastIndexOf(Object o);//获取某元素最后一次出现下标
ListIterator listIterator();//与下边的方法都是生成一个List迭代器
ListIterator listIterator(int index);
List subList(int fromIndex, int toIndex);//生成子列表

好了就这么多,其余的都是java8之后添加的一些stream相关的方法,包括一些默认方法。

所以实际上我们只要知道上面的最基本方法就行了。

然后现在看看这些都是咋实现的。

值得一提的是


public abstract class AbstractList extends AbstractCollection implements List {

就是说AbstractList是继承了抽象集合并且实现了List接口。。。虽然不懂为啥,但是说明了AbstractList应该是实现了一部分方法。

接下来从类中搜索这一个个方法。

首先是几个常量,整理一下大概是:

private static final long serialVersionUID = 8683452581122892189L;
//是序列化过程中的版本号uid,一个long数字
private static final int DEFAULT_CAPACITY = 10;
//顾名思义了,默认的容量是10
private static final Object[] EMPTY_ELEMENTDATA = {};
//后面两个未知,我们慢慢往后看
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//这是ArrayList的基础,是不可被序列化的Object数组,这时候还没初始化
transient Object[] elementData;
private int size;

一个私有变量,这还用说嘛?就是元素数量啊,这里需要说一下,元素数量一般是小于等于capacity的。

 

好的,接下来我们要初始化一个ArrayList了。

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 c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // defend against c.toArray (incorrectly) not returning Object[]
            // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

我把里面的一些注释去掉了,可以更直观的看到:

1、当你使用的是

new ArrayList<>(1024);

这种形式的,就是调用了第一个方法,如果给的capacity是0,就把之前的final的空数组赋值给元素数组。

小于0那肯定就抛出异常了。(不合法参数异常)

2、当你使用的是

new ArrayList<>();

相信大多数人和我一样都不爱给它设定容量,都想的是先凑合用呗,反正不够他能扩容。

那么调用的是第二个方法。

this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

表明此时已经初始化了,但是依然是个空数组!

虽然DEFAULTCAPACITY_EMPTY_ELEMENTDATA看上去和EMPTY_ELEMENTDATA有点区别,但是现在依然是一个空的,长度为0的Object数组。

3、当你使用的是:

Collection collection=new ArrayList<>();
new ArrayList<>(collection);

这种形式的,那么调用第三个构造器,那么实际上就会把集合元素复制进来。

 

看完了“构造”,现在看一看“添加”,毕竟增删改查是每个数据结构的必有特点嘛。

 

现在,我想调用一个add方法,来为ArrayList增加一个元素。

 

 

    public boolean add(E e) {
        modCount++;//modCount是AbstractList里的数字,记录列表被修改的次数
        add(e, elementData, size);//调用下边那个add
        return true;//加入元素成功
    }
private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();//当发现插入的位置已经达到了元素数组的容量极限length时,扩容。
        elementData[s] = e;//简单的插入步骤,并且size+1
        size = s + 1;
    }
private Object[] grow() {//与下边的调用生成了一个新的elementData,原来的数组复制到新的数组中
        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;//旧的容量
        int newCapacity = oldCapacity + (oldCapacity >> 1);//新容量是旧的容量的1.5倍
        if (newCapacity - minCapacity <= 0) {//当新容量小于等于最小容量进行下面操作
            //我们考虑一下什么时候新容量是小于等于最小容量的 答案是新建并且插入第一个元素的时候
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                return Math.max(DEFAULT_CAPACITY, minCapacity);
                  //返回最开始那个默认Capacity和 
               //minCapacity更大一点的,一般的话,除非一开始设定cap为1,否则就会变成默认容量10
            if (minCapacity < 0) // overflow,溢出了
                throw new OutOfMemoryError();
            return minCapacity;//如果是不非空的情况,那么会会返回只增加一个长度的
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
            //最终如果新长度还是小于MAX_ARRAY_SIZE(实际上是最大int-8)返回,否则扩容到最大int值
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE)
            ? Integer.MAX_VALUE
            : MAX_ARRAY_SIZE;
    }

 

所以总的来说,我们常规操作一般是,new一个默认的arraylist,然后没插入前长度为0,(被一个长度为0的fina数组赋值),然后插入一个元素因为长度不够就扩容,扩容变为10,之后就是15,22 。。。。这样的序列,如果发现扩容的过程中新数组长度即将超过MaxArraySize那么就考虑是不是要直接扩容到MaxInteger这么大的长度2^31。接下来就是正常的放置操作。

 

接下来我们看看其他的add方法。

public void add(int index, E element) {
        rangeCheckForAdd(index);
        modCount++;
        final int s;
        Object[] elementData;
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow();//如果index是最后的那个位置,那么就相当于原来的add(element)
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);//复制函数会把index之后的元素往后一个位置复制
        elementData[index] = element;
        size = s + 1;
    }
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

这个比较好理解,相当于是insert了,这个会把插入位置的后面元素统统向后移动一个位置,所以最坏状况是O(n)的

时间复杂度,所以要小心使用!

 

    public boolean addAll(Collection c) {
        Object[] a = c.toArray();
        modCount++;
        int numNew = a.length;
        if (numNew == 0)
            return false;
        Object[] elementData;
        final int s;
        if (numNew > (elementData = this.elementData).length - (s = size))
            elementData = grow(s + numNew);
        System.arraycopy(a, 0, elementData, s, numNew);
        size = s + numNew;
        return true;
    }
    public boolean addAll(int index, Collection c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        modCount++;
        int numNew = a.length;
        if (numNew == 0)
            return false;
        Object[] elementData;
        final int s;
        if (numNew > (elementData = this.elementData).length - (s = size))
            elementData = grow(s + numNew);

        int numMoved = s - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index,
                             elementData, index + numNew,
                             numMoved);
        System.arraycopy(a, 0, elementData, index, numNew);
        size = s + numNew;
        return true;
    }

接下来两个addAll就不说了,依然是先检查是否要扩容,然后一段一段的复制,或者是插入式一段段复制。

依然遵循1.5倍扩容和最大MAX_INTEGER的原则。

 

说完了“增”,现在说说“改“

    public E set(int index, E element) {
        Objects.checkIndex(index, size);//检查是否下标越界
        E oldValue = elementData(index);//获得旧的值
        elementData[index] = element;//设为新的值
        return oldValue;//返回旧的值
    }

嘿嘿嘿,不知道吧?set方法的返回值实际上是旧值啊。好吧其实我也是前不久才知道的。。。。

 

说完了“改”我们看看查是怎么查

 

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

很好,符合预期,就是简单的线性查找,contain方法也是基于indexOf和IndexOfRange来搜索的,不过,被搜索的对象要分为null和 非null两种对象的查找。

 

接下来,看看ArrayList的删除。

 

public E remove(int index) {
        Objects.checkIndex(index, size);
        final Object[] es = elementData;

        @SuppressWarnings("unchecked") E oldValue = (E) es[index];
        fastRemove(es, index);//调用“快速删除”函数

        return oldValue;//返回旧值
    }
 private void fastRemove(Object[] es, int i) {
        modCount++;
        final int newSize;
        if ((newSize = size - 1) > i)
        //把被删除的下标后面部分分别往前复制一个长度,达到删除的目的
            System.arraycopy(es, i + 1, es, i, newSize - i);
        es[size = newSize] = null;
    }

所以啊,实际上按下标删除元素也是最坏O(n)时间复杂度的,慎重使用!

现在看看组删除,组删除就是一次性删除许多元素。

public boolean removeAll(Collection c) {
        return batchRemove(c, false, 0, size);
    }
boolean batchRemove(Collection c, boolean complement,
                        final int from, final int end) {
        Objects.requireNonNull(c);
        final Object[] es = elementData;
        int r;
        // Optimize for initial run of survivors
        for (r = from;; r++) {
            if (r == end)
                return false;
            if (c.contains(es[r]) != complement)
                break;
        }
        int w = r++;
        try {
            for (Object e; r < end; r++)
                if (c.contains(e = es[r]) == complement)
                    es[w++] = e;
        } catch (Throwable ex) {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            System.arraycopy(es, r, es, w, end - r);
            w += end - r;
            throw ex;
        } finally {
            modCount += end - w;
            shiftTailOverGap(es, w, end);
        }
        return true;
    }
    private void shiftTailOverGap(Object[] es, int lo, int hi) {
        System.arraycopy(es, hi, es, lo, size - hi);
        for (int to = size, i = (size -= hi - lo); i < to; i++)
            es[i] = null;
    }

其实删除依然是线性搜索,但是我们考虑一下这个算法,它实际上就是在循环的过程中有两个指针r,w。

当发现搜索过程中的元素不是要删除的集合中的元素时,指针w就写一次,就覆盖了那些删除的元素,然后把后面的元素都置为null。

这种算法比较节省空间,但是最坏时间复杂度依然是O(n)。

 

综上所述,ArrayList的增删改查都已经讲解完毕。

 

说些题外话,我们看看它如何读写(序列化的)。

 

首先我们在字段名的地方可以发现一些事情,那就是

transient Object[] elementData;

元素数组是在序列化时被忽略的,那么我们怎么知道反序列化之后就能正确的得到一个ArrayList呢?

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioral compatibility with clone()
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (int i=0; i

看来,也并不是没有写元素嘛,还是通过for循环来把ArrayList的元素写进输出流中了,而且,defaultWriteObject实际上已经把类的信息写入了。实际上,由于前后检查的modCount导致并发条件下根本无法写入被修改的ArrayList的,所以ArrayList是线程不安全的,但是,我们可以看出:

defaultWriteObject //写入当前的ArrayList除了元素数组的部分。

然后写入size,和size个元素的值。

 

与此对应的:

 

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // like clone(), allocate array based upon size not capacity
            SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size);
            Object[] elements = new Object[size];

            // Read in all elements in the proper order.
            for (int i = 0; i < size; i++) {
                elements[i] = s.readObject();
            }

            elementData = elements;
        } else if (size == 0) {
            elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new java.io.InvalidObjectException("Invalid size: " + size);
        }
    }

与写过程完全对等的过程:

先用defaultReadObject读取出ArrayList对象

然后读取元素个数size

然后把每个元素写回数组。

 

好了,剩下的就是java8流相关的一些东西,和迭代器之类的东西了。我们暂且不表。有机会的话写一篇有关的,哈哈哈。

 

你可能感兴趣的:(随笔)