Java 集合(二)| List和ArrayList源码解析

一、List接口

简介


List:有序集合(也称为序列 )。 用户使用该接口可以精确控制List中每个元素的插入位置。 用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。

List是Collection的子接口,其最大的特点是允许保存有重复数据元素,该接口的定义如下:

public interface List<E> extends Collection<E> {...}

但是需要清楚的是List子接口对Collection接口进行了方法的扩充,最显而易见的是在方法中有了索引index

重要的扩充方法


方法名说明
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< E> listIterator();返回该List的LiistIterator迭代器

但是List本身依然属于一个接口,那么对于接口要想使用则一定要使用子类来完成定义

三个重要子类


ArrayList、LinkedList、Vector

继承图解


在这里插入图片描述

二、ArrayLIst(JDK1.8)

1.继承关系

ArrayList的类定义


public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输
  • 实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问
  • 实现了Cloneable接口,能被克隆

2.核心属性

    //默认初始化容量
    private static final int DEFAULT_CAPACITY = 10;

    //用于指定初始化容量为0时的数组实例
    private static final Object[] EMPTY_ELEMENTDATA = {};

	//用于调用默认无参构造方法时的数组实例
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    //存储ArrayList元素的数组缓冲区。
    transient Object[] elementData; 
	
	//ArrayList内的元素个数
    private int size;

EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA的区别是:前者是在指定初始化ArrayList容量为0时用到的空数组实例,而后者是在未指定初始化ArrayList容量时,即调用无参构造方法时用到的空数组实例,这些空数组实例最终都会由emementData所引用,具体下面会讲到

3.构造器

无参构造


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

当调用无参构造方法时,缓存数组elementData将会指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA,虽然此时是一个空Object型数组,但在第一次调用add()方法时,数组的容量将会设置为默认容量DEFAULT_CAPACITY,即10,这里在后面分析add()方法时会讲到

有参构造


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时会抛出异常
  • 当指定初始化容量initialCapacity等于0时,会将elementData指向EMPTY_ELEMENTDATA,一个空数组对象,这里在第一次调用add()的时候,容量不会设置为10,这就是和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的一个区别
  • 当指定初始化容量initialCapacity大于于0时,会初始化一个新的容量为initialCapacity的Object型数组,并将elementData指向它

4.add方法(自动扩容分析)

add(E e) 方法源码


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

add()方法会在ArrayList末尾添加指定元素,首先进行容量判断,如果容量足够,则将指定元素赋值给size的索引位置(因为索引从0开始),再将size数加1,并将最后返回添加成功标志

后面来分析一下容量判断:

ensureCapacityInternal(int minCapacity) 方法源码


其中的参数minCapacity就是上一步的size+1,即我们需要的最小容量

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

可以看到这个方法内部会调用两个方法:ensureExplicitCapacity()、calculateCapacity(),我们一个一个分析:

calculateCapacity(Object[] elementData, int minCapacity) 方法源码


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

这个方法其实很简单,就是计算我们需要的容量,它会判断我们是不是调用的无参构造方法

  • 如果是的话,那么elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA会指向同一块内存空间,所以进行==运算为true,返回的是默认容量DEFAULT_CAPACITY和传过来的minCapacity中的最大值,第一次调用肯定是默认容量最大,所以我们第一次调用add()方法会将我们所需的容量置为默认容量10后面的真正扩容方法grow会将生成的新容量也置为10,即第一次调用add方法后,数组扩容后的容量即为10,可以自行验证),第二次及以后调用add()方法这里的判断条件就为false了,因为后面的扩容会生成新的数组
  • 如果不是的话,会直接返回传过来的参数minCapacity

接下来这个方法就是判断是否要扩容:

ensureExplicitCapacity(int minCapacity)源码


其中的参数minCapacity还是我们需要的最小容量

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
  • 如果minCapacity大于了elementData数组的长度,那么就需要扩容,调用grow()方法进行扩容
  • 否则,容量足够,不进行扩容处理

关于modCount:这是AbstractList类中的一个属性,modCount表示了当前列表结构被修改的次数,在调用迭代器操作时,则会检查这个值,如果发现已更改,抛出异常(即一边迭代一边修改可能会抛出异常),具体参考:https://www.cnblogs.com/NextLight/p/9592069.html

一个扩容需要的函数是真的多,不过貌似JDK1.8之后源码做了简化,实际就是多个方法合在了一起,扯远了,接下来看一下真正的扩容处理:

grow(int minCapacity)方法源码


