一、ArrayList底层实现原理及其个人理解注释
ArrayList以数组作为底层。
当使用无参构造创建集合时,直接赋值一个空的数组;创建集合时也可以指定集合大小,或者将另一个集合作为参数传入;
当使用无参构造第一次添加元素时,会扩容数组为默认给定数组大小为10,第一次扩容;
当数组满后再次添加元素就会扩容;(扩容临界点)
每次扩容为扩容前的大小+扩容前大小*0.5,相当于扩容后的数组是扩容前数组的1.5倍;(扩容因子);
[经典面试题]:当使用无参构造创建集合时,向里面添加了11个元素后,此时数组扩容了几次?
两次。因为使用无参构造创建集合后,第一次添加元素时会默认创建大小为10的数组,然后当添加元素满10个后再次添加元素时,再次扩容到之前数组的1.5倍。
//部分源码解析
//定义一个静态常量--数组长度的默认容量为10
private static final int DEFAULT_CAPACITY = 10;
//定义一个静态常量--空的对象类型数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//定义一个常量--默认的空的对象类型数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//定义一个对象数组--集合中所有的元素数组
transient Object[] elementData;
//定义一个整型--元素个数,默认为0
private int size;
//定义一个静态常量--数组最大为整形最大值-8
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//无参构造
public ArrayList() {
//初始化赋值一个默认的空数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//有参构造,传入参数为创建集合时的集合大小
public ArrayList(int initialCapacity) {
//判断创建的集合大小是否大于0
if (initialCapacity > 0) {
//大于0时,创建指定大小的数组并赋值给元素数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//当创建集合大小等于0时,创建一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
//当小于0时。就报错
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//有参构造,传入参数为集合
public ArrayList(Collection<? extends E> c) {
//将传入的集合转为数组后传递给新集合的元素数组
elementData = c.toArray();
//计算元素数组长度赋值给size属性,并判断长度是否为0(空)
if ((size = elementData.length) != 0) {
// defend against c.toArray (incorrectly) not returning Object[]
// (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
//此处是缓存中的判断
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//当传入集合为空时,赋值一个空的数组
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
//添加元素时会调用此方法,传入参数为(传入的元素,元素数组,元素个数)
private void add(E e, Object[] elementData, int s) {
//判断元素个数和元素数组长度是否相等
if (s == elementData.length)
//相等时表示数组满了,数组需要扩容
elementData = grow();
//将插入的元素放到所有元素的最后位置
elementData[s] = e;
//元素个数+1
size = s + 1;
}
//添加元素
public boolean add(E e) {
//修改次数+1
modCount++;
//添加元素时(传入的元素,元素数组,此时元素个数)
add(e, elementData, size);
return true;
}
//插入元素指定插入位置
public void add(int index, E element) {
//判断插入位置是否小于0,或者大于此时元素个数,是则报错(索引越界)
rangeCheckForAdd(index);
//修改次数+1
modCount++;
//定义临时常量s
final int s;
//定义临时集合中所有的元素数组
Object[] elementData;
//将元素个数赋值给临时常量s,将集合的元素数组赋值给临时的元素数组
//判断元素总个数是否等于元素数组总长度
if ((s = size) == (elementData = this.elementData).length)
//长度相等时,表示数组满了需要扩容
elementData = grow();
//将插入位置index及其后面所有元素往后移一位(从最后一个元素开始往后移一个位置,一直移动到index位置)
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
//将插入元素赋值到指定位置
elementData[index] = element;
//元素个数+1
size = s + 1;
}
//扩容
private Object[] grow() {
//当数组满后
//我们需要存放所有元素的最小空间为元素个数+1个刚添加的元素个数
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
//扩容时我们需要创建一个符合最小空间的数组,然后将原来数组的元素复制到新数组中
//所以我们先得到最小空间的数组长度
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
//创建符合我们最小扩容的数组,传入参数为最小的扩容
private int newCapacity(int minCapacity) {
// overflow-conscious code
//记录扩容前的数组长度
int oldCapacity = elementData.length;
//将扩容前的长度+扩容前长度的0.5倍,结果是我们新创建符合最小扩容的新数组长度
//oldCapacity >> 1 相当于扩容时的扩容因子(一次扩容多一点避免经常扩容)
int newCapacity = oldCapacity + (oldCapacity >> 1);
//判断扩容后的长度是否大于需要的最小扩容长度(是否符合)
if (newCapacity - minCapacity <= 0) {
//当扩容后的长度小于此时需要的最小扩容长度(只有超出int的最大值时才会小于他或者扩容前的数组为空时)
//判断元素数组是否为默认的空数组(只有无参构造后的第一次扩容为true)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//当数组是空时,返回一个默认的数组大小10,并返回默认的大小与我们所需的最小扩容长度中的最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
//判断是否超出了int最大值范围(第一次指定集合长度就超出了最大范围)
if (minCapacity < 0) // overflow
//是则报错误(内存超出)
throw new OutOfMemoryError();
//扩容的长度小于我们需要的扩容长度时,返回我们所需的最小扩容长度(有参构造后数组为空时第一次扩容)
return minCapacity;
}
//当不是无参构造后的第一次扩容时
//判断此时扩容后的大小是否超出数组的最大值(int的最大值-8)
return (newCapacity - MAX_ARRAY_SIZE <= 0)
//没有超出就返回扩容后新的数组长度
? newCapacity
//超出后执行这段(当新创建的数组长度小于int的最大值且大于数组的最大值时(int的最大值-8))
: hugeCapacity(minCapacity);
}
//判断扩容大小是否超出范围
private static int hugeCapacity(int minCapacity) {
//判断需要的最小空间是否超出了int的最大值范围,超出则报错(内存超出)
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//若没有超出int最大值就判断最小空间是否大于数组的最大值(int最大值-8)
return (minCapacity > MAX_ARRAY_SIZE)
//大于就返回int的最大值
? Integer.MAX_VALUE
//不大于就返回数组的最大值(int的最大值-8)
: MAX_ARRAY_SIZE;
}
//返回元素个数
public int size() {
return size;
}
//删除某个元素(对象)
public boolean remove(Object o) {
//将元素数组用临时变量记录
final Object[] es = elementData;
//元素个数用临时变量记录
final int size = this.size;
int i = 0;
//found为标记,break可以跳出found代码块
found: {
//判断删除对象是否为null
if (o == null) {
//是空的话就比对元素数组中是否有null元素
for (; i < size; i++)
//判断每个元素是否与删除元素相等,相等就跳出found,此时i为null的索引值
if (es[i] == null)
break found;
} else {
//若传入参数不是null
for (; i < size; i++)
//判断每个元素是否与删除元素相等,相等就跳出found,此时i为删除元素的索引值
if (o.equals(es[i]))
break found;
}
//若没有删除的元素就返回false
return false;
}
//删除此时数组中该元素(传入此时元素数组和记录的元素索引值)
fastRemove(es, i);
//删除成功返回true
return true;
}
private void fastRemove(Object[] es, int i) {
//修改次数+1
modCount++;
final int newSize;
//删除后的大小为元素个数-1,判断删除的元素是否为最后一个元素
if ((newSize = size - 1) > i)
//若删除后数组中还有元素则将删除位置后面的元素往前移(从前往后移动,一个个往前移动一位)
//底层中:例如[1,2,3,4],删除索引位置1的数字后,就会将2,3,4往前移动一位,虽然读出来是[2,3,4],但是移动后底层中会是[2,3,4,4],所以底层中还将最后一位置为null,便于GC去清空最后一个索引,节省空间。
System.arraycopy(es, i + 1, es, i, newSize - i);
//删除为最后一个元素后将数组置为null
es[size = newSize] = null;
}
//修改指定位置元素
public E set(int index, E element) {
//该方法用于判断修改的元素索引值是否越界(是否小于0和超出了元素个数),是则报错(索引越界)
Objects.checkIndex(index, size);
//将修改前该索引位置的元素取出
E oldValue = elementData(index);
//将新元素赋值到该索引位置
elementData[index] = element;
//返回修改前该索引的元素值
return oldValue;
}
//查找元素(根据索引值)
public E get(int index) {
//该方法用于判断查询的元素索引值是否越界(是否小于0和超出了元素个数),是则报错(索引越界)
Objects.checkIndex(index, size);
//没有越界就返回该索引值的元素
return elementData(index);
}
二、LinkedList底层实现原理及其个人理解注释
LinkedList以链表作为底层。
-
使用无参构造创建集合后,第一次添加元素会将该元素记录为first节点和last节点;
-
当不是第一次添加元素且集合中有元素时,会默认将元素添加到最后位置;
-
当插入元素时,会先判断插入的元素是否为最后一个,是最后一个则直接调用addLast,若不是则底层会先得到插入之前该位置的节点,然后创建新节点(插入的元素节点),新节点的上一节点地址记录插入之前该位置的节点的上一节点地址,新节点的下一节点地址记录插入之前该位置的节点;然后将插入之前该位置的节点的上一节点地址改为新节点;将插入之前该位置的节点的上一节点的下一节点地址改为新节点;(简单来说就是将插入位置的上一节点地址记录新的节点,将插入位置的上一个节点的下一节点地址记录新的节点,新节点的上一节点地址记录插入前该位置的上一节点地址,下一节点地址记录插入前该位置节点。)
-
当删除节点时,会先判断删除节点是否为第一个或最后一个,若是第一个或者最后一个就只需要将first或last记录删除节点的下一节点或上一节点即可;若不是第一个则会将删除节点的上一节点地址所记录的下一节点地址改为删除节点的下一节点地址,将删除节点的下一节点地址所记录的上一节点地址改为删除节点的上一节点地址,让链表都不记录删除的节点地址,然后后将删除的节点置为空,便于GC垃圾回收。
//部分源码
//元素个数
transient int size = 0;
//第一个节点
transient Node<E> first;
//最后一个节点
transient Node<E> last;
//节点
private static class Node<E> {
//item存放元素值
E item;
//next存放下一个节点地址
Node<E> next;
//上一个节点地址
Node<E> prev;
//传入参数为上一节点地址,该节点元素值,下一节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
//无参构造
public LinkedList() {
}
//有参构造(传入集合)
public LinkedList(Collection<? extends E> c) {
this();
//将传入的集合所有元素添加到新集合中
addAll(c);
}
//添加元素
public boolean add(E e) {
//在最后添加
linkLast(e);
return true;
}
void linkLast(E e) {
//将添加前的最后一个节点赋值给l
final Node<E> l = last;
//创建一个新节点(上一节点地址为l,新节点元素值为新添加的元素,下一节点为空)
final Node<E> newNode = new Node<>(l, e, null);
//新创建的节点为最后一个节点(相当于默认从最后追加)
last = newNode;
//判断添加前的最后一个节点是否为null
if (l == null)
//当一个节点都没有时会执行这部分
//将新添加的置为第一个节点
first = newNode;
else
//当添加前有节点时会执行这部分
//添加前最后一个节点的next记录新添加的最后一个节点的地址
l.next = newNode;
//元素个数+1
size++;
//修改次数+1
modCount++;
}
//插入元素
public void add(int index, E element) {
//判断插入的索引值是否规范(不能大于此时的元素个数且不能小于0)
checkPositionIndex(index);
//判断插入的元素是否放到最后
if (index == size)
linkLast(element);
else
//不放到最后就执行该方法(传入插入元素值和计算后的该索引值的节点地址)
linkBefore(element, node(index));
}
//根据插入的索引值得到该索引值的节点地址,传入参数为插入的位置(索引值)
Node<E> node(int index) {
// assert isElementIndex(index);
//判断插入的位置处于整个元素一半的左边还有右边(相当于二叉树,提升代码效率)
//左边为true,右边为false
if (index < (size >> 1)) {
Node<E> x = first;
//从前往后循环遍历直到插入元素的位置
for (int i = 0; i < index; i++)
//用来变换节点到下一个地址
x = x.next;
return x;
} else {
Node<E> x = last;
//从后往前循环遍历到插入元素的位置
for (int i = size - 1; i > index; i--)
//用来变换节点到上一个地址
x = x.prev;
return x;
}
}
//传入参数为插入的元素值和插入位置的地址
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
//创建一个新节点,新节点为插入的元素值节点(上一节点为传入的插入位置的上一节点,元素值,下一节点为传入的插入位置的地址)
final Node<E> newNode = new Node<>(pred, e, succ);
//将传入的插入位置的地址上一节点处记录新节点地址
succ.prev = newNode;
//判断插入的这个元素是否处于第一个位置
if (pred == null)
first = newNode;
else
//将插入位置的上一节点地址的下一节点处记录新节点地址
pred.next = newNode;
//元素个数+1
size++;
modCount++;
}
//删除
public boolean remove(Object o) {
//判断删除的对象是否为null
if (o == null) {
//从第一个节点开始遍历判断是否为null
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//判断删除元素的上一节点是否为null(删除的节点是否为第一个节点)
if (prev == null) {
//是第一个节点后,修改第一个节点记录为删除元素的下一节点地址
first = next;
} else {
//若不是第一个节点
//将删除的节点的上一节点地址的下一节点处 记录 删除的节点的下一节点地址
prev.next = next;
//将删除节点的上一节点置为null
x.prev = null;
}
//判断删除的节点是否为最后一个节点
if (next == null) {
//将最后一个节点地址重新赋值为删除节点的上一节点地址
last = prev;
} else {
//若不是最后一个节点
//将删除节点的下一节点的上一节点处 记录 删除节点的上一节点
next.prev = prev;
//将删除节点的下一节点置为null
x.next = null;
}
//将删除节点的元素值置为null(便于GC垃圾回收)
x.item = null;
//元素个数-1
size--;
modCount++;
//返回删除的元素值
return element;
}
//修改
public E set(int index, E element) {
//判断修改的索引值是否符合规范(不能小于0或者不能大于此时元素的总个数)
checkElementIndex(index);
//得到修改前该节点的地址
Node<E> x = node(index);
//得到修改前该节点的元素值
E oldVal = x.item;
//修改该节点的元素值
x.item = element;
//返回修改前节点的元素值
return oldVal;
}
//查询
public E get(int index) {
//判断修改的索引值是否符合规范
checkElementIndex(index);
//返回该索引节点的元素值
return node(index).item;
}
//返回元素个数
public int size() {
return size;
}