Java -- 基于JDK1.8的ArrayList源码分析

1,前言java

  好久没有写博客了,很想念你们,18年都快过完了,才开始写第一篇,争取后面每周写点,权当是记录,由于最近在看JDK的Collection,并且ArrayList源码这一块也常常被面试官问道,因此今天也就和你们一块儿来总结一下面试

2,源码解读api

  当咱们通常提到ArrayList的话都会脱口而出它的几个特色:有序、可重复、查找速度快,可是插入和删除比较慢,线程不安全,那么如今阿呆哥哥就会有这些疑问:为何说是有序的?怎么有序?为何又说插入和删除比较慢?为何慢?还有线程为何不安全?因此带着这些问题,咱们一一的来源码中来找找答案。数组

  通常对于一个陌生的类,咱们想使用它,都会先看它构造方法,再看它的属性和方法,那么咱们也按照这种方式来读读ArrayList这个类安全

  2.1构造方法多线程

ArrayList<String> arrayList = new ArrayList();
ArrayList<String> arrayList1 = new ArrayList(2);

  通常来讲咱们常见使用ArrayList的建立方式是上面的这两种框架

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final Object[] EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;

public ArrayList() {
        this.elementData = 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);
        }
}

  上面是咱们两个构造方法和咱们类中基本的属性,从上面的代码上来看,在建立构造基本上都没有作,且定义了两个默认的空数组,默认容器的大小DEFAULT_CAPACITY为10,还有咱们真正存储元素的地方elementData数组,因此这就是为何说ArrayList存储集合元素的底层时是使用数组来实现,OK,上面的代码除了一个transient 修饰符以外咱们同窗们可能有点陌生以外,其他的应该都能看的懂,transient 有什么做用还有为何用它修饰elementData字段,这个须要看完整个源码以后,我再来给你们解释的话比较合适,这里只须要留心一下。异步

  还有一个不经常使用的构造方法函数

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

  第2行:利用Collection.toArray()方法获得一个对象数组,并赋值给elementData ui

  第3行:size表明集合的大小,当经过别的集合来构造ArrayList的时候,须要赋值size

  第5-6行:判断 c.toArray()是否出错返回的结果是否出错,若是出错了就利用Arrays.copyOf 来复制集合c中的元素到elementData数组中

  第9行:若是c中元素数量为空,则将EMPTY_ELEMENTDATA空数组赋值给elementData

  上面就是全部的构造函数的代码了,这里咱们能够看到,当构造函数走完以后,会建立出数组elementData和初始化size,Collection.toArray()则是将Collection中全部元素赋值到一个数组,Arrays.copyOf()则是根据Class类型来决定是new仍是反射来创造对象并放置到新的数组中,源码以下:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
}

  这里面System.arraycopy(Object src,  int  srcPos, Object dest, int destPos,  int length) 这个方法在咱们的后面会的代码中会出现,就先讲了,定义是:将数组src从下标为srcPos开始拷贝,一直拷贝length个元素到dest数组中,在dest数组中从destPos开始加入先的srcPos数组元素。至关于将src集合中的[srcPos,srcPos+length]这些元素添加到集合dest中去,起始位置为destPos

  2.2 增长元素方法

  通常常用的是下面三种方法

arrayList.add( E element);
arrayList.add(int index, E element);
arrayList.addAll(Collection<? extends E> c);

  让咱们一个个来看看

 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
}

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

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

Integer. MAX_VALUE = 0x7fffffff;
MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8

  第2行:调用ensureCapacityInternal()函数

  第8-9行:判断当前是不是使用默认的构造函数初始化,若是是设置最小的容量为默认容量10,即默认的elementData的大小为10(这里是有一个容器的概念,当前容器的大小通常是大于当前ArrayList的元素个数大小的)

  第16行:modCount字段是用来记录修改过扩容的次数,调用ensureExplicitCapacity()方法意味着肯定修改容器的大小,即确认扩容

  第26-30、35-44行:通常默认是扩容1.5倍,当时当发现仍是不能知足的话,则使用size+1以后的元素个数,若是发现扩容以后的值大于咱们规定的最大值,则判断size+1的值是否大于MAX_ARRAY_SIZE的值,大于则取值MAX_VALUE,反之则MAX_ARRAY_SIZE,也就数说容器最大的数量为MAX_VALUE

  第32行:就是拷贝以前的数据,扩大数组,且构建出一个新的数组

  第3行:这时候数组扩容完毕,就是要将须要添加的元素加入到数组中了

public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
}

  第2-3行:判断插入的下标是否越界

  第5行:和上面的同样,判断是否扩容

  第6行:System.arraycopy这个方法的api在上面已经讲过了,这里的话则是将数组elementData从index开始的数据向后移动一位

  第8-9行:则是赋值index位置的数据,数组大小加一

