ArrayList的底层原理

ArrayList简介

ArrayList 是我们开发中非常常用的数据存储容器之一,其底层是数组实现的,我们可以在集合中存储
任意类型的数据, ArrayList 是线程不安全的,非常适合用于对元素进行查找,效率非常高。

1.ArrayList的数据结构

ArrayList 的底层数据结构就是一个数组,数组元素的类型为 Object 类型,对 ArrayList 的所有操作底层都
是基于数组的。

2.ArrayList的线程安全性

ArrayList 进行添加元素的操作的时候是分两个步骤进行的,即第一步先在 object[size] 的位置上存放
需要添加的元素;第二步将 size 的值增加 1 。由于这个过程在多线程的环境下是不能保证具有原子性
的,因此 ArrayList 在多线程的环境下是线程不安全的。
具体举例说明:在单线程运行的情况下,如果 Size = 0 ,添加一个元素后,此元素在位置 0 ,而且
Size=1 ;而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0 。但是此时 CPU
度线程 A 暂停,线程 B 得到运行的机会。线程 B 也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0
(注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程 A 仅仅完成了步骤 1 ),所以线程 B 也将
元素存放在位置 0 。然后线程 A 和线程 B 都继续运行,都增 加 Size 的值。 那好,现在我们来看看
ArrayList 的情况,元素实际上只有一个,存放在位置 0 ,而 Size 却等于 2 。这就是 线程不安全 了。
如果非要在多线程的环境下使用 ArrayList ,就需要保证它的线程安全性,通常有两种解决办法:第一,
使用 synchronized 关键字;第二,可以用 Collections 类中的静态方法 synchronizedList(); ArrayList
行调用即可。

3.ArrayList的实现

对于 ArrayList 而言,它实现 List 接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。下
面我们来分析 ArrayList 的源代码:
1) 私有属性:
ArrayList 定义只定义类两个私有属性:
/** 
    * The array buffer into which the elements of the ArrayList are stored.
    * The capacity of the ArrayList is the length of this array buffer. */ 
private transient Object[] elementData; 
/**
    * The size of the ArrayList (the number of elements it contains). 
    *
    * @serial 
*/ 
private int size;
elementData 存储 ArrayList 内的元素, size 表示它包含的元素的数量。
有个关键字需要解释: transient
Java serialization 提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据
成员,我们不想用 serialization 机制来保存它。为了在一个特定对象的一个域上关闭 serialization ,可以
在这个域前加上关键字 transient
有点抽象,看个例子应该能明白。
public class UserInfo implements Serializable { 
    private static final long serialVersionUID = 996890129747019948L; 
    private String name; private transient String psw; 
    public UserInfo(String name, String psw) { 
        this.name = name; 
        this.psw = psw; 
    }
    public String toString() { 
        return "name=" + name + ", psw=" + psw; 
    } 
}
public class TestTransient { 
    public static void main(String[] args) { 
        UserInfo userInfo = new UserInfo("张三", "123456"); 
        System.out.println(userInfo); 
        try {
            // 序列化,被设置为transient的属性没有被序列化 
            ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream( "UserInfo.out")); 
            o.writeObject(userInfo); 
            o.close(); 
    } catch (Exception e) { 
        // TODO: handle exception e.printStackTrace();

    }
    try {
        // 重新读取内容 
        ObjectInputStream in = new ObjectInputStream(new FileInputStream( "UserInfo.out")); 
        UserInfo readUserInfo = (UserInfo) in.readObject(); 
        //读取后psw的内容为null 
        System.out.println(readUserInfo.toString()); 
    } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } }
被标记为 transient 的属性在对象被序列化的时候不会被保存。
回到 ArrayList 的分析中
2) 构造方法:
ArrayList 提供了三种方式的构造器,可以构造一个默认初始容量为 10 的空列表、构造一个指定初始容
量的空列表以及构造一个包含指定 collection 的元素的列表,这些元素按照该 collection 的迭代器返回它
们的顺序排列的。
    // ArrayList带容量大小的构造函数。 
    public ArrayList(int initialCapacity) { 
        super(); 
        if (initialCapacity < 0) 
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); 
        // 新建一个数组 
        this.elementData = new Object[initialCapacity]; 
    }
    // ArrayList无参构造函数。默认容量是10。 
    public ArrayList() { this(10); }
    // 创建一个包含collection的ArrayList 
    public ArrayList(Collection<? extends E> c) { 
        elementData = c.toArray(); 
        size = elementData.length; 
        if (elementData.getClass() != Object[].class) 
            elementData = Arrays.copyOf(elementData, size, Object[].class); 
    }
3) 元素存储:
ArrayList 提供了 set(int index, E element) add(E e) add(int index, E element)
addAll(Collection<? extends E> c) addAll(int index, Collection<? extends E> c) 这些添加元素的方
法。下面我们一一讲解:
// 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。 
public E set(int index, E element) {
    RangeCheck(index); 
    E oldValue = (E) elementData[index]; 
    elementData[index] = element; return oldValue;
}
// 将指定的元素添加到此列表的尾部。 
public boolean add(E e) { 
    ensureCapacity(size + 1); 
    elementData[size++] = e; 
    return true; 
}
// 将指定的元素插入此列表中的指定位置。 
// 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。 
public void add(int index, E element) { 
    if (index > size || index < 0) 
        throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size); 
    // 如果数组长度不足,将进行扩容。 
    ensureCapacity(size+1); 
    // Increments modCount!! 
    // 将 elementData中从Index位置开始、长度为size-index的元素, 
    // 拷贝到从下标为index+1位置开始的新的elementData数组中。 
    // 即将当前位于该位置的元素以及所有后续元素右移一个位置。 
    System.arraycopy(elementData, index, elementData, index + 1, size - index); 
    elementData[index] = element; size++; 
}
// 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾 部。public boolean addAll(Collection<? extends E> c) { 
    Object[] a = c.toArray(); 
    int numNew = a.length; 
    ensureCapacity(size + numNew); 
    // Increments modCount 
    System.arraycopy(a, 0, elementData, size, numNew); 
    size += numNew; 
    return numNew != 0; 
}
// 从指定的位置开始,将指定collection中的所有元素插入到此列表中。 
public boolean addAll(int index, Collection<? extends E> c) { 
    if (index > size || index < 0) 
        throw new IndexOutOfBoundsException( "Index: " + index + ", Size: " + size); 
    Object[] a = c.toArray(); 
    int numNew = a.length; 
    ensureCapacity(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; 
}
书上都说 ArrayList 是基于数组实现的,属性中也看到了数组,具体是怎么实现的呢?比如就这个添加元
素的方法,如果数组大,则在将某个位置的值设置为指定元素即可,如果数组容量不够了呢?
看到 add(E e) 中先调用了 ensureCapacity(size+1) 方法,之后将元素的索引赋给 elementData[size]
而后 size 自增。例如初次添加时, size 0 add elementData[0] 赋值为 e ,然后 size 设置为 1 (类似执
行以下两条语句 elementData[0]=e;size=1 )。将元素的索引赋给 elementData[size] 不是会出现数组越
界的情况吗?这里关键就在 ensureCapacity(size+1) 中了。
4) 元素读取:
// 返回此列表中指定位置上的元素。 
public E get(int index) { 
    RangeCheck(index); 
    return (E) elementData[index]; 
}
5) 元素删除:
ArrayList 提供了根据下标或者指定对象两种方式的删除功能。如下:
romove(int index):
// 移除此列表中指定位置上的元素。 
public E remove(int index) { 
    RangeCheck(index); 
    modCount++; 
    E oldValue = (E) elementData[index]; 
    int numMoved = size - index - 1; 
    if (numMoved > 0) 
         System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // Let gc do its work 
    return oldValue; 
}
首先是检查范围,修改 modCount ,保留将要被移除的元素,将移除位置之后的元素向前挪动一个位
置,将 list 末尾元素置空( null ),返回被移除的元素。
remove(Object o)
// 移除此列表中首次出现的指定元素(如果存在)。这是应为ArrayList中允许存放重复的元素。 
public boolean remove(Object o) { 
    // 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。 
    if (o == null) { 
        for (int index = 0; index < size; index++) 
            if (elementData[index] == null) { 
                // 类似remove(int index),移除列表中指定位置上的元素。 
                fastRemove(index);
                return true; 
            } 
    } else { 
        for (int index = 0; index < size; index++) 
            if (o.equals(elementData[index])) { 
                fastRemove(index); 
                return true; 
            } 
        return false;
    }
}
首先通过代码可以看到,当移除成功后返回 true ,否则返回 false remove(Object o) 中通过遍历
element 寻找是否存在传入对象,一旦找到就调用 fastRemove 移除对象。为什么找到了元素就知道了
index ,不通过 remove(index) 来移除元素呢?因为 fastRemove 跳过了判断边界的处理,因为找到元素
就相当于确定了 index 不会超过边界,而且 fastRemove 并不返回被移除的元素。下面是 fastRemove
代码,基本和 remove(index) 一致。
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; // Let gc do its work 
}
removeRange(int fromIndex,int toIndex)
protected void removeRange(int fromIndex, int toIndex) { 
    modCount++; 
    int numMoved = size - toIndex; 
    System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved); 
    // Let gc do its work 
    int newSize = size - (toIndex-fromIndex); 
    while (size != newSize) 
        elementData[--size] = null; 
}
执行过程是将 elementData toIndex 位置开始的元素向前移动到 fromIndex ,然后将 toIndex 位置之后
的元素全部置空顺便修改 size
这个方法是 protected ,及受保护的方法,为什么这个方法被定义为 protected 呢?
先看下面这个例子
ArrayList<Integer> ints = new ArrayList<Integer>(Arrays.asList(0, 1, 2, 3, 4, 5, 6)); 
// fromIndex low endpoint (inclusive) of the subList 
// toIndex high endpoint (exclusive) of the subList 
ints.subList(2, 4).clear(); 
System.out.println(ints);
输出结果是 [0, 1, 4, 5, 6] ,结果是不是像调用了 removeRange(int fromIndex,int toIndex) !哈哈哈,就
是这样的。但是为什么效果相同呢?是不是调用了 removeRange(int fromIndex,int toIndex) 呢?
6) 调整数组容量 ensureCapacity
从上面介绍的向 ArrayList 中存储元素的代码中,我们看到,每当向数组中添加元素时,都要去检查添
加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。
数组扩容通过一个公开的方法 ensureCapacity(int minCapacity) 来实现。在实际添加大量元素前,我也
可以使用 ensureCapacity 来手动增加 ArrayList 实例的容量,以减少递增式再分配的数量。
public void ensureCapacity(int minCapacity) { 
    modCount++; 
    int oldCapacity = elementData.length; 
    if (minCapacity > oldCapacity) { 
        Object oldData[] = elementData; 
        int newCapacity = (oldCapacity * 3)/2 + 1; //增加50%+1 
        if (newCapacity < minCapacity) 
            newCapacity = minCapacity; 
        // minCapacity is usually close to size, so this is a win: 
        elementData = Arrays.copyOf(elementData, newCapacity);
    } 
}
从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组
容量的增长大约是其原容量的 1.5 倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避
免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造 ArrayList 实例时,就指定其容量,
以避免数组扩容的发生。或者根据实际需求,通过调用 ensureCapacity 方法来手动增加 ArrayList 实例的
容量。
Object oldData[] = elementData;// 为什么要用到 oldData[]
乍一看来后面并没有用到关于 oldData , 这句话显得多此一举!但是这是一个牵涉到内存管理的类, 所
以要了解内部的问题。 而且为什么这一句还在 if 的内部,这跟 elementData =
Arrays.copyOf(elementData, newCapacity); 这句是有关系的,下面这句 Arrays.copyOf 的实现时新创
建了 newCapacity 大小的内存,然后把老的 elementData 放入。好像也没有用到 oldData ,有什么问题
呢。问题就在于旧的内存的引用是 elementData elementData 指向了新的内存块,如果有一个局部
变量 oldData 变量引用旧的内存块的话,在 copy 的过程中就会比较安全,因为这样证明这块老的内存依
然有引用,分配内存的时候就不会被侵占掉,然后 copy 完成后这个局部变量的生命期也过去了,然后释
放才是安全的。不然在 copy 的的时候万一新的内存或其他线程的分配内存侵占了这块老的内存,而
copy 还没有结束,这将是个严重的事情。
ArrayList 还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通
trimToSize 方法来实现。代码如下:
public void trimToSize() { 
    modCount++; 
    int oldCapacity = elementData.length; 
    if (size < oldCapacity) { 
         elementData = Arrays.copyOf(elementData, size); 
    } 
}
由于 elementData 的长度会被拓展, size 标记的是其中包含的元素的个数。所以会出现 size 很小但
elementData.length 很大的情况,将出现空间的浪费。 trimToSize 将返回一个新的数组给
elementData ,元素内容保持不变, length size 相同,节省空间。
7) 转为静态数组 toArray
4 、注意 ArrayList 的两个转化为静态数组的 toArray 方法。
第一个, 调用 Arrays.copyOf 将返回一个数组,数组内容是 size elementData 的元素,即拷贝
elementData 0 size-1 位置的元素到新数组并返回。
 
