在网上找了些ArrayList的讲解,包括源码分析,找来找去,感觉内容讲的不是那么精髓,今天呢,我就斗胆给大家分享下我对ArrayList的理解哈,斗胆哈哈。

      首先呢,我只讲java集合下ArrayList,其余LinkList、Map和Set等内容我在这篇文章中不进行分享,它们将依次在我的以后文章中出现和大家分享。

1、概述

      在开发过程中,要数java集合中那个数据结构最常用,那么大家都会毫不犹豫的认为是Arraylist。它继承AbstractList类,底层是基于数组来实现容量大小的动态变化,并且允许null值存在,同时它还实现了List,RandomAccess,Cloneable,Serializable接口。因此支持快速访问,复制和序列化。

      ArrayList是有序的,这里所说有序指的是元素放入ArrayList集合的先后顺序,不是里面的元素比较大小之类顺序。

2、ArrayList创建

      ArrayList有三种方式创建,其中无参构造函数和带初始容量参数有参构造最为常用,另外一个参数是Collection的构造函数不常用。下面我就来依次和大家分析

2.1、无参构造创建 ——>new ArrayList<>()

List<Object> objects = new ArrayList<>();

      这种创建方式,大家最为常用,我们来进这种方式创建源码来看看。

      注意: 我们可以将泛型Object可以改成基本数据类型,也可以该用引用数据类型,一旦改变,那么我们在添加元素时只能添加我们指定的数据类型。比如:List<String> objects = new ArrayList<>();那么我们添加的元素只能是String类型的数据,其余不能添加。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    
    //默认初始容量大小
    private static final int DEFAULT_CAPACITY = 10;
    
    //空数组elenmentData  注意:这里是有参构造函数中使用,且参数为 0 时使用
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    //默认空数组elenmentData  注意:这里是无参构造函数时使用
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    //注意:上面两个空数组,其目的是为了区分是无参构造函数还是有参构造函数
    
    //ArrayList底层数据元素  
    transient Object[] elementData;
    
    //集合数据实际元素大小  注意:这里不是集合容量
    private int size;

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

      在源码中,无参构造函数是直接将一个空Objects数组赋值给elementData(这就是ArrayList集合底层数据),因此我们知道,在无参构造创建ArrayList中,ArrarList初始大小为0,千万不要一昧听从某些人说是10。 为什么有些人说是10呢?因为我这里给讲的源码版本是11,ArrayList在1.8版本之前和之后发生了某些改变,这里的初始容量就是其一,在1.8之前默认容量是10,之后就改成了0。

      这里你还会发出疑问,为什么上面有个默认容量大小DEFAULT_CAPACITY = 10,没错这是默认容量大小,但是它在无参构造创建是初始容量是0,当它往里面添加第一个元素时,那么它的容量就变为了10, 在下面的ArrayList扩容时我将详细讲解。

      从无参构造函数创建这段源码中我们可以清楚的看出两点:

      一: ArrayList初始容量为0,而非10

      二: ArrayList底层是Object数组。

      注意:我在代码的注释中写的也有内容去解释,大家有那些不懂的一定记得看上面代码中注释,希望大家能养成一种习惯。

2.2、带有容量大小的有参构造创建——>new ArrayList<>(length);

List<Object> list = new ArrayList<>(20);

      这里传参数20代表集合容量大小为20

      源码如下所示:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    
    //默认初始容量大小
    private static final int DEFAULT_CAPACITY = 10;
    
    //空数组elenmentData  注意:这里是有参构造函数中使用,且参数为 0 时使用
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    //默认空数组elenmentData  注意:这里是无参构造函数时使用
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    //注意:上面两个空数组,其目的是为了区分是无参构造函数还是有参构造函数
    
    //ArrayList底层数据元素  
    transient Object[] elementData;
    
    //集合数据实际元素大小  注意:这里不是集合容量
    private int size;

    //参数initialCapacity代表集合初始容量大小
    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);
        }
    }
    
}

      源码中initialCapacity参数代表是集合初始容量大小,进入构造方法后,将先判断传值是否大于0,当大于0时,直接new出一个Object数组,且数组大小为initialCapacity;当等于0时直接将EMPTY_ELEMENTDATA空数组赋值给elementData,注意下这个空数组叫EMPTY_ELEMENTDATA和上面无参构造赋值空数组不是同一个,它是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,两者虽然全是空数组,名字之所以不一样,是因为两者为了区分是无参构造还是传值为0的有参构造。最后一种是如果传值小于0,那么直接抛出非法容量大小异常。

      通过有参构造再次看出ArrayList底层为Object类型数组。

