Java集合篇——ArrayList详解

@Java集合

Java集合大纲

Java中的集合包含多种数据结构,如数组、链表、队列、哈希表等。从类的继承结构来说,可以分为两大类,一类是继承自Collection接口,这类集合包含List、Set和Queue等集合类。另一类是继承自Map接口,这主要包含了哈希表相关的集合类
结构图如下
在这里插入图片描述

一、List

常用的通过实现List接口实现的集合类有ArrayList、LinkedList、Vector等。

1.1 ArrayList

ArrayList底层是由数组实现的,通过内部封装,变成一个动态数组。也正因如此,ArryayList的许多特点也就显而易见了。

数组定义:

首先数组一种顺序存储的线性表数据结构,由系统分配一组连续固定大小的内存空间来存储一组具有相同类型的数据。
通过定义我们就能知道,内存中一片连续的空间意味着,当我get(index)时,我可以根据数组的(首地址+偏移量),直接访问到第index个元素在内存中的位置,因此ArrayList获取元素get(n)的时间复杂度是O(1)。而当我们在做指定位置的增删操作时(除末位),抽入位置后面的元素都要做移动,因此时间复杂度O(n)。也因此ArrayList被贴上“查询快,增删慢”的标签。

自动扩容:

介绍完数组,就该了解一下为什么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. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

根据注释我们可知,elementData是存储ArrayList元素的数组,elementData.length就是就是集合的当前容量,当elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,进行第一次add,容量就会变为DEFAULT_CAPACITY(也就是10)。而size则是指elementData中实际的元素个数。

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

默认初始容量大小为 10;

protected transient int modCount = 0;

这个变量是定义在 AbstractList 中的。记录对 List 操作的次数。主要使用在 Iterator迭代集合上,用于检测该集合的结构是否被修改,当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast(快速失败机制),即抛出 ConcurrentModificationException异常。
详见:fail-fast(快速失败)机制详解

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {
     };

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
     };

这两个空数组本质上都是空数组,但是从注释以及后续方法中可以了解到是为了区分elementData是通过无参构造还是有参构造实例化的,以便区分后续扩容操作。

构造函数

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
     
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

ArrayList的无参构造函数,给elementData赋予一个空的数组,但在后续第一次执行add()方法时扩容至DEFAULT_CAPACITY(10)。

    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时,会将EMPTY_ELEMENTDATA赋值给elementData(空数组,但在后续扩容过程中容量只会逐个累加,即容量始终等于实际元素)。当 initialCapacity 大于零时初始化一个大小为 initialCapacity 的 object 数组并赋值给 elementData。

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

使用指定Collection为参构造,将 Collection 转化为数组并赋值给 elementData,把 elementData 中元素的个数赋值给 size。 如果 size 不为零,则判断 elementData 的 class 类型是否为 Object[],不是的话则做一次转换。 如果 size 为零,则把 EMPTY_ELEMENTDATA 赋值给 elementData,相当于new ArrayList(0),后续扩容如上。

tips
我们通过一个实验即可了解EMPTY_ELEMENTDATA 和DEFAULTCAPACITY_EMPTY_ELEMENTDATA 两种默认初始空数组的后续扩容的区别

public static void main(String[] args) {
     
        ArrayList<Integer> list1 = new ArrayList<>();
        ArrayList<Integer> list2 = new ArrayList<>(0);
        for (int i = 0; i < 11; i++) {
     
            list1.add(i);
            list2.add(i);
            System.out.println(getArrayListCapacity(list1));//10,10,10,10,10,10,10,10,10,10,15
            System.out.println(getArrayListCapacity(list2));// 1, 2, 3, 4, 6, 6, 9, 9, 9,13,13
        }
    }
	//因为elementData私有,通过反射访问长度
    public static int getArrayListCapacity(ArrayList<?> arrayList) {
     
        Class<ArrayList> arrayListClass = ArrayList.class;
        try {
     
            Field field = arrayListClass.getDeclaredField("elementData");
            field.setAccessible(true);
            Object[] objects = (Object[])field.get(arrayList);
            return objects.length;
        } catch (NoSuchFieldException e) {
     
            e.printStackTrace();
            return -1;
        } catch (IllegalAccessException e) {
     
            e.printStackTrace();
            return -1;
        }
    }

参考自:查看ArrayList的容量

扩容方法解析

  1. add操作
    public boolean add(E e) {
     
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
     
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
     
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    private void ensureCapacityInternal(int minCapacity) {
     
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
     
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

由上述方法我们能发现,每当通过add()添加元素时会先通过calculateCapacity判断elementData 是否等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,是的话则取DEFAULT_CAPACITY 和 minCapacity 的最大值也就是 10。这就是 EMPTY_ELEMENTDATA 与 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的区别所在。另外ensureExplicitCapacity中对odCount 自增 1,记录操作次数,然后如果 minCapacity 大于 elementData 的长度,则对集合进行扩容。那么为什么每次扩容都会是原容量的一半,则要我们关注grow这个方法。

	//jdk1.7后
    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);
    }

这里我们放一张jdk1.6时的方法对比

//jdk1.6
public void ensureCapacity(int minCapacity) {
     
 modCount++;
 int oldCapacity = elementData.length;
 if (minCapacity > oldCapacity) {
     
     Object oldData[] = elementData;
     int newCapacity = (oldCapacity * 3)/2 + 1;// 计算新容量
     if (newCapacity < minCapacity) newCapacity = minCapacity;
     // minCapacity is usually close to size, so this is a win:
     elementData = Arrays.copyOf(elementData, newCapacity);
 }
}