其中的参数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);
    }
  • 首先获得原来的容量oldCapacity:elementData.length
  • 然后获得新容量newCapacity:原来的容量+原来容量÷2(移位运算效率更高)
  • 新容量可能还是没有满足需要的容量,也有可能超过了Integer.MAX_VALUE,所以要和需要的容量进行比较,取较大的那一个赋值给newCapacity(超过了Integer.MAX_VALUE的会是负数)
  • 再将newCapacity和MAX_ARRAY_SIZE比较,看是否超过了最大数组容量,其中MAX_ARRAY_SIZE的值为Integer.MAX_VALUE-8,如果大于,那么会执行hugeCapacity()方法,这个方法后面介绍
  • 最后便确定了新容量newCapacity是多少,然后将原来的数组(elementData)中的数据调用Arrays.copyOf()方法拷贝到一个新的数组中,新的数组长度为newCapacity(生成新数组的操作在Arrays.copyOf()方法内部,源码就不贴了,感兴趣的可以看看)

接下来看看hugeCapacity()方法,实际上走到这一步的很少,但还是分析一波:

hugeCapacity(int minCapacity)方法源码


其中参数minCapacity是我们需要的容量

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

这个方法作用就是返回最大的容量

  • 如果minCapacity大于MAX_ARRAY_SIZE九返回Integer.MAX_VALUE
  • 否则直接返回MAX_ARRAY_SIZE

至此,add()方法分析完毕,果然是又臭又长,可该分析的还是得分析,梳理下来会发觉还是挺简单的,由于是自己对源码的解读,有可能不正确,特别是对于数据溢出的部分,仅供参考


总结一下

  • 如果是调用的默认无参构造方法,那么第一次调用add()方法后,缓存数组elementData的容量将扩容为10
  • 首先进行扩容判断:传入我们需要的最小容量–>将调用无参构造所生成的默认数组容量置为10(只有第一次调用add的时候会走这一步)–>将所需的最小容量和数组的当前长度比较来判断是否要扩容–>如果要扩容,则将新容量置为原来容量的3/2–>再次比较所需容量和新容量的大小–>如果新容量大于所需容量,并且小于数组所规定的最大长度,则进行数组拷贝,相当于将数组容量改为新容量–>扩容成功
  • 扩容成功后,将指定的元素赋值在size索引处,而后size加1

此外,还有add的重载方法,它加入了索引,简单介绍一下

add(int index, E 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++;
    }

跟上面的add(E e)方法不同的是,它的形参多了个index索引,相当于在指定索引添加指定元素,与其说是添加不如说是插入

源码方面,大同小异:

  • 先调用rangeCheckForAdd(index)检查索引是否合法,不合法抛出异常
  • 接着进行扩容判断,跟上面的毫无差别
  • 扩容判断完成后,然后便是插入,这里是用了数组的拷贝,实现的效果是将index索引开始的元素向后移一位,然后将element元素赋值在index位置上

5.其他常用方法

1)public E remove(int index)


删除指定索引位置的元素

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;
    }
  • rangeCheck(index)检查索引是否超过了size,超过则抛出异常,注意这里与上面的rangeCheckForAdd(index)方法不同的是,这里不会检查index为负的情况,因为后面elementData(index)会在index为负时抛出数组越界异常
  • 将要删除的值存储在oldValue中
  • 计算需要移动的位数
  • 如果位数大于0,则调用数组拷贝,将index+1位置及后面的元素前移一位
  • 由于删除了一个元素,所以ArrayList的size减1,并且由于整体向前移了1位,所以最后一位元素为垃圾元素(如果删除的是最后一个,那么不会前移,但最后一位还是垃圾元素),因此将最后一个元素置为null,利于垃圾回收
  • 最后返回已经删除的元素

2)public boolean remove(Object o)


删除指定元素

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;
    }
  • 这里的删除是删除线性查找所找到的第一个指定元素
  • 可以删除为null的对象
  • 注意用的是Object的equals方法,即比较对象的地址,如果有需求,可以重写equals方法,以此来改变remove的作用
  • fastRemove(index)方法的内容和上面的1)中的remove(index)方法大致相同,就是少了索引检查,也不会返回oldValue
  • 如果要删除的元素存在,则返回true,否则返回false

3)public void clear()


清空ArrayList中的元素

public void clear() {
        modCount++;

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

        size = 0;
    }

源码很简单,就是将所有元素置为null,size置为0

4)public boolean addAll(Collection<? extends E> c)


将Collection对象中的元素全部添加到ArrayList中

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;
    }
  • 先将Collection转化为数组,得到数组的长度
  • 扩容分析
  • 数组拷贝
  • size增加数组的长度
  • 返回最终结果是否影响了ArrayList的标志

5)public E get(int index)


得到指定索引的元素

public E get(int index) {
        rangeCheck(index);
        
        return elementData(index);
    }

6)public E set(int index, E element)


设置指定索引处的元素值

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

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

7)public boolean contains(Object o)


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

没作分析的都是一眼可以看懂的,就不浪费篇幅了

6.特点

  • ArrayList基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低
  • ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长
  • ArrayList不是线程安全的,确保线程安全可以用Vector,这也是两者的重要区别(除此之外,Vector和ArrayList除了扩容的机制有些许不同外,其他几乎没有什么区别)
  • 通过remove和contains方法可以看出,ArrayList的元素可以存储null

— —事常与人违,事总在人为

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值