个人深入理解java集合框架:6、ArrayList

在写ArrayList前,我还是先推荐一个课程《玩转算法系列–玩转数据结构》,学习完这个课程再来深入理解java的Collection、Map、Set都更有帮助。
在这个课程中,我在学习时跟着编写一个Array实现类,其机制和ArrayList极为相似,相信学完这个课程能更好、更快的理解。
博文:《持续学习合集–数组》

ArrayList

官方文档中的介绍:

Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)
The size, isEmpty, get, set, iterator, and listIterator operations run in constant time. The add operation runs in amortized constant time, that is, adding n elements requires O(n) time. All of the other operations run in linear time (roughly speaking). The constant factor is low compared to that for the LinkedList implementation.
Each ArrayList instance has a capacity. The capacity is the size of the array used to store the elements in the list. It is always at least as large as the list size. As elements are added to an ArrayList, its capacity grows automatically. The details of the growth policy are not specified beyond the fact that adding an element has constant amortized time cost.
An application can increase the capacity of an ArrayList instance before adding a large number of elements using the ensureCapacity operation. This may reduce the amount of incremental reallocation.
Note that this implementation is not synchronized. If multiple threads access an ArrayList instance concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements, or explicitly resizes the backing array; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list. If no such object exists, the list should be “wrapped” using theCollections.synchronizedList method. This is best done at creation time, to prevent accidental unsynchronized access to the list:
List list = Collections.synchronizedList(new ArrayList(…));
The iterators returned by this class’s iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator’s own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.
机器翻译:
List接口的可调整大小的数组实现。实现所有可选列表操作,并允许所有元素,包括null。除了实现List接口之外,此类还提供了一些方法来操作内部用于存储列表的数组的大小。 (这个类大致相当于Vector,除了它是不同步的。)
size,isEmpty,get,set,iterator和listIterator操作以恒定时间运行。添加操作以分摊的常量时间运行,即添加n个元素需要O(n)时间。所有其他操作都以线性时间运行(粗略地说)。与LinkedList实现相比,常数因子较低。
每个ArrayList实例都有一个容量。容量是用于存储列表中元素的数组的大小。它始终至少与列表大小一样大。随着元素添加到ArrayList,其容量会自动增加。除了添加元素具有恒定的摊销时间成本这一事实之外,未指定增长策略的详细信息。
在使用ensureCapacity操作添加大量元素之前,应用程序可以增加ArrayList实例的容量。这可能会减少增量重新分配的数量。
请注意,此实现不同步。如果多个线程同时访问ArrayList实例,并且至少有一个线程在结构上修改了列表,则必须在外部进行同步。 (结构修改是添加或删除一个或多个元素的任何操作,或显式调整后备数组的大小;仅设置元素的值不是结构修改。)这通常通过同步一些自然封装的对象来实现。名单。如果不存在此类对象,则应使用Collections.synchronizedList方法“包装”该列表。这最好在创建时完成,以防止意外地不同步访问列表:
List list = Collections.synchronizedList(new ArrayList(…));
这个类的iterator和listIterator方法返回的迭代器是快速失败的:如果在创建迭代器之后的任何时候对列表进行结构修改,除了通过迭代器自己的remove或add方法之外,迭代器将抛出ConcurrentModificationException。因此,在并发修改的情况下,迭代器快速而干净地失败,而不是在未来的未确定时间冒任意,非确定性行为的风险。
请注意,迭代器的快速失败行为无法得到保证,因为一般来说,在存在不同步的并发修改时,不可能做出任何硬性保证。失败快速迭代器会尽最大努力抛出ConcurrentModificationException。因此,编写依赖于此异常的程序以确保其正确性是错误的:迭代器的快速失败行为应该仅用于检测错误。

ArrayList的特性

我个人总结了一些关于ArrayList的特性:

  1. 有序的(这个需要好好说明)
  2. 元素可为null
  3. 数据可重复
  4. 拥有扩容机制(可调整集合大小的、可自动增加集合大小的)
  5. 线程不同步,存在线程安全问题
    ……

本人目前总结出部分特点,若大家还有其他特点请指出,一起探讨。

源码解析