通过grow方法我们能明白,jdk1.7以后arraylist通过右偏移位运算进行扩容(算法优化,效率更高),即默认扩容至原来容量的 1.5 倍。但是扩容之后也不一定适用,有可能太小,有可能太大。所以才会有下面两个 if 判断。如果1.5倍太小的话,则将我们所需的容量大小赋值给newCapacity,如果1.5倍太大或者我们需要的容量太大,那就直接拿 newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE 来扩容。然后通过Arrays.copyof()将原数组中的数据复制到大小为 newCapacity 的新数组中,并将新数组赋值给 elementData。因此ArrayList的动态数组实际上就是数组间数据的浅拷贝(ArrayList不能存放普通数据类型,因此只能对对象的引用进行拷贝),并让element指向了新的数组,这是非常耗时的操作,因此我们要尽量避免频繁的拷贝问题,因此需要体检预知数据的数量级,但为了不一直占用较大的内存,我们可以在插入大量数据前,通过list.ensureCapacity(N)提前进行扩容,一步到位!

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

而为什么arraylist的最大容量是Integer.MAX_VALUE - 8呢?其实是因为数组对象有一个额外的元数据,数组的最大尺寸为2^31 = 2147483648,但是需要8bytes的存储大小表示数组的长度等元数据,因此定义为MAX_VALUE - 8。

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

有以上源码可知,add(int index, E element),addAll(Collection c),addAll(int index, Collection c) 操作是都是先对集合容量检查 ,以确保不会数组越界。然后通过 System.arraycopy() 方法将旧数组元素拷贝至一个新的数组中去。

  1. remove操作
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;
}
	
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
}

当我们调用 remove(int index) 时,首先会检查 index 是否合法,然后再判断要删除的元素是否位于数组的最后一个位置。如果 index 不是最后一个,就再次调用 System.arraycopy() 方法拷贝数组。说白了就是将从 index + 1 开始向后所有的元素都向前挪一个位置。然后将数组的最后一个位置空,size - 1。如果 index 是最后一个元素那么就直接将数组的最后一个位置空,size - 1即可。
当我们调用 remove(Object o) 时,会把 o 分为是否为空来分别处理。然后对数组做遍历,找到第一个与 o 对应的下标 index,然后调用 fastRemove 方法,删除下标为 index 的元素。其实仔细观察 fastRemove(int index) 方法和 remove(int index) 方法基本全部相同。

  1. get操作
public E get(int index) {
     
	rangeCheck(index);
	return elementData(index);
}

由于 ArrayList 底层是基于数组实现的,所以获取元素就相当简单了,直接调用数组随机访问即可。

补充

ArrayList存放的是引用还是值?
前面提到了ArrayList的浅拷贝,这里补充一下。ArrayList只能存放引用数据类型,这大家都明白,但为什么网上的人会有“当存基本数据类型存的是值,存对象存的是引用”的观念,实际上大多都是受到了基础类型的包装类影响

public final class Integer extends Number implements Comparable<Integer> {
     
public final class Double extends Number implements Comparable<Double> {
     

可以看到,基本数据类型的包装类和String一样都是final修饰的,而且Double和Integer等基本数据类型包装类中也没有提供修改值的方法,也就是说之前看样子是在修改数据,其实是指向了一个新的内存地址,ArrayList中第二次存放数据的时候,并没有改变第一次存放的引用中的内存地址中的值,而是存了一个新的引用。也因为如此,我们在使用ArrayList存储对象时,可以先add对象,再对对象的成员变量进行赋值如下:

List<A> entityList = new ArrayList<>();
A a = new A();
 
entityList.add(a);
 
a.setXXX(XXXX);
a.setXXX(XXXX);
a.setXXX(XXXX);

因此,真正的结论是:ArrayList中存放的一直都是引用!
感兴趣可以参考边刷题边思考:LeetCode刷题时引发的思考:Java中ArrayList存放的是值还是引用?

transient
transient简单理解就是一个关键词,什么作用呢?一句话:将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。
而为什么要在elementData前加上transient呢?
因为序列化ArrayList的时候,ArrayList里面的elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法:

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

这样每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍历elementData,只序列化那些有的元素,结果:
1.加快了序列化的速度
2.减小了序列化之后的文件大小

总结

ArrayList 底层基于数组实现容量大小动态可变。 扩容机制为首先扩容为原始容量的 1.5 倍。如果1.5倍太小的话,则将我们所需的容量大小赋值给 newCapacity,如果1.5倍太大或者我们需要的容量太大,那就直接拿 newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE 来扩容。 扩容之后是通过数组的拷贝来确保元素的准确性的,所以尽可能减少扩容操作。 ArrayList 的最大存储能力:Integer.MAX_VALUE。 size 为集合中存储的元素的个数。elementData.length 为数组长度,表示最多可以存储多少个元素。 如果需要边遍历边 remove ,必须使用 iterator。且 remove 之前必须先 next,next 之后只能用一次 remove。

转自:https://juejin.im/post/5a90c37af265da4e83267f8e

你可能感兴趣的:(Java集合,java,arraylist,集合)