java---ArrayList源码解析

目录 

一、ArrayList概述

1.ArrayList是什么

2.顺序表的优缺点

二、源码分析

1.ArrayList声明

2.ArrayList属性 

3.ArrayList构造方法 

3.1无参构造

3.2传入初始容量构造 

3.3传入集合构造 

4.扩容机制

4.1 ensureCapacityInternal(int minCapacity) 

4.1.1 ensureExplicitCapacity(minCapacity)

4.1.2 grow(minCapacity)

4.2 rangeCheckForAdd(index)

add(E e)

add(int index, E element)

addAll(Collection c) 

addAll(int index, Collection c)

5.set方法

6.get方法 

7.remove(int index) 

三、elementData数组被修饰transient问题

总结


一、ArrayList概述

1.ArrayList是什么

ArrayList 底层就是一个数组,依赖其扩容机制(后面会提到)它能够实现容量的动态增长,所以 ArrayList 就是数据结构中顺序表的一种具体实现。

其特点为:查询快,增删慢,线程不安全,效率高。

2.顺序表的优缺点

优点:

  1. 逻辑与物理顺序一致,顺序表能够按照下标直接快速的存取元素
  2. 无须为了表示表中元素之间的逻辑关系而增加额外的存储空间

缺点:

  1. 线性表长度需要初始定义,常常难以确定存储空间的容量,所以只能以降低效率的代价使用扩容机制
  2. 插入和删除操作需要移动大量的元素,效率较低

二、源码分析

1.ArrayList声明

public class ArrayList extends AbstractList
        implements List, RandomAccess, Cloneable, java.io.Serializable
{ 
    // 源码具体内容... 
}
  • ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
  • ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
  • ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
  • ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
     

2.ArrayList属性 

// 序列化自动生成的一个码,用来在正反序列化中验证版本一致性。
private static final long serialVersionUID = 8683452581122892189L;

/**
 * 默认初始容量大小为10
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 指定 ArrayList 容量为0(空实例)时,返回此空数组
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 与 EMPTY_ELEMENTDATA 的区别是,它是默认返回的,而前者是用户指定容量为 0 才返回
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 具体存放元素的数组
 * 保存添加到 ArrayList 中的元素数据(第一次添加元素时,会扩容到 DEFAULT_CAPACITY = 10 ) 
 * transient:表述序列化的时候该修饰符修饰的属性不被序列化
 */

transient Object[] elementData; // non-private to simplify nested class access

/**
 * ArrayList 实际所含元素个数(大小)
 */
private int size;

3.ArrayList构造方法 

ArrayList向我们提供了三种构造器:

无参构造器:public ArrayList()
带初始容量构造器:public ArrayList(int initialCapacity)
带集合参数的构造器:public ArrayList(Collection c)

3.1无参构造

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

默认无参构造函数,初始值为 0 也说明 DEFAULT_CAPACITY = 10 这个容量不是在构造函数初始化的时候设定的(而是在添加第一个元素的时候) 

3.2传入初始容量构造 

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 参数大于0,创建 initialCapacity 大小的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 参数为0,创建空数组(成员中有定义)
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 其他情况,直接抛异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

从上面两个构造器来看:

空参的时候ArrayList中的数组是它:DEFAULTCAPACITY_EMPTY_ELEMENTDATA
容量是 0 的时候ArrayList中的数组是它:EMPTY_ELEMENTDATA
那么问题就简化到空容量和0容量的问题了,有的人会说这不一样的嘛!有什么好区别的。

其实不是的,这两个的区别还是蛮大的,我们一贯的思维就是不传值就是空容量数组,传值就是对应的容量数组,那我们有没有想过如果一个人他就是想创建一个容量为 0 的数组,而不是一来就给我默认扩容到 10 这个容量。

所以我们可以得一个结论,DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA就是在扩容得时候区别出来到底是扩容为 10 还是从 0 开始一步步得扩容。

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 判断ArrayList中的数组是哪种类型
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // DEFAULTCAPACITY_EMPTY_ELEMENTDATA类型,直接扩容到 10 
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // EMPTY_ELEMENTDATA类型,从 0 开始一步步扩容上去
    return minCapacity;
}

3.3传入集合构造 

public ArrayList(Collection c) {
    // 将传入得集合变成数组,赋值给ArrayList的数组
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // 将类型转为Object然后再次调用copyOf进行赋值
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 传入的是空的集合,那么赋值一个容量为EMPTY_ELEMENTDATA类型的空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

注意:

if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);

明明已经赋过值了elementData = c.toArray(),那为什么还要整出上面的一个逻辑?

其实关键在于toArray()方法,它返回的不是一个 Object[] ,而是 E[] 类型,意味着如果不转成 Object[] ,你想某个位置add一个Object的子类时,这个时候就会出现异常。


public void demoTest03() {
    // 字符串类型
    String[] strings = new String[8];
    strings[0] = "a";
    strings[1] = "b";
    // 向上转型为Object类型
    Object[] objects = strings;
    // 定义一个Object类型数据
    Object obj = 1;
    // 赋值给Object类型数组,在这会出错:java.lang.ArrayStoreException
    objects[3] = obj;
    System.out.println(Arrays.toString(objects));
}

总结:该代码的功能就是将elementData数组中的所有元素变为Object类型,防止在向ArrayList中添加数据的时候抛错 。

4.扩容机制

ArrayList 提供了 1 个无参构造和 2 个带参构造来初始化 ArrayList ,我们在创建 ArrayList 时,经常使用无参构造的方式,其本质就是初始化了一个空数组,直到向数组内真的添加元素的时候才会真的去分配容量。例如:向数组中添加第一个元素,数组容量扩充为 10。

