听说你还没搞懂ArrayList底层源码?

听说你还没搞懂ArrayList底层源码?

1、概述ArrayList

ArrayList底层数据结构是一个Oject类型的动态数组,可以自动扩容;线程不安全

ArrayList继承于 AbstractList ,实现了 List(规定了List的操作规范), RandomAccess(可随机访问), Cloneable(可拷贝), java.io.Serializable(可序列化) 这些接口。

  • RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
  • ArrayList 实现了 Cloneable 接口 ,即覆盖了函数clone(),能被克隆。
  • ArrayList 实现了 java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。

Arraylist 和 Vector 的区别?

  • ArrayListList 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ;
  • VectorList 的古老实现类,底层使用 Object[ ]存储,线程安全的

Arraylist 与 LinkedList 区别?

  • 线程安全性: ArrayList 和 LinkedList 都是不保证线程安全;
  • 底层数据结构: Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
  • 插入和删除操作:
    • ArrayList 采用数组存储,所以元素的索引位置直接影响插入和删除元素的时间复杂度。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
    • LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。
      是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  • 内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)

2、ArrayList源码分析

2.1、ArrayList主要的成员变量

// ArrayList底层数组结构。用来存放数据,用transient来修饰,说明该字段不会被序列化
transient Object[] elementData;
// 默认容量。当构造方法中没有显式指出ArrayList的数组长度是,使用默认容量
private static final int DEFAULT_CAPACITY = 10; 
// 空数组。当构造方法中显式指出ArrayList的数组长度为0时,将该数组赋给elemetData[]
private static final Object[] EMPTY_ELEMENTDATA = {}; 
// 空数组。当构造方法中没有显示指出ArrayList的数组长度时,将该数组赋给elemetData[]
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 实际ArrayList中存放元素的个数,默认为0
private int size;
// ArrayList中的对象数组的最大数组容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
  • java 中的 length属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.
  • java 中的 length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法.
  • java 中的 size() 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!

2.2、ArrayList的构造方法

  • 空参构造器
// 将elementData的值设为空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
public ArrayList() {
    // 这里并没有将其数组初始化为容量为10的Object数组!!!
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

JDK7 new无参构造的ArrayList对象时,直接创建了长度是10的 Object[]数组 elementData 。JDK7中的 ArrayList 的对象的创建类似于单例的饿汉式,而JDK 8中的 ArrayList 的对象的创建类似于单例的懒汉式

  • int参数构造器
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 容量参数大于0,对数组进行初始化
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 容量参数为0,将数组设为空数组
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 容量参数小于0,报异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
  • Collection参数构造器
public ArrayList(Collection<? extends E> c) {
    // 底层利用Arrays.copyOf(elementData, size)创建一个长度和内容相同的新数组
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        // 如果a是非空集合
        if (c.getClass() == ArrayList.class) {
            // 如果可以转换为Object类型数组,直接赋值给elementData
            elementData = a;
        } else {
            // 如果不可以转为Object数组,则进行copy
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // replace with empty array.空集合直接赋值为空
        elementData = EMPTY_ELEMENTDATA;
    }
}

2.3、ArrayList中的主要方法

2.3.1、add()方法

添加元素,添加成功则为true,否则为false。默认添加到末尾

三步走

  • 判断ArrayList容量是否足以添加新元素,不够则进行扩容(详细见扩容机制);
  • 添加元素到指定位置;
  • 集合中的实际元素个数size+1。
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
2.3.2、扩容机制
// 首先在add()中传参size+1确保添加元素之后最小容量为minCapacity(即添加元素后的实际元素个数)
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 判断是否为空参所赋值的空数组
    // 若用户构造器传入initialCapacity = 0时,不会扩容为10,只会扩容为1
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //若是空数组,则直接返回默认数组容量与minCapacity之间的最大值
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 不为空数组时,返回最小容量
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
    // 当最小容量已经超过了elementData的长度时,需要进行扩容
        grow(minCapacity);
}

grow(int minCapacity)扩容方法

ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)