那么我们根据ArrayList中的源码开始分析:

  • 成员属性
private static final long serialVersionUID = 8683452581122892189L;
    / **
    *默认初始容量。
    * /
private static final int DEFAULT_CAPACITY = 10;
    / **
     *用于空实例的共享空数组实例。
     * /
private static final Object[] EMPTY_ELEMENTDATA = {};
    / **
     *共享的空数组实例,用于默认大小的空实例。 
     *我们将此与EMPTY_ELEMENTDATA区别开来,以了解添加第一个元素时需要添加多少空间。
     * /
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    / **
     *存储ArrayList的元素的数组缓冲区。
     *ArrayList的容量是此数组缓冲区的长度。 
     *添加第一个元素时,任何具有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空ArrayList都将扩展初始长度为DEFAULT_CAPACITY的Object[]对象。
     * /
transient Object[] elementData; // non-private to simplify nested class access
    / **
     * ArrayList的大小(它包含的元素数)。
     * /
private int size;
    /**
      *要分配的最大数组大小。
      *有些VM会在数组中保留一些头部节点。
      *尝试分配更大的数组可能会导致
      *OutOfMemoryError:请求的数组大小超过VM限制
     */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

由此可见ArrayList底层是由数组组成的,初始化是一个空数组(这个版本之间差异较大),容器最大容量为 Integer.MAX_VALUE - 8(2147483639);
在jdk1.8以前,是默认数组大小为10的一个数组,1.8以后时第一次添加操作后扩容时改变大小;(如果添加元素数量小于10,数组大小则为10);
具体想尝试的,可以利用反射机制映射出ArrayList实例的elementData,获取elementData的大小。

  • 构造方法
// 根据传参判断需要创建多大的Object[]
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);
    }
}

// 默认构造方式,直接创建一个空的Object[];
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

ArrayList提供了3种构造方法,默认创建空Object数组,如果指定传入大小则会判断创建多大的Object数组;
要注意的是:创建数组时,size变量是不会变的。
举个例子:

public static void main(String[] args) {
    ArrayList<People> list = new ArrayList(10);//传入的定义大小并不影响ArrayList中的size变量
    System.out.println(list.size());
    People p = new Man("name",19);
    list.add(p); // ArrayList中的size变量在add()方法进行size++
    list.add(p); 
    System.out.println(list.size());
}

结果如下:
在这里插入图片描述
不知道有没有朋友在看了源码之后就想试试这段代码的测试,经过测试后就会对size属性、size()以及add()有个认识。(其实本人非常想在这段测试就加入多线程来测试一下线程安全问题,但是还是缓缓。)

我们肯定会思考,什么时候size才会发生改变呢?问题先留着,接下来我们分析一些常用的方法;

  • 增的方法
    当然在ArrayList中有一个特性就是容量会自增加,就是当原本设定的容量满了以后会自动增加容量。
public boolean add(E e) {
    ensureCapacityInternal(size + 1); // Increments modCount!!
    elementData[size++] = e;
    return true;
}

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

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

public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(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;
}

这四种添加方法,都一样的涉及到了计算容量的方法:

// 比较传参的值,然后是否扩容/缩容操作
private void ensureCapacityInternal(int minCapacity) { 
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); // 用这个对象的公共属性(数组)
}

// 如有必要,增加此ArrayList实例的容量,以确保它至少可以容纳最小容量参数指定的元素数。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 
        return Math.max(DEFAULT_CAPACITY, minCapacity); // 如果是空的数组,则看你设定的值和默认数组值取大值
    }
    return minCapacity;
}

// 改变系统的modCount维持fail-fast机制,
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;     // 在AbstractList中定义的变量

    // overflow-conscious code
    if (minCapacity - elementData.length > 0) // 如果你计算后的值大于目前数组的大小则进行扩容
        grow(minCapacity); 
}
  • 扩容的方法:
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;// 当前数组大小
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容1.5倍
    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);
}

扩容一定程度上是加大了性能的消耗,因此扩容的时机是很重要的,扩容的关键就在于扩容的大小,目前内部实现的是1.5倍。至于为什么是1.5倍,这是众多前辈程序员的一个最佳实践,同时也可以理解为最小浪费的实现。

  • 删除方法