2.3、使用指定 Collection 来构造 ArrayList 的构造函数(这个不常用)

ArrayList<Object> objects1 = new ArrayList<>(collection);

      上面代码是在代码中创建方式,下面给大家列出源码,如下所示:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    
    //默认初始容量大小
    private static final int DEFAULT_CAPACITY = 10;
    
    //空数组elenmentData  注意:这里是有参构造函数中使用,且参数为 0 时使用
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    //默认空数组elenmentData  注意:这里是无参构造函数时使用
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    //注意:上面两个空数组,其目的是为了区分是无参构造函数还是有参构造函数
    
    //ArrayList底层数据元素  
    transient Object[] elementData;
    
    //集合数据实际元素大小  注意:这里不是集合容量
    private int size;

    public ArrayList(Collection<? extends E> 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;
        }
    }
    
}

      当进入构造函数时,先将collection转换为数组,然后将数组长度赋值给size,判断size是否不等于0,不等于0时,然后判断类型是否为Object类型,如果不是Object类型,那么将它转换为Object类型;如果长度等于0,那么直接将空Object数组赋值给elementData。

3、主要操作方法解析

      在分享操作方法时,我不单单要讲方法使用,而且还要着重讲源码分析,分析源码嘛,过程肯定无聊,希望大家仔细琢磨透。在下面操作方法中,我将会着重讲解add()方法,这个是大家最常用并且关联的问题比较多,因此这里我将多下点功夫。

3.1、add()

      add()方法是往集合中添加元素,其有两种情况,一种是直接添加元素,也就是add(“a”),这种将元素直接集合最后一个后面进行添加;另外一种方式是指定添加位置来添加元素,add(5,“a”),意思是在集合第五个元素中添加a这个元素。

      直接添加元素add(“a”)方法分析:

//代码使用add()方法
list.add("a");

add()源码:

//向集合中添加元素,调用add()返回是boolean类型,成功则返回true,否则返回false    
public boolean add(E e) {
        modCount++;
    	//参数e代表我们要添加元素,elementData代表目前集合,size代表当前集合中元素个数
        add(e, elementData, size);
        return true;
    }


//add()增加方法
private void add(E e, Object[] elementData, int s) {
    	//判断集合容量大小是否等于集合元素大小    你可以直接理解为  集合容量大小————>数组长度    集合元素大小————>数组中实际元素个数
        if (s == elementData.length)
            //容量大小等于集合元素大小说明集合容量已经满了,无法添加元素;必须要扩充集合容量大小
            elementData = grow();
    	//将元素e赋值给集合最后一个元素
        elementData[s] = e;
        size = s + 1;
    }


//增加集合容量大小方法
private Object[] grow() {
    	//将size+1
        return grow(size + 1);
    }

//增加集合容量大小
private Object[] grow(int minCapacity) {
    	//将原集合复制到容量大小扩充后的新集合中
        return elementData = Arrays.copyOf(elementData,
                                           //newCapacity()方法判断将集合容量增加长度1后是否满足能够将新元素添加进去
                                           newCapacity(minCapacity));
    }


