ArrayList核心知识总结

  • Post author:
  • Post category:其他




ArrayList核心知识总结

本文主要从源码的角度出发,对ArrayList体系进行总结。



一、有⽤过ArrayList吗?它是做什么用的?

ArrayList就是数组列表,底层是数组 Object[] elementData,ArrayList在装载基本数据类型时,实际装载的是对应的包装类。

与ArrayList类似的还有LinkedList,他们俩相⽐:

  • ArrayList:查找和访问元素的速度快,新增、删除的速度慢。线程不安全,使⽤频率⾼。
  • LinkedList:查找和访问元素的速度慢,新增、删除的速度快。

    在这里插入图片描述



二、ArrayList线程不安全,为什么还要去用?

实际开发中有以下⼏种场景:

  • 频繁增删:使⽤LinkedList,但是涉及到频繁增删的场景不多
  • 追求线程安全:使⽤Vector。
  • 普通的⽤来查询:使⽤ArrayList,使⽤的场景最多。

根据数据结构的特性,三者难以全包含,只能在相互之间做取舍。



三、ArrayList底层是数组,那是怎么实现不断扩容的?

  • 使⽤⽆参构造创建ArrayList
/**
 * Default initial capacity.
 * 默认的初始化容量
 */
 private static final int DEFAULT_CAPACITY = 10;
 /**
 * 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.
 这个是创建的默认⼤⼩的空数组。EMPTY_ELEMENTDATA⽤于表示当第⼀个数据被添加时该空数组初始化的⼤⼩。
 */
 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 /**
 * Constructs an empty list with an initial capacity of ten.
 * 构造⼀个空的List,该List具有10个容量
 */
 public ArrayList() {
 	this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
 }

使⽤ArrayList空参的构造器创建集合时,数组并没有创建。当集合中添加第⼀个元素时,数组被创建,初始化容量为10.

  • 使⽤有参构造创建ArrayList

有参构造创建时,如果指定了容量则会创建出指定容量⼤⼩的数组。如果指定容量为0,则与⽆参构造⼀样。

/**
 * Constructs an empty list with the specified initial capacity.
 *
 * @param initialCapacity the initial capacity of the list
 * @throws IllegalArgumentException if the specified initial capacity
 * is negative
 */
 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);
	 }
 }



四、ArrayList(int initialCapacity)会不会初始化数组大小?

/**
 * Constructs an empty list with the specified initial capacity.
 *
 * @param initialCapacity the initial capacity of the list
 * @throws IllegalArgumentException if the specified initial
capacity
 * is negative
 */
 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);
 	}
 }

会初始化⼤⼩,但如果通过ArrayList的size()⽅法进⾏判断时结果依然为0,因为只有在添加元素时才会对ArrayList的size属性+1

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



五、ArrayList底层是⽤数组实现,但数组长度是有限的,如何实现扩容?

当新增元素,ArrayList放不下该元素时,触发扩容。

扩容的容量将会是原容量的1/2,也就是新容量是旧容量的1.5倍。

确定新容量确定的源码:

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
 private void grow(int minCapacity) {
	 // overflow-conscious code
	 int oldCapacity = elementData.length;
	 //新容量=旧容量+1/2旧容量
	 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);
}

执⾏扩容时使⽤系统类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;
 }



六、ArrayList1.7之前和1.7及以后的区别?

1.7之前ArrayList在初始化的时候直接调⽤this(10),初始化容量为10的数组。在1.7及以后,只有第⼀次执⾏add⽅法向集合添加元素时才会创建容量为10的数组。



七、为什么ArrayList增删⽐较慢,增删是如何做的?

  • 没有指定index添加元素
/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
 public boolean add(E e) {
	 ensureCapacityInternal(size + 1); // Increments modCount!!
	 elementData[size++] = e;
	 return true;
 }
  • 如果指定了index添加元素
/**
 * Inserts the specified element at the specified position in this
 * list. Shifts the element currently at that position (if any) and
 * any subsequent elements to the right (adds one to their
indices).
 *
 * @param index index at which the specified element is to be
inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
 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++;
 }

从源码⾥看到,是将要添加的元素位置index之后的已有元素全部拷⻉到添加到原数组index+1处,然后再把新的数据加⼊。



八、ArrayList插⼊和删除数据⼀定慢吗?

不⼀定,取决于删除的数据离数组末端有多远,如果离末端较近,则性能不⼀定差。



九、ArrayList如何删除数据?

/*
 * Private remove method that skips bounds checking and does not
 * return the value removed.
 */
 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
 }

ArrayList删除数据时同样使⽤拷⻉数组的⽅式,将要删除的位置之后的所有元素拷到当前位置,最后再对最后⼀个位置的数据设置为null,交给gc来回收。这种删除,其实就是覆盖,如

果数据量⼤,那么效率很低。



十、ArrayList适合做队列吗?

队列需要遵循先进先出的原则,如果从ArrayList的数组头部⼊队列,数组尾部出队列,那么对于⼊队列时的操作,会涉及⼤数据量的数组拷⻉,⼗分耗性能。队头队尾反⼀反也是⼀样,因此ArrayList不适合做队列。



十一、数组适合做队列吗?

ArrayBlockingQueue环形队列就是⽤数组来实现的。ArrayBlockingQueue的存和取操作的索引是在当索引值等于容量值时,将索引值设置为0实现环形队列的效果,因此在这种情况下,数组适合做队列。

/**
 * Inserts element at current put position, advances, and signals.
 * Call only when holding lock.
 */
 private void enqueue(E x) {
	 // assert lock.getHoldCount() == 1;
	 // assert items[putIndex] == null;
	 final Object[] items = this.items;
	 items[putIndex] = x;
	 if (++putIndex == items.length)
	 	putIndex = 0;
	 count++;
	 notEmpty.signal();
 }
 /**
 * Extracts element at current take position, advances, and
signals.
 * Call only when holding lock.
 */
 private E dequeue() {
	 // assert lock.getHoldCount() == 1;
	 // assert items[takeIndex] != null;
	 final Object[] items = this.items;
	 @SuppressWarnings("unchecked")
	 E x = (E) items[takeIndex];
	 items[takeIndex] = null;
	 if (++takeIndex == items.length)
	 	takeIndex = 0;
	 count--;
	 if (itrs != null)
	 	itrs.elementDequeued();
	 notFull.signal();
	 return x;
}



十二、ArrayList和LinkedList两者的遍历性能孰优孰劣?

ArrayList的遍历性能明显要⽐LinkedList好,因为ArrayList存储的数据在内存中时连续的,CPU内部缓存结构会缓存连续的内存⽚段,可以⼤幅降低读取内存的性能开销。



十三、ArrayList和LinkedList的区别?


ArrayList:

基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存储数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能,甚至超过LinkedList(需要创建大量的node对象)。


LinkedList:

基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历。遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时需要对list重新进行遍历,性能消耗极大。

另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexOf对list进行了遍历,当结果为空时会遍历整个列表。



版权声明:本文为qq_49529322原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。