JDK源码阅读之 ArrayList。JDK版本1.8,

ArrayList是我们最常用的集合框架,可以说没有之一,接下来阅读一下一下ArrayList的源码

简介

ArrayList继承自 AbstractList,实现了List,RandomAccess,Cloneable,Serializable(java.io)。JAVA类讲究见名知意。光看名字就能猜出来ArrayList底层是以数组实现的。所以ArrayList一定是有序的,但是存在重复数据。观看源码我们可以看出来,它是线程不安全的(因为通篇源码都没有synchronizedasynchronized关键字)

接下来放个截图,是ArrayList的所有方法和成员变量,内部类等

 

成员变量

serialVersionUID :

用来进行java序列化和反序列化的,没啥好说的(什么?你问我什么叫序列化?自己百度去,我特么怎么知道)

DEFAULT_CAPACITY:

我们看一下这个属性的定义

   private static final int DEFAULT_CAPACITY = 10;

作为一个合格的java码农(文档翻译官),我们解读一下这条属性的含义。我们都知道ArrayList的底层是个数组,我们还知道java集合框架内的元素数量是可变的,可是数组不可变,那么JDK在制作ArrayList的时候,一定要给他底层的数组(我们一会就能看到它)一个默认的长度,那么,这个属性的作用就是默认的长度。如果元素个数发生变化,那又是另一个故事了,以及当你使用一下代码

package com.study.basicJava.sourceCode;

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        System.out.println(list.size());
    }
}

运行的结果为0,那么又是另一个故事了。具体我们可以等到ArrayList的构造方法里面详细研究,我们暂且记下来这个属性

EMPTY_ELEMENTDATA

先看声明

private static final Object[] EMPTY_ELEMENTDATA = {};

注释翻译过来就是用来为空的ArrayList共享的数组,这句话说起来有点拗口,但是 我争取给大家解释清楚到底是怎么一回事。首先我们看这个EMPTY_ELEMENTDATA是用static关键字修饰的,那么说明,在整个ArrayList对象的生命周期内,它是一直不变的,也就是一直都是一个空数组。当你创造出一个空的ArrayList时,空的ArrayList用来存储元素的数组就是这个狗东西,但是一旦你向这个空的ArrayList中插入元素时,ArrayList就不使用它了。具体可以往后看。

DEFAULTCAPACITY_EMPTY_ELEMENTDATA

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

好,我们继续翻译注释。用来为确定大小的空的Arraylist实例共享的数组。这个数组的作用是用来和EMPTY_ELEMENTDATA区分当第一个元素被插入到ArrayList中时,我们应该为ArrayList扩充多大的空间。。。。。这个解释起来比较拗口,我们统一都放到构造方法里讲吧。大家就记住一点,当代码为

List<T> list = new ArrayList()<>

时,起作用的是EMPTY_ELEMENTDATA;、

当代码为

List<T> list =new ArrayList<>(10)

时,起作用的就是我们的 DEFAULTCAPACITY_EMPTY_ELEMENTDATA

-----------------------------------------未完待续--------------------------------------------------------------------------------------------------------------------

elementData

代码先上

transient Object[] elementData;

在这行代码的后面,还有一句话,我觉得很有意思,也给大家写出来

// non-private to simplify nested class access

我们先看最后一句话,翻译过来就是,没有使用private修饰,以便简化嵌套类访问(很有意思,不是吗?)

先翻译一下注释:存储ArrayList元素的数组缓冲区,ArrayList的容量等于此数组的长度。任何一个空ArrayList都满足 elementData == DEFAULICAPACITY_EMPTY_ELEMENTDATA,当向这个空的ArrayList中插入第一个元素时,ArrayList的容量将会被扩充到DEFAULT_CAPACITY。

这一段话都很好理解,不好理解的讲到构造函数的时候也就好理解了,而我比较好奇的地方在于两个方面

1 为什么elementData没有用private之类的修饰符控制?

2 transient关键字的作用是什么?为什么要用transient关键字修饰?

因为本菜鸡是一边看源码,一遍写的这篇博客,所以,先记下来这两个问题,等下再研究。而且,我认为,特备是第二个问题,很有必要专门开一篇博客

size

代码

private int size;

一句话:当前ArrayList的容量,或者是当前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);
        }
    }

注释里面写很清楚,构造一个指定容量的空ArrayList

参数 initialCapacity代表指定的容量大小,类型为int。需要注意的是,当参数initialCapacity为负数时,会抛出IllegalArgumentException