//进行判断应该以何种方式去扩充集合容量,如果容量为minCapacity能够增加新元素,那么扩充容量为minCapacity
//否则按照扩充至oldCapacity + (oldCapacity >> 1),也就是原来元素的1.5倍
private int newCapacity(int minCapacity) {
        // overflow-conscious code
    	//旧集合长度
        int oldCapacity = elementData.length;
    	//扩充后集合元素,扩充为旧集合元素1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
    	//新集合长度小于需要扩充的最小集合长度大小
    	//可以直接理解为原来集合为空,集合中没有任何一个元素
        if (newCapacity - minCapacity <= 0) {
            //判断集合是否为空
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                //这里就是取出默认集合大小和最小集合长度的值
                //也就是开始给空集合确定大小,默认集合长度为10
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
    
    	//扩充新集合元素小于int类型的最大值,则直接返回新集合元素大小,否则再调用hugeCapacity()方法
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }

      大家是不是看的贼恶心了?你们恶心不恶心我不知道,我就知道我在源码中分析在注解中解释是快要把我恶心死了,好了,给大家直接扔出结论吧,别再让大家恶心了。

      结论: 直接调用add()方法添加元素,它是直接将该元素添加在集合的末尾;这里将会出现个问题,就是当添加这个元素时,这个集合长度恰好不够了怎么办?上面分析了那么多的源码基本上全是围绕这个问题在解决。也就是如何扩充这个元素集合长度大小,其实就是当向集合中添加元素时,会先判断其集合长度大小,当集合长度大小不够时,会再判断该集合是否为空集合,那这时也就是第一次向集合中添加元素,那么这个时候直接将集合长度大小扩容为10;如果不是空集合,那么需要注意还有三点:一是我们需要的集合长度大小 大于扩充后的1.5倍容量,那么我们就直接扩充为我们需要的集合长度;二是我们扩充的1.5倍之后集合容量大小比较大,那么我们直接扩充1.5倍;三是扩充1.5倍之后实在太大了,那么这个时候我们去判断我们需要的集合容量大小是否大于int的最大值,如果大于,那么就直接扩充为int最大值否则扩充为int最大值-8。

      注意: 当别人问ArrayList集合如何扩充时,大家不要再傻傻直接说按照1.5倍扩容了,它是分好几种情况进行扩充的哦。

3.2、addAll()

      废话少说,我直接上源码解析,希望大家耐着性子看完哈:

public boolean addAll(Collection<? extends E> 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))
            //剩余原来容量集合大小不够开始进行集合扩容,和add()方法扩容机制一样,不再详述
            elementData = grow(s + numNew);
    	//将传来新集合添加在原来集合后面
        System.arraycopy(a, 0, elementData, s, numNew);
    	//改变原来集合中元素大小
        size = s + numNew;
        return true;
    }

      addAll()方法目的是将一个集合中所有元素复制到另一个集合中。

      流程:先将传递来的集合转换成数组,然后判断数组长度是否大于原来集合剩余的元素大小,如果大于,说明原来集合不能够完全将新元素复制过来,那么需要将集合容量进行扩容,扩容方法和add()方法扩容是一样的,这里就不再说明;最后直接将新集合复制到旧集合最后一个元素后面即可,记得size也要改变下。

3.3、set()

      set()方法目的是将集合指定位置元素进行修改。

源码如下所示:

//index修改元素位置
public E set(int index, E element) {
    	//检查索引位置是否超过集合元素大小
        Objects.checkIndex(index, size);
        E oldValue = elementData(index);
    	//直接将指定元素修改为新元素
        elementData[index] = element;
        return oldValue;
    }

      set()方法流程:先判断传递过来的索引是否超过集合元素大小,如果超过则报异常;不超过说明索引在元素大小范围内。最后直接将指定元素修改为新元素即可。因为底层是数组,因此set修改非常快。

3.4、get()

      get()方法目的为了获取指定位置元素。

      源码如下所示:

public E get(int index) {
        //判断索引是否越界
        Objects.checkIndex(index, size);
        //直接返回数组位置
        return elementData(index);
    }

//返回数组位置
E elementData(int index) {
        return (E) elementData[index];
    }

      get()方法流程:根据传值过来索引,判断是否越界,越界直接报异常;不越界则直接返回数组位置。因为底层是数组,因此get查询非常快。

