面试题:
看的面经里面,关于 ArrayList 的题目不多,牛客里面的选择题关于 ArrayList 的有一些。
关注点:
讲讲 ArrayList 吧;
ArrayList 的设计细节:
数据结构,默认参数,扩容机制等;
—————————————— 美丽的分割线 ——————————————-
本以为 ArrayList 应该就是,维护一个数组,然后实现增删改查,比较简单,没想到 上来就是 1000+ 行的代码…
浏览了一下,大部分和迭代器有关
先开始撸增删改查的代码吧~
- 先从数据结构开始吧~
/**
* 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;
蹩脚翻译一下大概意思:
(1)这个 elementData 是 ArrayList 中数据储存缓存的地方;
(2)这个 elementData 的大小就是 ArrayList 的Capacity;
(3)当 elementData等于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,第一次 add 的时候,elementData 的大小会被扩容到 DEFAULT_CAPACITY。
再看看 三个 构造函数 和 几个变量:
/**
* 默认容量,不指定容量初始化时,在第一次 add 时,会被扩容到capacity = 10 的大小.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 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.
*
* 默认初始化大小的共享实例
* !!!这个常量 是用来 在第一次 add 时,与 EMPTY_ELEMENTDATA 区别开。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
/**
* 指定 初始化 容量的 构造函数
*/
public ArrayList(int initialCapacity) {
// 初始化 容量 > 0,直接初始化数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
// 初始化 容量 > 0,使用共享的空数组常量 EMPTY_ELEMENTDATA,从而 与 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 区分开来。
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
/**
* 默认无参的构造函数,使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 暂时作为内部数组 的实例
**/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 对于 Collection 参数的初始化,Collection --> 数组 --> 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;
}
}
以上,需要记住的几个细节问题:
(1)ArrayList 的使用无参构造函数初始化时,容量(capacity)时多少?
答:DEFAULT_CAPACITY = 10,默认初始化容量为 10。
(2)ArrayList 初始化后,内部数组的情况?
答:无参构造函数初始化时,内部数组都为一个空数组;指定初始容量initCapacity的构造函数初始化时,内部是一个 容量为 initCapacity 的数组。
(3)initCapacity = 0 和 无参构造函数初始化后,内部数组都为一个空数组,如何区别(第一次 add时)?
答:无参构造函数初始化后 his.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;但是 initCapacity = 0 后的情况是,this.elementData = EMPTY_ELEMENTDATA。
- 增 :add(E e)方法:
主要过程分为两个部分 —- 1个是在需要的时候 对数组进行扩容;2 在内部数组的 size+1 处赋值
public boolean add(E e) {
// 1.保证扩容,必要时扩容
ensureCapacityInternal(size + 1);
// 2. 赋值
elementData[size++] = e;
return true;
}
先来看看扩容的时机 和 过程(撸代码):
private void ensureCapacityInternal(int minCapacity) {
// 确定当前需要的 最小容量
// 如果是 无参构造函数 的话,会让它 >= 10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
以上,就区分开了 无参构造函数 还是指定容量构造函数 的初始化。
无参构造函数的话,最小容量 = 10
指定容量的构造函数初始化,最小容量可以是 0(其实是1)
private void ensureExplicitCapacity(int minCapacity) {
// fast-fail 机制
modCount++;
// overflow-conscious code
// 内部数组不够大 --> 扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 扩容 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 保证容量 > 0,也就是 当 oldCapacity = 0的时候,newCapacity = minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 主要针对 minCapacity > MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 的情况
// minCapacity > MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 ? MAX_ARRAY_SIZE: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)保证 无参构造函数 的初始化情况下,在第一次 add 时,保证容量 为10(DEFAULT_CAPACITY )?
答: 在每次add 的时候,判断elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA ,如果处理就是无参初始化的情况,指定最小容量为 10。
(2)如何保证 add 的时候,数组不越界?
答:首先确定本次 add 所需的最小容量 minCapacity :无参构造函数初始的情况下,minCapacity = 10,其他情况下都是 minCapacity = size + 1;当 minCapacity > elementData.length 时,会对数组进行扩容;在 gorw 函数中进行扩容,会对 旧数组的长度 * 1.5(记为 newCapacity)进行扩容,newCapacity < minCapacity 的情况下,直接让newCapacity = newCapacity,也就保证了 newCapacity > newCapacity,保证数组不越界。
在 ArrayList 中,还有一个 add 函数 add(int index, E element) ,在指定 index 位置添加元素。
过程也很简单:1. 必要时扩容;2. index 开始及后的元素(包括index)全部向后移动一位;3. elementData[index] = element。
直接看代码:
public void add(int index, E element) {
// 检查 index <= size ?小于就没有必要插入了
rangeCheckForAdd(index);
//必要时扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 复制数组
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 赋值
elementData[index] = element;
size++;
}
- 删除过程 remove(int index):
简单啊,看代码:
public E remove(int index) {
// 1. 检查 index
rangeCheck(index);
//2. 取出 旧值
modCount++;
E oldValue = elementData(index);
// 3. 调整size - index - 1 后的元素 向前移动 一个(复制)
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// size - 1 位置 赋值为 null,以让空间被gc 回收
elementData[--size] = null; // clear to let GC do its work
// 返回旧值
return oldValue;
}
// 1. 查找 元素的 index
//2. 删除 index 位置的元素
public boolean remove(Object o) {
//这里一个小细节就是 o==null 的判断,放在 for 循环外面,
//只需要一次判断,否则 放在 for 循环里面就要多次判断 ---- 效率问题呀
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;
}
// fastRemove 过程 和 remove(int 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; // clear to let GC do its work
}
- 查找 get(int index),contains(Object o):
get(int index) 非常简单,也就是 内部采用数组实现的优势了吧~ 直接 获取 elementData[index] 位置元素返回即可:
public E get(int index) {
// 检查 index 范围
rangeCheck(index);
//返回 elementData[index]
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
contains(Object o) 相对没有那么简单,但是思路也很清晰:1.遍历内部数组 2. 返回第一个匹配的索引
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
// 遍历数组,返回对应索引, -1表示没有找到
// o == null 的在for循环的设计,同样是为了避免多次判断
public int indexOf(Object o) {
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;
}
- 增删改查的时间复杂度
add :O(1)
remove:O(n)要对数组进行移动
get:O(1)
contains:O(n)要遍历一遍内部数组
关于 System.arraycopy 的效率问题(https://blog.csdn.net/wangyangzhizhou/article/details/79504818):
虽然说啊,System.arraycopy为 JVM 内部固有方法,它通过手工编写汇编或其他优化方法来进行 Java 数组拷贝,这种方式比起直接在 Java 上进行 for 循环或 clone 是更加高效的。
但是内部也是要对数组每个元素进行拷贝,最坏情况下,还是O(n)