一般来说,都是通过 add 方法触发扩容机制。

ArrayList中向我们提供了四种添加元素的方法

向末尾添加元素:public boolean add(E e)
指定位置添添加元素:public void add(int index, E element)
添加一个集合元素:public boolean addAll(Collection c)
在指定位置添加集合元素:public boolean addAll(int index, Collection c)
 

而在ArrayList中这些主要的实现就是下面两个方法

ensureCapacityInternal(int minCapacity):数组容量判断,容量够就不做处理,容量不足就进行相应的扩容
rangeCheckForAdd(index): 检查下标时候合理,如果合理不做处理,否则抛出异常
 

4.1 ensureCapacityInternal(int minCapacity) 

//确定数组容量
private void ensureCapacityInternal(int minCapacity) {
       //如果数组默认是空数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

elementData:存放元素的数组

minCapacity:可以放下元素的最小的容量

 该方法的功能就是确定数组的容量,空数组就是 10 ,数组不空就是数组中元素个数加 1。 

4.1.1 ensureExplicitCapacity(minCapacity)

// 进行相应的扩容
private void ensureExplicitCapacity(int minCapacity) {
    // 数组修改次数加一
    modCount++;

    // 计算的最小容量是否大于数组的长度
    if (minCapacity - elementData.length > 0)
        // 扩容
        grow(minCapacity);
}

可以看出,该方法主要是判断其内部的数组是否允许再添加元素,如果容量不够则进行扩容从而保证元素的正常添加而不溢出。

4.1.2 grow(minCapacity)

// 真正扩容方法
private void grow(int minCapacity) {
    // 获取数组的长度
    int oldCapacity = elementData.length;
    // 计算新得长度,新长度为旧长度的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 判断计算的新长度与传入的最小容量的大小
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 开始扩容
    elementData = Arrays.copyOf(elementData, newCapacity);
}

从这个方法,我们就可以知道如果ArrayList中如果数组容量不足,则会扩容到原来的1.5倍,而具体的扩容操作这是要看这个方法Arrays.copyOf(elementData, newCapacity)的具体实现了。 

public static  T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

在调用下面的:

// 扩容方法的具体实现
public static  T[] copyOf(U[] original, int newLength, Class newType) {
    // 创建指定长度的某种类型的数组。
    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()作用就是将旧数组元素移动到新数组中,那为什么用非Java所写的C++方法呢!我想应该就是为了追求效率,因为扩容会移动很多元素用C++显然是比较快的。

java---ArrayList源码解析_第1张图片

4.2 rangeCheckForAdd(index)

private void rangeCheck(int index) {
	// 如果传入的下标大于等于数组中的元素个数,溢出
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

下面是具体方法分析

add(E e)

public boolean add(E e) {
    // 确保数组容量
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 在数组末尾添加元素
    elementData[size++] = e;
    // 返回添加成功
    return true;
}

 ensureCapacityInternal(size + 1)这个方法我已经分析过了,它会确保我们添加元素的时候容量是充足的,然后就会直接添加元素到数组末尾,最后再返回成功标识。

在这里,我们也可以解释ArrayList为什么可以添加重复的值并且输出的值与我们输入的值顺序一致的问题。

add(int index, E element)

public void add(int index, E element) {
    // 1. 检查下标
    rangeCheckForAdd(index);
	// 2. 保证容量
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 3. 开始移动元素,空出指定下标的位置出来
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 4. 在指定下标出赋值
    elementData[index] = element;
    // 5. 数组元素值加 1
    size++;
}

addAll(Collection c) 

// 添加一个集合到数组中
public boolean addAll(Collection 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;
}

最后返回 numNew != 0 也就是当传入的集合是空的时候,返回false。 

addAll(int index, Collection c)

public boolean addAll(int index, Collection c) {
    // 1. 检查下标
    rangeCheckForAdd(index);
	// 2. 将集合转为数组
    Object[] a = c.toArray();
    // 3. 获取数组长度
    int numNew = a.length;
    // 4. 保证容量
    ensureCapacityInternal(size + numNew);  // Increments modCount
	// 5. 计算需要移动元素的开始下标
    int numMoved = size - index;
    if (numMoved > 0)
        // 6. 开始移动元素
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);
	// 7. 开始向目标数组中添加元素
    System.arraycopy(a, 0, elementData, index, numNew);
    // 8. 设置元素个数
    size += numNew;
    // 9. 返回结果
    return numNew != 0;
}

5.set方法

public E set(int index, E element) {
    // 检查下标
    rangeCheck(index);
	// 获取对应下标数据
    E oldValue = elementData(index);
    // 在对应下标处赋值
    elementData[index] = element;
    // 返回原始数据
    return oldValue;
}
private void rangeCheck(int index) {
    // 下标是否大于ArrayList中的元素个数
    if (index >= size)
        // 下标越界错误
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

只要是了解了add的这个就很简单了。

6.get方法 

public E get(int index) {
    // 检查下标
    rangeCheck(index);
	// 返回对应下标值
    return elementData(index);
}

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

三、elementData数组被修饰transient问题

我们知道ArrayList是支持序列化的,那为什么其中关键的存储元素的数组要被修饰成transient(序列化时忽略该数组),矛盾了。

其实不然,我们点进源码可以发现,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

ArrayList底层是基于动态数组实现的,数组的长度是动态变化的,当数组的长度扩容到很大的时候,其中的元素却是寥寥几个的话,那要是将这些没有用的空元素也序列化到内存中就有点非内存了。所以就是考虑到这一点,ArrayList才会自己实现一套序列化标准,只序列化有用的元素,这样可以节省空间。
 


总结

加油哦~~

你可能感兴趣的:(java,数据结构,面试)