文档章节

ArrayList源代码分析

恋佳
 恋佳
发布于 2015/07/22 14:05
字数 4382
阅读 4
收藏 1

本源代码来自JDK1.8  与1.7、1.6 略有不同

1 ArrayList中的属性

1 初始容量

初始大小为10

/**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

2 空的Object数组

当初始化容量为0时,就构造这样一个空的Objcet类型数组。

/**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

3 默认的空的Object数组

根据注释,这个大概意思就是构造一个空的对象数组,用来与EMPTY_ELEMENTDATA 这个数组进行对比,来确定当第一次向ArrayList中添加数据时,应该如果进行扩容,就是增加多大的容量暂时还不太好理解这个,往后看就懂了

/**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

4 真正用于保存数组的对象数组

ArrayList中 用来存储数组的对象数组 这个对象是transient 的,即不可被序列化的

由此可见,ArrayList底层实际是在维护一个对象数组

/**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

5 ArrayList 大小Size

用于标识当前ArrayList的真实大小

/**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

2 构造方法

ArrayList 共有三个构造方法

1 默认的空的构造方法

根据这个方法的注释,默认的构造方法构造的是构造了一个容量为十的List,但是从源代码来看,实际上在这个地方只是构造了一个空的对象数组,那么为什么说是十呢,在后来就能看懂了。

/**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

2 带有初始化容量的构造方法

根据指定的容量构造一个空的对象数组。如果容量为负数,抛出异常。应该注意的是,当指定容量为0时,构造时使用的就是前面的全局变量,即为一个空的对象数组。

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

3 利用一个已有的Collection来构造

这里首先用到了Collection.toArray()方法进行数组转换。

在这里有一个确认对象类型的检验,如果集合中的对象不是Objec类型,利用Array类的静态方法进行数组的拷贝,这里给出的注释是在调用toArray()方法时可能存在问题,这里的6260652应该BUG的Id号

当转换的数组为空时,ArryaList并没有使用转换的空结果,而是依然使用自己构造的空数组。也许Java程序员认为外来的都是不可信任的吧(个人理解)

<span style="font-size:18px;">public ArrayList(Collection<? extends E> c) {</span>
        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;
        }
    }


3 Add方法

这里把Add方法单独拿出来,这是因为这个Add不仅仅是一个方法,它是一个流程。如果把这个流程理解了,那么整个ArrayList也就懂了。前面遗留的几个不懂得地方在这都有解答

Add方法有两个

(1)Add(E e)

可以看到这个方法永远返回true,所以不应该通过add的返回值来判断是否添加成功。换句话说,Java的大神们在告诉你,我的方法永远都不会出错,你要相信我

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
这个方法一共只有三行,其中第一行的才是关键。这里面是在确定ArrayList的容量能否再多加一个。我们看到ArrayList是个无底洞对吧?想放多少放多,实际上都是源于这句话。而这句话的方法在做什么呢?下面给出源代码

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

ensureCapacityInternal 这个方法首先判断了当前的对象数组是不是默认的空数组,如果是的话,那么就在默认容量(10)和需要的容量中取一个最大值,然后把得到的这个值传递给下一个函数ensureExplicitCapacity。

从这个地方就可以解答了前面的疑问了,如果使用默认的构造方法,那么elementData 就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,当首次插入对象时,取最大值的时候一定是10,再经过后面的扩容,实际上就相当于构造了一个长度为10的对象数组。

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
ensureExplicitCapacity 这个方法第一行先不管是干嘛的,看后面的代码,很简单吧! 就是判断需要的容量和现在的用来存储的对象的数组(elementData)那个更大,如果现在已有的不够大了,那就扩容呗! grow(minCapacity)

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

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



这段代码进行了扩容,可以看到扩容通过newCapacity = oldCapacity + (oldCapacity >> 1);这段代码实现,实际上就是将新的容量变为原来容量的1.5倍

但是需要对扩容后的容量进行检验。共有两种可能,

1得到的容量比需要的容量小,为什么会有这种情况呢,溢出了呗。

2得到的容量超过了规定的最大容量,进入hugeCapacity中,如果需要的容量小于0,抛出内存溢出异常,如果需要的容量比规定的最大容量大,那么最大容量只能是 Integer.MAX_VALUE

最后将elementData 通过Array的复制拷贝方法进行了扩容。


由此就完成了整个添加新的元素的过程。从这个过程可以看出,ArrayList也并不是无限大的,它指定了一个最大容量 是Integer.MAX_VALUE - 8,实际最大只能是Integer.MAX_VALUE这个值了。

(2)add(int index, E element)

这个方法是在指定的位置插入元素 element

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++;
    }
第一行没什么好说的,肯定是检验index是否合法
private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
第二行与前面add方法一致,检验容量并扩容

第三行是arraycopy的方法,在这里面实现的就是将原来的数组从index位置开始,每个元素向后移一位。这是一个本地方法,看不到源代码啦

第四行比较简单啦,就是把要插入的元素放在他应该出现的位置上。

4Remove 方法

(1) remove(int index)

从指定的位置删除元素。实现原理就是把index后面的数据都向前移位。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;
    }

private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
(1)第一行就是检验index的返回,有人会问为什么不检验负数的情况。本人亲自试验了一下,如果index是负数的话,第一行是可以正确执行的,但是在elementData(index)这样地方就会抛出java.lang.ArrayIndexOutOfBoundsException: -1即 数组越界异常其实个人觉得,如果没有第一句 检验,仍然会抛出数组越界异常。只不过多了第一行的话,抛出的异常就是java.lang.IndexOutOfBoundsException: Index: 2, Size: 2,而且会打印出错误信息。又有哪个Java程序员不喜欢看到更详细的异常信息呢?

(2) 计算移动量,如果要删除的元素不是最后一位,就要进行移动。

(3)将移动后的数组末尾赋值null。这里有一句注释,让GC去回收。这是一种释放内存的方式。但是我们不能依赖这种方法, 因为我们永远不知道GC如何能去回收它。

(2)remove(Object o)

删除指定的元素,仅删除符合条件的第一个元素,如果不存在,返回false 即删除失败

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

分析这段代码,首先对o进行了非空处理。如果不进行这样的处理,将会引发o.equals的空指针异常。这个是非常常见的处理方式

在这if else语句块中,都用了fastRemove(index) 听上去挺高大上的,快速删除的,其实看了源码就知道,很简单。

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
    }

这不就是remove(index)方法的后半部门吗?为何不直接调用这个方法呢? 肯定是为了提交一点点效率吗,这毕竟是Java API嘛,要是不能高效一点,是吧、、、


(3)removeAll(Collection<?> c)

在集合中,删除与Collection中元素相等的元素。

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

第一行是java1.7新出的工具类,用于检验c非空的,如果为空,抛出空指针异常

接下来就是batchRemove(c, false) 这个方法了。源码里虽然没有给出注释,但从第一个if就可以看出,这个方法是用于返回与给定c相同或不同的元素,而且是全部。

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;
    }
下面来分析一下这段代码。首先对集合中的对象数组进行了一次复制,然后将数组中的元素依次与collection进行对比。try{}块的结果就是得到了一个数组, 数组前w位元素或与collection相同(complement=true的时候)或不同(complement=false)

下面来看finally,先来看第二个if{}代码块,它的作用就是把数组中w以后的元素全部变为null值,让gc来回收。

现在回到finally的第一个if中,看条件(r != size),似乎永远不会满足这个条件吧。上面的for循环一直r++啊,可是别忘了,c.contains(elementData[r])这句话是有可能抛出异常的,如果一旦类型不匹配,就会抛出异常 进入finally中。

这个方法,如果没有删除任何数据,那么将会返回false。


现在我们再来看return batchRemove(c, false);这行的含义吧。它指定了第二个参数为false,所以在batchRemove()保留下来的都是与collection中不同的元素。相同的元素都被删除了。这样就达到了removeAll(Collection<?> c)的目的。

5 迭代器

迭代器可谓是操作ArrayList的神器,在ArrayList内部通过内部类的形式实现了Iterator接口,完成对ArrayList的操作。先让我们来内部一 Itr的源码

private class Itr implements Iterator<E> {
        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 != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                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();
            checkForComodification();

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

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

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


这个类并不长,让我们来分析一下

(1) 属性

a  游标

int cursor; 我们知道iterator是始终向前走的,就是这个游标始终在++的原因

b 末尾标识

 int lastRe 标识了最后一个返回的元素的索引位置,-1代表这个元素不存在

C 操作数标识

 int expectedModCount = modCount;  这个非常重要,它用来校验在使用iterator期间,是否存在非Iterator的操作对ArrayList进行了修改。

(2)checkForComodification 方法

这个方法很简单,但是可以看到在这个内部类中,几乎所有方法都在调用它。它所做的工作就是校验在使用iterator期间,是否存在非Iterator的操作对ArrayList进行了修改。

在ArrayList中,有很多操作都修改了一个变量,modCount。每次进行操作,modCount都在++。前面一直不理解这个有什么用,在这看到了它的用意。Iterator的游标特性决定了它对ArrayList中元素在这一时刻的位置很敏感,如果当前游标在index位置,而有其他操作在index-1的位置上插入了一个元素,那么调用iterator的next()方法,返回的还是当前这个元素,这样就乱了。为了避免这个情况发生,需要在这个期间把ArrayList“锁住“。它并没有实现真正的锁,所以采用了这个校验的方式。

(3)next()

返回当前游标位置的元素,这里面第一个if判断的条件很有意思。因此游标前移,当移动到一个不存在数据的地方,它抛出了异常。而并没有返回null。这就是我们为什么在使用iterator的时候不能用(null==iterator.next())来判断的原因。而是在要每次循环开始的时候判断iterator.hasNext()。

(4) remove()

删除lastRe 所标识位置的元素。我们可以把它理解为当前元素。在try前面有一个校验,保证元素没有被改动过,要不就删错了。

在try{}语句块中,首先删除了lastRe标识的元素,然后让游标指向了这个位置。我们知道在删除元素以后,这个位置有了新的元素,这样再次调用next()的时候不会出现空指针异常,更不会跳过一个元素。

最后expectedModCount = modCount;这句相当于释放了锁。也是在表示,在我Iterator的地盘,只有我能够去修改mod,别人动了就不行!

6 set 与get


set将指定位置的元素替换

public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
get  获取指定位置的的元素

public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

这两个方法里面  都有一步rangeCheck,即检验输入索引位置是否越界,它将会引发越界异常。

7 contains 、indexOf、lastIndexOf

这三个方法功能很相近

第一个是查看ArrayList中是否含有指定元素

第二个是返回指定元素的第一个出现的索引位置

第三个返回指定元素的最后一个出现的索引位置

这是一组依赖的方法,实际上执行的是同一段代码。

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


这段代码很简单,就是一个简单的遍历,只是变量的起点不同而已,就实现了不同的功能。indexof的代码有没有觉得似曾相识呢?就是跟remove的方法差不多呢,这也再一次提醒我们,如果要使用一个对象的方法时,一定要注意解决空指针异常

8 clone

拷贝,重写了Objec类的拷贝方法,ArrayList的拷贝是浅拷贝

public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

这里面用到了前面看到很多次的方法,Arrays.copyOf()

9 sort

排序方法,这个方法用到的是Array类的静态排序方法

public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;
        Arrays.sort((E[]) elementData, 0, size, c);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

10 总结

上面介绍了ArrayList中常用的方法,第一次较为认真地读源代码,确实发现了不少秘密。

(1) ArrayList是用Object数组来实现了,所谓的动态扩容,实际上就是在不断地产生新的数组,然后进行复制。

(2)在使用ArrayList插入大量元素时,应该有选择的申请一个容量,而不是使用默认的容量。扩容也是会消耗时间的

(3) ArrayList不是线程安全的,它的操作中没有使用同步方法。如果想使用线程安全的,可以用Vector,也可以使用Collections.synchronizedList来创建线程安全的list。






















版权声明:本文为博主原创文章,未经博主允许不得转载。

© 著作权归作者所有

恋佳
粉丝 0
博文 76
码字总数 62107
作品 0
大连
私信 提问
正确认识Arrays.asList方法

Arrays工具类提供了一个方法asList, 使用该方法可以将一个变长参数或者数组转换成List 。 其源代码如下: 运行上述程序,输出如下内容。 心血来潮,突然想在创建的列表中添加一个字符串“Coo...

王孟君
2016/10/20
1K
11
ArrayList源码分析

整体介绍 ArrayList实现了List接口,是一个常见的集合类,它有一下特点: 是顺序容器,即元素存放的数据与放进去的顺序相同, 允许放入null元素, 底层通过数组实现。 添加元素而容量不足时,...

SeaRise
2017/11/23
0
0
ArrayList线程安全问题

自己的博客地址:http://www.lixiang.red/tech/java/2018/02/05/arrayList-mutiThread.html 欢迎点击,更多好技术好文在等着您! 验证思路 新建若干个(1000)个线程,一个全局arraylist. 然后...

微笑小小刀
2018/02/05
0
0
Java对象内存占用分析

本文深入分析并验证了不同Java对象占用内存空间大小的情况。对于不同的jvm实现,Java对象占用的内存空间大小可能不尽相同,本文主要分析HotSpot jvm中的情况,实验环境为64位window10系统、J...

codershamo
2017/11/29
0
0
fail-fast原理以及解决办法

1.背景 今天处理一个多线程业务,这个业务原来是在单线程中运行的,现在对这个业务进行改造,使其能在多线程中运行以加快处理效率。结果抛出了ConcurrentModificationException异常。究其原因...

kukudeku
2016/08/31
141
0

没有更多内容

加载失败,请刷新页面

加载更多

mysql-connector-java升级到8.0后保存时间到数据库出现了时差

在一个新项目中用到了新版的mysql jdbc 驱动 <dependency>     <groupId>mysql</groupId>     <artifactId>mysql-connector-java</artifactId>     <version>8.0.18</version> ......

ValSong
今天
5
0
Spring Boot 如何部署到 Linux 中的服务

打包完成后的 Spring Boot 程序如何部署到 Linux 上的服务? 你可以参考官方的有关部署 Spring Boot 为 Linux 服务的文档。 文档链接如下: https://docs.ossez.com/spring-boot-docs/docs/r...

honeymoose
今天
6
0
Spring Boot 2 实战:使用 Spring Boot Admin 监控你的应用

1. 前言 生产上对 Web 应用 的监控是十分必要的。我们可以近乎实时来对应用的健康、性能等其他指标进行监控来及时应对一些突发情况。避免一些故障的发生。对于 Spring Boot 应用来说我们可以...

码农小胖哥
今天
9
0
ZetCode 教程翻译计划正式启动 | ApacheCN

原文:ZetCode 协议:CC BY-NC-SA 4.0 欢迎任何人参与和完善:一个人可以走的很快,但是一群人却可以走的更远。 ApacheCN 学习资源 贡献指南 本项目需要校对,欢迎大家提交 Pull Request。 ...

ApacheCN_飞龙
今天
5
0
CSS定位

CSS定位 relative相对定位 absolute绝对定位 fixed和sticky及zIndex relative相对定位 position特性:css position属性用于指定一个元素在文档中的定位方式。top、right、bottom、left属性则...

studywin
今天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部