public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
}

  第2行:将集合转成数组,这时候源码没有对c空很奇怪,若是传入的Collection为空就直接空指针了

  第3-7行:获取数组a的长度,进行扩容判断,再将新传入的数组复制到elementData数组中去

  因此对增长数据的话主要调用add、addAll方法,判断是否下标越界,是否须要扩容,扩容的原理是每次扩容1.5倍,若是不够的话就是用size+1为容器值,容器扩充后modCount的值对应修改一次

  2.3 删除元素方法  

  经常使用删除方法有如下三种,咱们一个个来看看

arrayList.remove(Object o);
arrayList.remove(int index)
arrayList.removeAll(Collection<?> c)

  

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

  从上面源码能够看出,若是要移除的元素为null和不为空,都是经过for循环找到要被移除元素的第一个下标,因此这里咱们就会思考,当咱们的集合中有多个null的话,是否是调用remove(null)这个方法只会移除第一个出现的null元素呢?这个须要同窗们下去验证一下。而后经过System.arraycopy函数,来从新组合elementData中的值,且elementData[size]置空原尾部数据 再也不强引用, 能够GC掉。

public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++;
        E oldValue = (E) 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;
}

  能够看到remove(int index)更简单了,都不须要经过for循环将要删除的元素下边确认下来,总体的逻辑和上面经过元素删除的没什么区别,再来看看批量删除

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

public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
}

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

  第二、6-10行:对传入集合c进行判空处理

  第13-15行:定义局部变量elementData、r、w、modified   elementData用来从新指向成员变量elementData,用来存储最终过滤后的元素,w用来纪录过滤以后集合中元素的个数,modified用来返回此次是否有修改集合中的元素

  第17-19行:for循环遍历原有的elementData数组,发现若是不是要移除的元素,则从新存储在elementData,且w自增

  第23-28行:若是出现异常,则会致使 r !=size , 则将出现异常处后面的数据所有复制覆盖到数组里。

  第29-36行:判断若是w!=size,则代表原先elementData数组中有元素被移除了,而后将数组尾端size-w个元素置空,等待gc回收。再修改modCount的值,在修改当前数组大小size的值

  2.3 修改元素方法

arrayList.set(int index, E element)

  常见的方法也就是上面这一种,咱们来看看它的实现的源码

public E set(int index, E element) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        E oldValue = (E) elementData[index];
        elementData[index] = element;
        return oldValue;
}

  源码很简单,首先去判断是否越界,若是没有越界则将index下表的元素从新赋值element新值,将老值oldValue返回回去

  2.4 查询元素方法

arrayList.get(int index);

  让咱们看看源码

public E get(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        return (E) elementData[index];
}

  源码也炒鸡简单,首先去判断是否越界,若是没有越界则将index下的元素从elementData数组中取出返回

  2.5 清空元素方法

arrayList.clear();

  常见清空也就这一个方法

public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
}

  源码也很简单,for循环重置每个elementData数组为空,修改size的值,修改modCount值

  2.6 判断是否存在某个元素

arrayList.contains(Object o);
arrayList.lastIndexOf(Object o);

  常见的通常是contains方法,不过我这里像把lastIndexOf方法一块儿讲了,源码都差很少

public boolean contains(Object o) {
        return indexOf(o) >= 0;
}

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

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

  经过上面的源码,你们能够看到,不论是contains方法仍是lastIndexOf方法,其实就是进行for循环,若是找到该元素则记录下当前元素下标,若是没找到则返回-1,很简单

  2.7 遍历ArrayList中的对象(迭代器)

Iterator<String> it = arrayList.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
}

  咱们遍历集合中的元素方法挺多的,这里咱们就不讲for循环遍历,咱们来看看专属于集合的iterator遍历方法吧

public Iterator<E> iterator() {
        return new Itr();
}

private class Itr implements Iterator<E> {
        // Android-changed: Add "limit" field to detect end of iteration.
        // The "limit" of this iterator. This is the size of the list at the time the
        // iterator was created. Adding & removing elements will invalidate the iteration
        // anyway (and cause next() to throw) so saving this value will guarantee that the
        // value of hasNext() remains stable and won't flap between true and false when elements
        // are added and removed from the list.
        protected int limit = ArrayList.this.size;

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

        @SuppressWarnings("unchecked")
        public E next() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            int i = cursor;
            if (i >= limit)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
                limit--;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

  第1-3行:在获取集合的迭代器的时候,去new了一个Itr对象,而Itr实现了Iterator接口,咱们主要重点关注Iterator接口的hasNext、next方法

  第12-16行:定义变量,limit:用来记录当前集合的大小值;cursor:游标,默认为0,用来记录下一个元素的下标;lastRet:上一次返回元素的下标

  第18-20行:判断当前游标cursor的值是否超过当前集合大小zise,若是没有则说明后面还有元素

  第24-31行:在这里面作了很多线程安全的判断,在这里若是咱们异步的操做了集合就会触发这些异常,而后获取到集合中存储元素的elemenData数组

  第32-33行:游标cursor+1,而后返回元素 ,并设置此次次返回的元素的下标赋值给lastRet

 

3,看源码以前问题的反思

  ok,上面的话基本上把咱们ArrayList经常使用的方法的源码给看完了。这时候,咱们须要来对以前的问题来一一进行总结了