3.5、remove()

      remove()方法有两种,一种是传值为索引,那么目的是将该索引位置的元素进行删除;另外一种传值是元素,目的是将该元素从集合中删除。

      传值为索引的remove()方法源码如下所示:

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()复制方法将该索引的元素给跳过不复制,其余全部复制
            System.arraycopy(es, i + 1, es, i, newSize - i);
    	//将集合最后一个元素赋值为null
        es[size = newSize] = null;
    }

      源码中根据索引来删除元素也非常简单:首先就是判断下索引是否越界,不越界的话开始删除该索引位置的元素,根据System.arraycopy()方法将该索引元素成功跳过复制(也就是不复制该索引的元素,其余都复制),最后将集合最后一个位置赋值为null,那么就巧妙的删除了该索引元素。

      传值为元素的remove()方法源码如下所示:

public boolean remove(Object o) {
        final Object[] es = elementData;
        final int size = this.size;
        int i = 0;
        found: {
            //判断要删除的元素是否为null
            if (o == null) {
                for (; i < size; i++)
                    //循环判断集合中元素是否为null
                    if (es[i] == null)
                        break found;
            } else {
                //循环判断集合中元素是否与要删除元素相等
                for (; i < size; i++)
                    if (o.equals(es[i]))
                        break found;
            }
            return false;
        }
    	//删除索引方法
        fastRemove(es, i);
        return true;
    }

//删除索引方法
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;
    }

      源码中根据元素来删除集合中元素是非常巧妙,分析:先判断下删除元素是否为null值,如果是的话,那么开始循环遍历判断集合中元素是否为null值,如果是的话,那么记录下元素索引位置,然后根据System.arraycopy()方法将该索引元素成功跳过复制(也就是不复制该索引的元素,其余都复制),最后将集合最后一个位置赋值为null。这种处理方法真的非常巧妙。实名爱了爱了。

3.6、removeAll()

      removeAll()传值为collection类型,目的是将原来集合与传来的集合所相等的元素全部删除。代码如下所示:

//removeAll方法
public boolean removeAll(Collection<?> c) {
    	//调用批量删除方法
        return batchRemove(c, false, 0, size);
    }

//批量删除方法
boolean batchRemove(Collection<?> c, boolean complement,
                        final int from, final int end) {
    	//判断传来集合是否为null
        Objects.requireNonNull(c);
        final Object[] es = elementData;
        int r;
        // Optimize for initial run of survivors
    	//遍历查询原集合中是否存在和新集合中相等元素,当有相等的,则直接跳出循环
    	//如果没有,则说明两个集合之间没有交集,直接报false
        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;
            //将最后的脏数据设置为null方法
            shiftTailOverGap(es, w, end);
        }
        return true;
    }

//将最后的脏数据设置为null方法
private void shiftTailOverGap(Object[] es, int lo, int hi) {
        System.arraycopy(es, hi, es, lo, size - hi);
    	//将最后脏数据赋值为null数据
        for (int to = size, i = (size -= hi - lo); i < to; i++)
            es[i] = null;
    }

      这个方法的源码我就不带大家一点点分析了,因为这个方法大家用的不太多,源码大家大概知道下就行了,其实也是我太菜,我分析的也是个大概、、、希望大家凑合看看吧。

3.7、retainAll()

      retainAll()是只保留两个集合中相等的元素,也就类似于两个集合的交集。和上面所分享的removeAll()方法正好相反,它是删除两个集合中相等的元素,而retainAll()中是只保留两个相等的元素。源码分析如下所示:

//retainAll方法
public boolean retainAll(Collection<?> c) {
    	//调用批量删除方法
        return batchRemove(c, true, 0, size);
    }