代码很好理解,大意就是当initialCapacity大于0时,用来存放元素的数组elementData被初始化,或者被赋值为一个长度为initialCapacity的Object数组。如果initialCapacity等于0的时候呢,elementData会被赋值为EMPTY_ELEMENTDATA。我们仍然记得,EMPTY_ELEMENTDATA是一个长度为0的Object静态static数组,为什么会将EMPTY_ELEMENTDATA赋值给elementData呢?我们可以这么理解,initialCapacity等于0,说明此时创造的ArrayList数组是个空数组,至少在此时,使用者没有发生或者没有现象预示即将发生对这个ArrayList的扩充行为(向ArrayList插入元素),我们用它来标识这个ArrayList是个空的,而且不会向里面插入元素

无参构造方法

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

构造一个初始容量为10的ArrayList,但是里面没有元素;

在这个构造方法中并没有体现初始容量为10,只是将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给elementData,标识此时的ArrayList虽然是空的,但是会插入元素。

参数为一个集合对象的构造函数

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

构造包含指定集合对象的元素的ArrayList,按集合对象的迭代器返回元素的顺序排列。

参数是指定的集合对象(Coolection对象都可以,但是如果ArrayList有泛型的话,只能放同样泛型的Coolection对象)

当指定的集合对象为null时,会抛出NPE异常

代码逻辑也很好理解,首先将指定集合对象C转换成数组赋值给elementData,再计算出此时ArrayList的容量,如果容量为0时,则将EMPTY_ELEMENTDATA赋值给elementData,如果容量不为0,那么此时还需要判断elementData是不是一个Object类型的数组,如果不是,就需要将elementData转换成Object数组。在这里,JDK使用了一个工具类Arrays的copy方法,这个方法的作用是将一个数组对象转换为指定大小,指定类型的数组对象,还是很有意思的,有机会可以好好研究一下

接下来就是ArrayList的各个方法了

trimToSize

public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

我们先不看具体这个方法是如何实现的,我想和大家一起,根据这个方法的名称去猜测这个方法到底是干啥的。trim,根据有道词典,意思为修剪,去除的意思。在java中呢,一般也是这个意思,比如,String就有一个trim()方法,作用是去除字符串内所有的空格。那么,我们大胆的猜测,trimToSize()的作用是将ArrayList的容量修剪为存储在当前ArrayList内拥有的元素数量。胡扯完毕,看注释

将ArrayList实例的容量缩减至ArrayList此时拥有的元素数量,应用程序可以使用此操作将ArrayList占有的内存得以最小化。

看来,我们才对了。

然后,我们看方法实现。这时候,我发现一个有意思的变量   modCount。我并没有在ArrayList的源码中看到这个变量的声明,看来,我们应该去ArrayList他爹,AbstractList中找找,不负众望,我找到这个鳖孙了

protected transient int modCount = 0;

这么长的一段话呢,看了就头皮发麻,咱们主要的目的就研究ArrayList,不是研究他爹,我们说一说这个属性是干嘛的就行了。

这个属性在ArrayList中的作用主要是为了记录ArrayList中存储的元素增删改的次数,也可以说是元素数量变化的次数。当然,他还一个别的作用,我们暂时研究不到这个地步,不过说一说也无妨。根据注释解释的来看,他还记录了一种状况,那就是当ArrayList使用迭代器的时候,由于某些操作,使迭代后的结果和我们期望的结果不同。说白了,就当多线程同时工作时引起的ArrayList(所有继承了abstractlist的类都会有这种情况)数据不同步。这些是一些线程安全问题,暂时先不讨论

接下来的这个方法是用来保证或者说扩充ArrayList的容量的

    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

方法内先计算最小扩充量,以保证能够放下参数minCapacity所代表的容量。计算规则比较简单。首先要看存储数据的elementData是否是一个空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA),如果是的话,最小扩充量就为0.如果不是的,最小扩充量就是10(DEFAULT_CAPACITY)

之后在比较一下最小扩充量和参数的大小。如果参数大于我们计算出的最下扩充量,说明我们确实需要扩容,因此,调用另一个方法

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

既然选择扩容了,那么集合内的元素数量是肯定会发生变化,因为modCount需要自增。之后开始条件判断,如果参数大于此时集合内部元素的数量,那么再次调佣另一个方法

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

这个方法才是真正保证集合的容量能够大于等于参数。先记录之前的元素数量,接下来我们看到一个移位运算,且不管这个移位运算最终的结果如何,但是我们会发现,新的容量一定是大于旧的容量的。接下来的逻辑就简单了。如果新容量小于参数,那么就将参数赋值给新容量。

然后重置elementData。

还有几个方法也是用来计算容量相关的,我们来看一下

容量计算

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

逻辑很好理解,这里不再赘述。

接下来这个方法的意义没看太懂,先记录一下

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

返回集合内元素的数量

    public int size() {
        return size;
    }

判断集合内是否有元素

public boolean isEmpty() {
        return size == 0;
    }

判断集合内是否含有参数元素

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