  ①有序、可重复是什么概念?

public static void main(String[] args){
        ArrayList arrayList = new ArrayList();
        arrayList.add("1");
        arrayList.add("1");
        arrayList.add("2");
        arrayList.add("3");
        arrayList.add("1");
        Iterator<String> it = arrayList.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
}

输出结果
1
1
2
3
1

  可重复是指加入的元素能够重复,有序是指的加入元素的顺序和取出来的时候顺序相同,通常这个特色是List相对于Set和Map来比较出来的,后面咱们把Set、Map的源码看了以后会更加理解这两个特色

  ② 为何说查找查找元素比较快,但添加和删除元素比较慢呢?

  咱们从上面的源码获得,当增长元素的时候是有可能会触发扩容机制的,而扩容机制会致使数组复制;删除和批量删除会致使找出两个集合的交集,以及数组复制操做;而查询直接调用return (E) elementData[index]; 因此说增、删都相对低效 而查找是很高效的操做。

  ③ 为何说ArrayList线程是不安全

  从上面的代码咱们都知道,如今add()方法为例

public boolean add(E e) {
        //肯定是否扩容,这里能够忽略
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }    

  这里咱们主要看两点,第一点add()方法前面没有synchronized字段、第二点 elementData[size++] = e;这段代码能够拆开为下面两部分代码

 elementData[size] = e;
 size++

  也就是说整个add()方法能够拆为两步,第一步在elementData[Size] 的位置存放此元素,第二步增大 Size 的值。咱们都知道咱们的CUP是切换进程运行的,在单线程中这样是没有问题的,可是通常在咱们项目中不少状况是在多线程中使用ArrayList的,这时候好比有两个线程,线程 A 先将元素存放在位置 0。可是此时 CPU 调度线程A暂停,线程 B 获得运行的机会。线程B也向此 ArrayList 添加元素,由于此时 Size 仍然等于 0 ,因此线程B也将元素存放在位置0。而后线程A和线程B都继续运行,都增长 Size 的值。这样就会获得元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这样就形成了咱们的线程不安全了。

  你们能够写一个线程搞两个线程来试试,看看size是否是有问题,这里就不带你们一块儿写了。

  ④ transient 关键字有什么用?

  唉,这个就有点意思了,这个是咱们以前读源码读出来的遗留问题,那源码如今读完了,是时候来解决这个问题了,咱们来看看transient官方给的解释是什么

当对象被序列化时(写入字节序列到目标文件)时,transient阻止实例中那些用此关键字声明的变量持久化;当对象被反序列化时(从源文件读取字节序列进行重构),这样的实例变量值不会被持久化和恢复。

  而后咱们看一下ArrayList的源码中是实现了java.io.Serializable序列化了的,也就是transient Object[] elementData; 这行代码的意思是不但愿elementData被序列化,那这时候咱们就有一个疑问了,为何elementData不进行序列化?这时候我去网上找了一下答案,以为这个解释是最合理且易懂的

在ArrayList中的elementData这个数组的长度是变长的,java在扩容的时候,有一个扩容因子,也就是说这个数组的长度是大于等于ArrayList的长度的,咱们不但愿在序列化的时候将其中的空元素也序列化到磁盘中去,因此须要手动的序列化数组对象,因此使用了transient来禁止自动序列化这个数组

  这时候咱们是懂了为何不给elementData进行序列化了,那当咱们要使用序列化对象的时候,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 behavioural compatibility with clone()
        s.writeInt(size);

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

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

    /**
     * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
     * deserialize it).
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

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

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

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);

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

    经过writeObject方法将数据非null数据写入到对象流中,再使用readObject读取数据

 

4,总结

  上面咱们写了这么一大篇,是时候该来总结总结一下了

  ①查询高效、但增删低效,增长元素若是致使扩容,则会修改modCount,删出元素必定会修改。 改和查必定不会修改modCount。增长和删除操做会致使元素复制,所以,增删都相对低效。而在咱们常见的Android场景中,ArrayList多用于存储列表的数据,列表滑动时须要展现每个Item(element)的数组,因此查询操做是最高频的,且增长操做只有在列表加载更多时才会用到 ,并且是在列表尾部插入,因此也不须要移动数据的操做。而删操做则更低频。 故选用ArrayList做为保存数据的结构

  ②线程不安全,这个特色通常会和Vector作比较,Vector的源码,内部也是数组作的,区别在于Vector在API上都加了synchronized因此它是线程安全的,以及Vector扩容时,是翻倍size,而ArrayList是扩容50%。Vector的源码你们能够在后面闲下来的时候看看,这里给你们留一个思考题:既然Vector是安全的,那为何咱们在平常开发Android中基本上没有用到Vector呢?你们能够闲下来的时候来寻找一下这个问题的答案

  最后再啰嗦一句,写彻底篇后发现 ,感受很久没写博客手很生了,在写的过程总发现大致框架不对也在一点点的修复,后面争取坚持写下来,加油!!!

相关文章
相关标签/搜索