// 删除指定索引的元素,会返回该元素
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;
}

//删除根据参数第一个匹配到的元素(顺序),不存在该元素返回false
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;
}

// 快速删除
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; // clear to let GC do its work
}

// 删除索引fromIndex 到 toIndex 之间的所有元素
protected void removeRange(int fromIndex, int toIndex) { 
    modCount++;
    int numMoved = size - toIndex;
    System.arraycopy(elementData, toIndex, elementData, fromIndex,numMoved);

    // clear to let GC do its work
    int newSize = size - (toIndex-fromIndex);
    for (int i = newSize; i < size; i++) {
        elementData[i] = null;
    }
    size = newSize;
}

// 删除根据参数提供集合匹配的元素。
public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return batchRemove(c, false);
}

// 删除根据参数不匹配的元素(除条件内的元素外其他元素)
public boolean retainAll(Collection<?> c) { 
    Objects.requireNonNull(c);
    return batchRemove(c, true);
}

// 进行“删除”操作,true为删除不匹配的元素,false为删除匹配的元素
private boolean batchRemove(Collection<?> c, boolean complement) { 
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        for (; r < size; r++)
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
        if (r != size) {
            System.arraycopy(elementData, r,elementData, w,size - r);
            w += size - r;
        }
        if (w != size) {
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

// 清空元素
public void clear() {
    modCount++;

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

    size = 0;
}

需要注意的removeAll方法并不是删除全部,而是删除参数提供集合内匹配的元素;
删除全部是clear方法;

疑问先生上线:ArrayList在删除操作后并没有进行缩容操作,这是为什么呢?

首先Arraylist并不是没有缩容操作;

// 将当前实例调整为size大小;
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

为什么每次删除后都不去处理呢?
我个人认为这是因为每次删除都去调整大小是很浪费性能的表现,还不如把这种操作交给用户去判断何时操作;
假设当前操作是删除操作,下一次是增加操作;
如果我们在删除操作时进行了缩容操作,下一次增加的时候,我们又要进行扩容操作,这样非常浪费了性能。
删除->缩容->添加->扩容;(4个步骤)
目前默认操作是:
删除->添加->不一定需要进行扩容操作。(2个步骤,可能3个步骤);
因此不在每次删除时都进行缩容操作是利用了空间换时间的做法。

  • 查找的方法
E elementData(int index) {
    return (E) elementData[index];
}

public E get(int index) { // 根据下标获得元素
    rangeCheck(index);

    return elementData(index);
}

public boolean contains(Object o) { // 是否包含此元素
    return indexOf(o) >= 0;
}

public int indexOf(Object o) { // 根据元素查找第一次出现的索引,无则返回-1
    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;
}

public int lastIndexOf(Object o) { // 和上面方法相比,就是倒着查询第一次出现的索引,无则返回-1
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}
  • 修改的方法
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}
  • 相关流的操作
// 将实例写入到流中
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<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

// 读取流写入到实例中
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}
  • 其他:

此次讲述并没有覆盖全部的方法,只是深究了部分的方法,但是也受益匪浅,其实看懂了ArrayList的实现原理就能明白,ArrayList经过包装的Array的一种集合对象;
如何根据两者的特点去选择使用Array或者ArrayList是值得思考的事;

关于ArrayList实现了RandomAccess接口
RandomAccess 只是一个标记接口,用于判断是否支持随机快速访问;
Collections中的一个二分法查询中就存在这样一个判断;

public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
        return Collections.indexedBinarySearch(list, key);
    else
        return Collections.iteratorBinarySearch(list, key);
}

indexedBinarySearch()
使用普通的循环去查找。
iteratorBinarySearch()
使用iterator迭代器去查找。

大家可以使用ArrayList和LinkedList使用这个方法进行搜索比较一下速度。

最后,是关于ArrayList线程不安全的问题,由于ArrayList的特性,导致ArrayList适合于进行查询操作,在进行增删元素的时候会存在线程不安全的存在。

引用这篇文章的测试过程来说明:为什么说ArrayList是线程不安全的?

参考资料:
Java 集合深入理解(7):ArrayList
官方文档

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值