目录
ArrayList增删效率和查询效率相较于LinkedList比怎么样?
前言
在学习初期和刷面试题的时候,都是死记硬背,没有看过源码,,这些问题基本上都是从源码角度出发的,这次从源码角度去分析一下这些问题的答案。
ArrayList底层的数据结构是什么?
是一个Object[] elementData 数组,看下源码
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
//传入初始化长度
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);
}
}
//无参构造,直接使用默认值,空
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//传入一个集合,这里就直接调用copyof方法进行拷贝,这个方法底层是System.arraycopy
//在下面的add、扩容时候讲
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;
}
}
这里要注意一个点,初始化的时候,使用无参构造,创建的数组是空数组,没有长度,是在第一次放入元素,才会进行第一次扩容,到底扩多少,继续看源码。
ArrayList扩容机制是什么?
1、长度变化
在数组为空时,第一次放入元素,会先扩容10个长度,后面加入元素如果超过10个,则进行1.5倍扩容,此时的1.5倍不是直接乘1.5倍,而是(原长度+原长度>>1),像这样做了一个位运算,这是jdk1.8后做的一个运算优化,看下源码:
public boolean add(E e) {
modCount++;
//无参构造后,elementData和size都是为空的
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
//无参构造后,这个if校验是相等的,都是0,随后调用了grow()方法
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
//调用了扩容的主要方法newCapacity(),然后配合copyof完成elementData的扩容
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
这里可以看到主要的扩容方法就是newCapacity(),进去看源码:
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//这里就是上面所说的(原长度+原长度>>1),相当于(原长度+原长度/2)
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//这种情况的出现就表明了,创建list对象时是使用的无参构造或者,有参构造但是传入初始长度为0
//这里的DEFAULT_CAPACITY就是10
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
//这里表示新的长度小于MAX_ARRAY_SIZE,则使用新长度,否则,使用Integer.MAX_VALUE
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
//这里MAX_ARRAY_SIZE比Integer.MAX_VALUE小8,为int的的取值范围留出了空余,防止越界
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE)
? Integer.MAX_VALUE
: MAX_ARRAY_SIZE;
}
2、复制CopyOf()
这里ArrayList.copyof()方法中调用了System.arraycopy()方法,此方法上标注的@HotSpotIntrinsicCandidate注解,表示对此方法进行手写,可以跳过JNI,直接在JVM中运行,所以这个深拷贝是比较快的。
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
@HotSpotIntrinsicCandidate
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;
}
@HotSpotIntrinsicCandidate
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
下面是我在网络上找到的解释,看着比较好理解。
可以看到这是一个native(本地)方法,也就是说是用C++写的,所以其效率比非native方法更高。在JVM里对@HotSpotIntrinsicCandidate注解的进行了手写,这里要提一个叫JNI(Java Native Interface)的东西,普通的native方法通俗的讲就是编译后还要通过JNI再次编译成.cpp文件才能执行.而有 @HotSpotIntrinsicCandidate这个注解的方法在JVM里就是用.cpp文件写好的,所以就跳过了JNI阶段,所以速度就能提升,这也是System.arraycopy()速度快的原因。
ArrayList删除机制
ArrayList在删除时都是先找出这个元素,然后把这个元素后面的全部元素向前移动,随后为最后一位赋值为空,看一下图解,就很好理解:
再看一下源码:
public E remove(int index) {
//这就是用来防止数组越界的校验
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
//调用FastRemove方法
fastRemove(es, index);
return oldValue;
}
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
//这个操作可以理解为,将es数组中的i+1向后位置的元素,往前移一位;
//实际上是进行了一个深拷贝,重新整体放置了一下元素
//这时数组的长度不会改变,以前是10,删除一个元素,现在有9个元素,但是长度还是10
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
这里想强调一下,修改元素set方法,不是先remove后add,是直接找到这个index,然后进行赋值,简单粗暴哈,所以set方法不会造成大量的资源浪费,不要和remove混为一谈。
ArrayList新增元素
ArrayList如果在数组中有数据的情况下,在某index处新增元素,那么,这个怎么操作的,带入删除元素的想法来模拟一下,那就是先把某index处以及往后的元素向后移动一位,再把index处赋上新值,那么实际是怎么样的,看一下源码:
public void add(int index, E element) {
//防止下标越界校验
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
//如果长度已经满了,那就扩容
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
//然后把index以及往后的元素向后移动一位,实际上也是深拷贝,重新放置元素,把index处置为null
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
//长度加一
size = s + 1;
}
ArrayList查询逻辑
底层为数组,就直接取索引即可,所以查询很快,看一下源码:
public E get(int index) {
//防止下标越界
Objects.checkIndex(index, size);
//直接取index处的值
return elementData(index);
}
ArrayList增删效率和查询效率相较于LinkedList比怎么样?
在上面两部分,可以看出ArrayList增删都会进行深拷贝数组,比较耗费资源,这也从根本上说明了,它不适合作为队列来使用,因为太耗费资源了;那么LinkedList
是怎么样操作的?看一下LinkedList源码
//可以看出LinkedList都是Node节点操作
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
//看到这里只需要浪两步操作,即可添加
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
所以,看到源码,LinkedList的底层是Node节点构成的双向链表,它的增删只需要两步,1、找到元素,2、调整节点,即可。
但是链表的查询就很慢了,不管是哪个数,链表都要从头遍历,很慢,不如数组。
这里就印证了大家都说的,ArrayList查询快,LinkedList增删快。
ArrayList线程安全吗?
No,在add,remove时都没有加锁,如果在操作ArrayList时候,尽量防止对列表进行增删操作和修改操作。
想要线程安全则使用Vector,Collections.synchronizedList(),CopyOnWriteArrayList原理都是给方法套个synchronized。
Ok了,这就是对常见的ArrayList面试题的源码解析,耐心看完应该对ArrayList没啥太大问题,至于深拷贝在JVM中怎么操作的,怎么就那么快,这个问题暂时还挖掘不了那么深的层次,记住就好了。
最后我还整理汇总了⼀些 Java ⾯试相关的⾼质量 PDF 资料和免费Idea账号
公众号:Java小白,回复“⾯试” 和“idea破解”即可获取!