源码解析ArrayList,LinkedList,CopyOnWriteArrayList
以下是我的blog,采用前后端分析,后端springboot,前端vue+iview实现,也算是原文地址了.点击跳转
1. 环境及介绍
1.1 环境
- JDK环境: 1.8
- 工具: IDEA
1.2 介绍
List集合是我们日常编码中常用的类型,但是并不是每个人都会有一个看源码的习惯.虽然我们也是应用层开发,但是理解底层实现更能帮助我们写出高效的代码.本文就对常用的一些类型通过源码来进行备注.如有错漏,烦请同行告知.
2. 类层级及结构分析
本文只是介绍部分实现,囊括范围为常用类型
2.1 Iterable接口
2.1.1 描述
官方文档如是说: “实现此接口,允许一个对象使用for-each循环.”,这就是讲,我们实现了该接口就能够进行遍历元素操作.
2.1.2 功能
我们可以把它看做一个标志,是否支持遍历
2.2 Collection接口
2.2.1 描述
集合结构中的根接口,也称为集合最上层.集合表示一组对象.单个对象被称为元素.一些集合允许重复元素存在,而另一些则不允许.一些集合是有序的,一些是无序的.JDK没有提供该接口的任何具体实现.但是提供了子接口.它们实现了Collection接口.而本接口通常用于传递集合,在没有具体类型出现的时候不需要转换就可以.这也是java的特性多态的场景.
2.2.2 功能
作为一个集合结构的制定者,这就是抽象的魅力.它囊括了集合的所有基本操作.虽然没有提供具体实现,但是它却是一种规范化.可以把它看做一个模板,后边的具体实现是这个模板的衍生.
2.3 List接口
2.3.1 描述
有序集合,接前文Collection接口中有有序和无序之差.而List接口则是有序,可重复集合的根接口.我们可以在集合的任何位置进行插入操作.也可以通过下标进行随机访问.
2.3.2 功能
所有有序可重复集合父类.在没有确定子类的具体类型时我们会使用,而常用的是用子类引用指向父类对象.eg. List list = new ArrayList();
2.4 AbstractList
2.3.1 描述
AbstractList类是抽象类,此类提供了List接口的基本实现.最大限度支持"随机访问", 对于顺序访问数据,建议优先使用AbstractSequentialList.
2.3.2 方法解析
2.3.2.1 未实现方法
本类中有很多方法是没有具体实现的,因为本来是抽象类.它把具体的方法实现交给了子类.以下列出未实现方法.
add(E e);add(int index, E element);remove(int index);set(int index, E element);
当调用这些方法,而子类又没有具体实现时,会抛出 throw new UnsupportedOperationException()异常,不支持操作异常
此中 get(int index); 方法是没有实现,子类实现必须实现该方法.
2.3.2.2 已经实现方法
这里我们选一个通过元素寻找下标方法解析.
public int indexOf(Object o) {
ListIterator<E> it = listIterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return it.previousIndex();
} else {
while (it.hasNext())
if (o.equals(it.next()))
return it.previousIndex();
}
return -1;
}
public ListIterator<E> listIterator() {
return listIterator(0);
}
public ListIterator<E> listIterator(final int index) {
rangeCheckForAdd(index);
return new ListItr(index);
}
private void rangeCheckForAdd(int index) {
if (index < 0 || index > size())
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
以上代码ListItr 为内部类.首先通过listIterator获取遍历器.然后判断传入对象是否为空,如果为空则获取下一个,直到下一个为null,返回当前元素的前一个下标.因为是返回下标,所以是从0开始.如果传入对象不为空,则进行判断是否下一个元素等于传入对象.如果等于,则返回前一个下标.
2.5 ArrayList
2.3.1 描述
ArrayList是由可调整大小的数组实现,实现了所有List中的方法,此类还提供了操纵内部数组大小的方法.
ArrayList不同步.每个ArrayList 都有容量,是可以动态增加的,其扩容是通过数组复制实现的,即新建一个数组,将原数组复制.如果频繁扩容效率极低.扩容的阈值为负载因子*容量.具体参数请看构造方法
2.3.2 方法解析
构造方法
// 无参构造,生成对象,将对象中目标数据定义为默认 空对象数组.
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 携带集合入参,将集合转换为数组,然后通过Arrays.copyOf 将原数组复制到新数组中.
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;
}
}
// 携带容量入参.入参大于0则取入参,如果等于0,则设置目标数组为空(后续增加就会触发一次扩容),否则异常抛出
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);
}
}
操作方法
add-添加一个元素
// 将一个元素插入到数组末位
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// 判断数组是否需要扩容
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 如果当前数组为默认空数组,则进行使用入参容量与默认容量(16)比较取最大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//
private void ensureExplicitCapacity(int minCapacity) {
// 结构变化计数字段,调用add,remove都会操作该字段
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);
// 如果扩容后长度小于传入长度,则取传入长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 如果扩容后长度大于默认的最大长度(Integer.MAX_VALUE - 8 21亿多吧)则取Integer.MAX_VALUE
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
get-获取一个元素
// 获取指定位置的元素
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
// 如果长度大于数组的长度,异常抛出
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 这里就可以验证,list集合底层是数组结构了,通过下标取元素
E elementData(int index) {
return (E) elementData[index];
}
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);
// 将尾部元素置为null,这是类似手动提示GC的方式,不一定会被马上回收
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
2.6 LinkedList
2.3.1 描述
非同步容器,双链表结构,可以通过头部查找或者尾部查找
2.3.2 方法解析
构造方法
// 此处省略无参构造,本方法是集合类入参
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
// size为链表长度,即大小
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
// 长度校验,如果不满足大于0小于等于链表长度,则异常
checkPositionIndex(index);
// 将集合转换为数组
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
// Node为静态内部类,其结构由三部分组成,自身数组,下一元素引用,上一元素引用.这就是被称为双向链表的原因
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
// 如果链表末位为null,则表明该链表为空,直接设置为链头
if (pred == null)
first = newNode;
else
// 将新元素查到末位
pred.next = newNode;
// 设置为末位
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
add-增加或者插入一个元素
// 还可以通过下标加元素,添加指定位置,此处只讲元素插入末位
public boolean add(E e) {
linkLast(e);
return true;
}
// 新元素插到末位
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++;
}
get-获取一个元素
public E get(int index) {
// 判断元素下标是否大于等于0,小于当前长度,不满足抛出下标越界异常
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
// 这里使用了二分查找,即判断下标是否超过半数,超过则从末位开始查找,否则从首位开始
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;
}
}
poll-获取并移除链头元素
// 获取并移除首位
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
// 移除头尾元素,并将下一位置位链头
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
peel-获取链头元素,不移除
// 简单的只是获取元素
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
注: 这里面其实还有很多方法,需要去看源码或者api,不过建议jdk还是看源码,三方框架看手册
2.7 CopyOnWriteCopyOnWriteArrayList
2.3.1 描述
线程安全的集合,使用lock可重入锁实现。基于集合的增加和删除,都是通过数组的复制来做的,频繁扩容会导致效率低下。
2.3.2 方法解析
构造方法
// 此处省略无参构造,讲解集合入参
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
// 如果入参类型与当前类一致,就直接转数组赋值
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
// 如果不一致,先转数组赋值
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
// 如果不是Object对象,则进行复制
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
add-添加一个元素至集合末位
// 这里使用了lock锁,lock是基于api层面的,优化空间较小了.
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 底层任然是数组的复制
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 设置回当前类的目标数据数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
get-获取指定下标元素
// 这里居然没有使用锁,我们可能会考虑是否真的能够线程安全.其实可以仔细看看private transient volatile Object[] array; 它的目标数组是volatile修饰,也就是说内存透明,让工作内存每次去读取主内存数据,这样一来就能够做到数据及时性.因为我们的锁更多的是锁写入.这里不适用锁,就会更高效了
public E get(int index) {
return get(getArray(), index);
}
// 数组方式通过小标获取元素
private E get(Object[] a, int index) {
return (E) a[index];
}
remove-删除一个指定下标元素
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 在目标数组取下标返回元素
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
// 如果删除的元素不是末尾,则进行数组的复制
setArray(Arrays.copyOf(elements, len - 1));
else {
// 如果是末尾,进行复制,抛弃末尾即可
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
// 将变更后数组赋值到目标数组
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
3. 小结
本文主要以源码为根基,浅略的讲解了一些方法和它的一点点描述,其实更多的部分还是需要以源码入手,自己看一看才能够有所收获。jdk中的很多思路其实跟现有软件中的一些实现是相通的。本文浅显,如有遗漏错论,希望各位同行提意见。