集合拷贝方法(浅拷贝,不会复制元素本身)

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

浅拷贝最大的特点就是,假设我们的源集合对象是list,复制得到的集合对象是copyList,那么

list.equals(copyList)

的返回值为false。

数组转换方法

    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

这个方法按照JDK注释来讲,其实是数组和集合之间的桥梁,允许集合转数组。

在ArrayList中,这个方法的返回值是一个新分配的数组,它包含ArrayList内的所有元素,元素顺序和ArrayList中的元素顺序一致。而且,由于返回值是新分配的,所以我们可以对这个数组做出更改操作(添加或移除元素)

另一个带泛型的集合转数组方法(推荐使用)

这个方法带一个入参,就是一个泛型数组。这个方法的精妙之处在于返回数组的处理。如果参数a的长度比集合内元素的数量小的话,返回值是一个长度为集合元素数量的泛型数组。反之则会将集合内的元素填充到数组a中,然后a剩下的位置将会以null填充。

    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

根据下标获取元素的一个方法。由于缺省了修饰符,所以这个方法只能在本类和同一包下的其他类内使用。

众所周知,我们的ArrayList是以数组为底层实现的,那么获取某个位置的元素,其实也就是获取elementData数组中某个位置的元素。

E elementData(int index) {
        return (E) elementData[index];
    }

接下来是暴露给外部的获取元素的方式,参数也是下标。当然,首先还是要检查一下元素下标是否超过元素数量

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

        return elementData(index);
    }

元素替换

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

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

在集合尾部添加元素。首先先对集合进行扩容,也就是说得判断elementData的长度是否还够用,不够用就要重新申请一个新数组,具体逻辑就在前面。

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

在集合的某个位置添加元素。从这个位置(包括这个位置)的元素统一向后移动。

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

移除集合中某个位置的元素

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

这是一个私有方法,具体作用在于我们接下来要介绍的一个方法,删除特定元素

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

原理和contains类似,如果集合内有这个元素就移除,没有就返回false。

清除集合

    public void clear() {
        modCount++;

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

        size = 0;
    }

将某个集合被的全部元素添加到ArrayList中,第一个参数是指插入的位置

    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

逻辑也很好理解,仍然是重新分配数组,利用toArray方法将参数集合C转换为数组然后与elementData合并。

移除集合内一段区间内的元素

protected void removeRange(int fromIndex, int toIndex) {
        modCount++;
        int numMoved = size - toIndex;
        System.arraycopy(elementData, toIndex, elementData, fromIndex,
                         numMoved);

        // clear to let GC do its work
        int newSize = size - (toIndex-fromIndex);
        for (int i = newSize; i < size; i++) {
            elementData[i] = null;
        }
        size = newSize;
    }

我们注意到这个方法是使用protected修饰的,说明它只在本类和同包下使用。

首先对modCount进行自增操作,表示集合内的元素数量发生了改变;然后计算需要移除的元素数量。在之前,我们说过System.arraycopy方法的含义,在这里我们重新说一下

public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

从src数组中下标为srcPos的位置开始,向后复制length个元素,然后从dest数组的下标为destPos位置开始粘贴。

继续来看我们的方法。那么这个操作的含义就是将elementData中从下标为toIndex位置开始复制numMoved个元素,然后从elementData的下标为fromIndex的位置开始粘贴。

计算出移除后集合元素的新数量newSize(总数量减去移除的数量就是剩下的数量),然后将移除过的位置统一赋值为null,通知GC回收

移除交集

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

移除参数C集合内的所有元素,但是有个要求,集合C不能为null。集合C可以是当前ArrayList的子集,也可以不是,甚至可以没有交集。

但是我们可以根据返回值看出来。如果当前集合和参数c有交集就会返回true,然后交集会在当前集合中被移除;否则返回false。

移除差集

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

按照注释的说法是只保留集合内与C相同的元素,说白了就是保留交集,移除差集。

如果当前List不是集合C的一个子集,则返回true。否则返回false。

或者调用此方法后,当前集合发生了改变就返回true,反之false。

批量删除

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

刚才的removeAll和retainAll的具体实现都是靠这个方法。通过传递complement来决定到底移除那些元素,删除方式和removeRange方法基本一样

接下来看一看ArrayList的几个内部类。

第一个,Itr,据说是AbstractList.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;

        Itr() {}

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

基本和AbstractList.Itr一样唯一不同的是,AbstarctList.Itr获取数据用的是get,但是在ArrayList中进行了更确切的实现,就是直接获取数组中的元素。

至此,ArrayList基本的东西都扯完了。由于本楼主也是个菜鸟,这篇博客其实就是把JDK文档翻译了一遍,一些难懂的知识基本也没设计(序列化等),有什么不对的地方希望大家给我指出来,谢谢

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值