public Object[] toArray() { return Arrays.copyOf(elementData, size); }
第二个,如果传入数组的长度小于 size ,返回一个新的数组,大小为 size ,类型与传入数组相同。所传
入数组长度与 size 相等,则将 elementData 复制到传入数组中并返回传入的数组。若传入数组长度大于
size ,除了复制 elementData 外,还将把返回数组的第 size 个元素置为空。
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; 
}
Fail-Fast 机制:
ArrayList 也采用了快速失败的机制,通过记录 modCount 参数来实现。在面对并发的修改时,迭代器很
快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险

4.总结:

关于ArrayList的源码,给出几点比较重要的总结:

1、注意其三个不同的构造方法。无参构造方法构造的ArrayList的容量默认为10,带有Collection参数

的构造方法,将Collection转化为数组赋给ArrayList的实现数组elementData

 

2、注意扩充容量的方法ensureCapacityArrayList在每次增加元素(可能是1个,也可能是一组)

时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的

容量的1.5倍加1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容

量),而后用Arrays.copyof()方法将元素拷贝到新的数组(详见下面的第3点)。从中可以看出,当容

量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先

能确定元素数量的情况下,才使用ArrayList,否则建议使用LinkedList

 

3ArrayList的实现中大量地调用了Arrays.copyof()System.arraycopy()方法。我们有必要对这两个

方法的实现做下深入的了解。

首先来看Arrays.copyof()方法。它有很多个重载的方法,但实现思路都是一样的,我们来看泛型版本

的源码:

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

很明显调用了另一个copyof方法,该方法有三个参数,最后一个参数指明要转换的数据的类型,其源码

如下:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> 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; 
}

这里可以很明显地看出,该方法实际上是在其内部又创建了一个长度为newlength的数组,调用

System.arraycopy()方法,将原来数组中的元素复制到了新的数组中。

下面来看System.arraycopy()方法。该方法被标记了native,调用了系统的C/C++代码,在JDK中是看

不到的,但在openJDK中可以看到其源码。该函数实际上最终调用了C语言的memmove()函数,因此它

可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批

量处理数组。Java强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。

 

4ArrayList基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次

插入或删除元素,就要大量地移动元素,插入删除元素的效率低。

5、在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理,

ArrayList中允许元素为null

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值