private void grow(int minCapacity) {
    // overflow-conscious code
    // 原数组的容量
    int oldCapacity = elementData.length;
    // 位运算远远快于整除运算!!!
    // 新数组的容量。相当于扩容1.5倍!!!
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        // 如果扩容后的数组容量仍小于最小容量,则直接将最小容量赋值给新数组容量
        newCapacity = minCapacity;
    // 比较新数组容量与MAX_ARRAY_SIZE大小
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        // 如果新数组容量大于MAX_ARRAY_SIZE,调用hugeCapacity(minCapacity)方法
        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) {
    // 容量小于0,抛出异常
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    // 比较最小容量与数组最大容量,若大于最大容量,则返回Integer.MAX_VALUE,否则返回最大数组容量
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}
2.3.3、add(index,element)方法

将元素添加到某一位置。相当于插入操作

public void add(int index, E element) {
    // 边界检查
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将原始数组进行拷贝扩容,将index开始的数据拷贝到index+1位置
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element;
    size++;
}
2.3.4、indexOf(Object o)方法

查询传入参数的从左往右的第一个索引值并返回,不存在则返回-1

public int indexOf(Object o) {
    // 逐个遍历,需要进行equals比较。所以要进行null值判断
    if (o == null) {
        // 逐个遍历是否为null。找到第一个为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;
    }
    // 不存在时返回-1
    return -1;
}
2.3.5、remove(index)方法

移除某一索引上的元素,返回移除值

public E remove(int index) {
    // 边界检查
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
	// 需要移动的元素的个数(移除元素之后的元素需要向前移动)
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 若大于0,则不是末尾元素,则进行copy到新的数组(相当于将后面元素向前平移一位)
        // 此时,末尾元素重复
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 将末尾元素赋值为null
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}
2.3.6、set(index, element)

将某一索引位置的数据进行修改,返回原来值

public E set(int index, E element) {
    rangeCheck(index);
    // 取出原来值
    E oldValue = elementData(index);
    // 将该索引下的元素赋予新值
    elementData[index] = element;
    return oldValue;
}
2.3.7、get(index)

查找某一索引下的元素,并返回

public E get(int index) {
    // 检查边界
    rangeCheck(index);
	// 直接利用索引值对数据进行索引查找,并返回
    return elementData(index);
}
2.3.8、ensureCapacity(minCapacity)方法

该方法在ArrayList类内没有主动进行调用,所以是提供给用户使用的。用户可以在add大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数

public void ensureCapacity(int minCapacity) {
    // 如果是true,minExpand的值为0,如果是false,minExpand的值为10
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0: DEFAULT_CAPACITY;
    // 如果最小容量大于已有的最大容量
    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}
public class ArrayListTest {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        final int N = 10000000;
        long startTime1 = System.currentTimeMillis();
        for (int i = 0; i < N; i++) {
            list.add(i);
        }
        long endTime1 = System.currentTimeMillis();
        System.out.println("没有使用ensureCapacity方法:"+(endTime1 - startTime1));
        list = new ArrayList<>();
        startTime1 = System.currentTimeMillis();
        list.ensureCapacity(N);
        for (int i = 0; i < N; i++) {
            list.add(i);
        }
        endTime1 = System.currentTimeMillis();
        System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1));
    }
}
// 没有使用ensureCapacity方法:2617
// 使用ensureCapacity方法后:1020
// 性能提高

2.4、ArrayList中两种copy方法

2.4.1、System.arraycopy() 方法

native方法

/* 
src: 源数组
srcPos: 复制源数组的起始位置
dest: 目标数组
destPos: 目标数组的起始下标位置
length: 需要复制的长度
*/
public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos, int length);
2.4.2、Arrays.copyOf()方法

Arrays工具类的方法。底层仍调用了System.arraycopy()方法

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    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;
}
2.4.3、比较两种copy方法

联系:

  • Arrays.copyOf()方法底层仍调用了System.arraycopy()方法

区别:

  • arraycopy()需要目标数组,可以选择要拷贝的部分
  • copyOf()是内部定义了一个数组,并从索引为0的位置开始拷贝,最后返回该拷贝的数组

3、ArrayList的优缺点

3.1、优点

  • ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
  • ArrayList 在顺序添加一个元素的时候非常方便。
  • 根据下标遍历元素,效率高。
  • 根据下标访问元素,效率高。

3.2、缺点

  • 插入和删除元素的效率不高。
  • 根据元素查找元素索引需要遍历整个元素数组,效率不高。
  • 线程不安全。

4、【补充知识】RandomAccess 接口

该接口源码中没有任何定义,则推断该接口仅为标识作用,代表该实现类具有随机访问功能

public interface RandomAccess {
}

ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值