//批量删除方法
boolean batchRemove(Collection<?> c, boolean complement,
                        final int from, final int end) {
    	//判断传来集合是否为null
        Objects.requireNonNull(c);
        final Object[] es = elementData;
        int r;
        // Optimize for initial run of survivors
    	//遍历查询原集合中是否存在和新集合中相等元素,当有相等的,则直接跳出循环
    	//如果没有,则说明两个集合之间没有交集,直接报false
        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;
            //将最后的脏数据设置为null方法
            shiftTailOverGap(es, w, end);
        }
        return true;
    }

//将最后的脏数据设置为null方法
private void shiftTailOverGap(Object[] es, int lo, int hi) {
        System.arraycopy(es, hi, es, lo, size - hi);
    	//将最后脏数据赋值为null数据
        for (int to = size, i = (size -= hi - lo); i < to; i++)
            es[i] = null;
    }

      这个方法的源码和上面的removeAll方法源码基本上是一样的,唯一一点不一样的是在调用批量删除的方法中的第二个传值为true,正好这里和remiveAll是相反的,得到的结果是相反的。

3.8、indexOf()

      indexOf()方法是查找传值元素的索引位置,源码也比较简单。源码如下所示:

//index()方法
public int indexOf(Object o) {
    	//遍历查找元素位置
        return indexOfRange(o, 0, size);
    }

//遍历查找元素位置方法
int indexOfRange(Object o, int start, int end) {
        Object[] es = elementData;
    	//判断查找元素是否为null
        if (o == null) {
            //逐个遍历进行判断是否为null,采用==进行判断
            for (int i = start; i < end; i++) {
                if (es[i] == null) {
                    //直接返回索引位置
                    return i;
                }
            }
        } else {
            //逐个判断元素是否与传值元素相等,底层采用的是equals方法进行判断
            for (int i = start; i < end; i++) {
                if (o.equals(es[i])) {
                    //直接返回索引位置
                    return i;
                }
            }
        }
    	//如果查找内容不存在,则直接返回-1
        return -1;
    }

      从上层源码中可以发现,查找元素索引位置底层采用的是遍历集合比较的方法进行查找。

3.9、contains()

      contains()方法是判断集合中是否包含这个元素,返回类型为boolean类型。源码如下所示:

//contains()方法
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;
    }

      从上面源码中可以发现它和上面查找索引位置indexOf()方法是基本一样的,只不过它去判断下索引位置是否大于0,如果大于0则说明集合中存在该元素,返回为true;如果<0,则说明集合中不存在该元素,返回为false。

3.10、clear()

      clear()方法目的是清空集合,其源码如下所示:

//clear()方法
public void clear() {
        modCount++;
        final Object[] es = elementData;
    	//循环遍历元素,将每个元素设置为null
        for (int to = size, i = size = 0; i < to; i++)
            es[i] = null;
    }

4、集合三种遍历方式

for (int i = 0; i < list.size(); i++) {
    int elem = (int) list.get(i);
    System.out.println(i + "------->" + elem)
}

这种for循环比较常用,也不过多描述

for (Object elem : list) {
    System.out.println(elem);
}

for-each同样也是比较常用,也不过多描述。

Iterator iterator = list.iterator();
while (iterator.hasNext()) {//是否还有元素
    //取出下一个元素
    Object next = iterator.next();
    //删除这个迭代元素
    iterator.remove();
    System.out.println(next);
}

      这个稍微比较重要,它采用迭代器循环,先从list中获取迭代对象,然后判断迭代对象中下一个元素是否存在,如果存在则可以利用next()方法获取该迭代元素,remove()方法删除该迭代元素。

      注意: 集合循环删除元素必须使用迭代器循环删除,如果使用其他循环则会出现异常。具体原因分析,请看以前专门所写的foreach遍历集合删除元素抛异常文章。

      总结: ArrayList底层采用数组类型,查询、修改效率高,但是添加和删除效率比较低。

      好了,这篇吐血源码分析就写到这吧,一下子写了六千多字。拜拜啦,有哪些不正确的内容希望大家在评论区中给指出哦,还希望大家动动手指来个一键三连啦。