java集合类型源码解析之ArrayList
前言
作为一个老码农,不仅要谈架构、谈并发,也不能忘记最基础的语言和数据结构,因此特开辟这个系列的文章,争取每个月写1~2篇关于java基础知识的文章,以温故而知新。
如无特别之处,这个系列文章所使用的java版本都是1.8.0。
第一篇当然谈ArrayList了,因为这是java最常用的list集合类型,它内部使用数组作为存储空间,在增加元素时能够自动增长。总体来说,ArrayList的实现比较简单,这里不罗列它的全部代码,只看一些有意思的地方。
成员变量
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
- DEFAULT_CAPACITY 常量,初始话的时候如果没有指定capacity,使用这个值;
- EMPTY_ELEMENTDATA 空数组,所有空的ArrayList的elementData可以共享此值,避免使用null;
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA 也是elementData的值,代表初始化容量为默认值(非0)的存储空间,但是暂时还没有实际元素,等到需要增长存储空间时,与EMPTY_ELEMENTDATA的策略不一样;
- elementData 提供存储空间的数组;
- size list长度,list的元素存储在elementData[0~size)。
PS:说实话,DEFAULTCAPACITY_EMPTY_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;
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
- 第一种,指定初始容量;
- 第二种,使用默认容量,此时DEFAULTCAPACITY_EMPTY_ELEMENTDATA派上用场;
- 第三种,使用另一个集合来初始化,使用了集合的toArray方法,值得注意的是,集合的toArray返回的不一定是Object[]类型。
空间增长
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
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);
}
这是三个私有方法
- ensureCapacityInternal,在增长容量前加了一层检查,如果elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,确保容量不小于默认容量;
- ensureExplicitCapacity,检查数组的容量是否确实需要扩充;
- grow,执行数组扩容,先尝试扩容一半,看是否满足需求;也就是说空间的扩容,不是严格按照输入参数来的。
这里有一个疑问,ensureExplicitCapacity方法的那句modCount++
为什么会在if语句外执行.
增删改查
基本操作方法有很多,这里只列几个有代表性的。
1、查找:
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;
}
ArrayList查找元素的过程就是遍历数组,而且null也是可以查找的。
2、插入:
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++;
}
插入前做了范围检查,并确保容量,最后把插入位置之后的元素全部再往后挪一个位置;移动内存使用了System.arraycopy
,这是一个native方法。
值得注意的是,add方法并没有直接调用modCount++,因为ensureCapacityInternal里面调用了,所以我猜测modCount++放在ensureCapacityInternal里面纯碎就是为了更方便,即使有些没有修改数组的场景也会导致modCount被修改。
3、删除:
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);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
把删除位置之后的元素全部往前挪一个位置,注意的是,最后空出来的那个内存位置要置成null,否则会内存泄露。
4、批量删除
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
removeAll删除集合参数里的所有元素,反过来retainAll删除不在集合参数里面的所有元素,二者都通过batchRemove来实现。
batchRemove使用两个游标w,r来执行连续的元素移动,w代表写入位置,r代表读取位置,读取一个元素后判断是否应该删除,如果是那么继续读下一个,否则写入w位置。 这样,到了最后w位置就是list的末尾,w~r之间的位置需要置成null。
值得注意的是,最后的收尾逻辑放到finally里面,保证了一定程度的异常安全性。如果发生异常,那么剩余未扫描的元素(r位置之后的元素),要拷贝到w之后;这样一来,batchRemove可能执行了一半而失败,但ArrayList的状态并没有乱掉。
迭代器
ArrayList支持两种迭代器,Iterator和ListIterator,后者是前者的增强版本,可以向前移动、插入元素、返回元素索引。
这里就解读下Iterator实现,ListIterator是差不多的。
1、Iterator成员变量
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
Iterator实现为ArrayList的非静态内部内,因此可以直接访问ArrayList的成员字段,它只需要记住当前位置(cursor)即可。lastRet指向上一次next操作返回的元素位置,这个是非常有必要的,因为在迭代器上,如果要对元素做一些操作,都是针对这个位置的元素。
expectedModCount是ArrayListmodCount的快照,防止在迭代过程中,意外对list执行修改。
2、next操作
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
很简单,几乎没啥可解释的。
3、remove操作
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
remove操作会更新expectedModCount,这是一个允许的、意料之中的修改操作。
4、modCount
modCount定义在基类AbstractList当中,使用来追踪list修改的,自身的值没有实际意义。
modCount主要最用,就是在发生意外的修改时,快速失败,而不是等到出现难以追踪的数据状态错误时才失败。思路是,如果在执行一个操作的过程中,如果不期望ArrayList被其他操作所修改,那么可以在开始时记录一下modCount快照,在执行操作的过程中,通过比较ArrayList的modCount和这个快照来判定ArrayList是否被修改了,然后抛出ConcurrentModificationException。
ConcurrentModificationException看起来像是多线程相关的,但实际上这里和多线程一点关系都没有,ArrayList不是线程安全的,modCount的设计也没有针对多线程的意思。
SubList
ArrayList的subList可以直接通过游标锁定原ArrayList的一个区段,从而避免了copy。
由于SubList要实现List的全部接口,全部源码比较多,这里讲解一下get方法的实现,其他的都是类似的。
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
}
1、首先,构造函数可以看出来,SubList实际就是通过索引来操作原ArrayList的数据,只不过加一个偏移(offset)和长度限制(size)。
2、get方法检查modCount,说明在SubList的生命周期内,期望parent ArrayList不会被修改。这也说明SubList只适合做临时变量,不适合长期存活,除非原ArrayList是不变的。
AbstractList
AbstractList是ArrayList的基类,它也实现了Iterator,ListIterator,SubList的一个版本。不过AbstractList并不清楚存储的实现细节,因此只能基于list的公共接口来实现,因此效率肯定不佳。
比如AbstractList的Iterator的next方法是这样实现的:
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
获取元素用的是list.get方法: E next = get(i)
。
java集合类型源码解析之ArrayList的更多相关文章
- java集合类型源码解析之PriorityQueue
本来第二篇想解析一下LinkedList,不过扫了一下源码后,觉得LinkedList的实现比较简单,没有什么意思,于是移步PriorityQueue. PriorityQueue通过数组实现了一个堆 ...
- Java集合---LinkedList源码解析
一.源码解析1. LinkedList类定义2.LinkedList数据结构原理3.私有属性4.构造方法5.元素添加add()及原理6.删除数据remove()7.数据获取get()8.数据复制clo ...
- java集合-HashSet源码解析
HashSet 无序集合类 实现了Set接口 内部通过HashMap实现 // HashSet public class HashSet<E> extends AbstractSet< ...
- java集合-HashMap源码解析
HashMap 键值对集合 实现原理: HashMap 是基于数组 + 链表实现的. 通过hash值计算 数组索引,将键值对存到该数组中. 如果多个元素hash值相同,通过链表关联,再头部插入新添加的 ...
- java基础类型源码解析之String
差点忘了最常用的String类型,我们对String的大多数方法都已经很熟了,这里就挑几个平时不会直接接触的点来解析一下. 先来看看它的成员变量 public final class String { ...
- java基础类型源码解析之HashMap
终于来到比较复杂的HashMap,由于内部的变量,内部类,方法都比较多,没法像ArrayList那样直接平铺开来说,因此准备从几个具体的角度来切入. 桶结构 HashMap的每个存储位置,又叫做一个桶 ...
- 【java集合框架源码剖析系列】java源码剖析之ArrayList
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 本博客将从源码角度带领大家学习关于ArrayList的知识. 一ArrayList类的定义: public class Arr ...
- 【源码解析】- ArrayList源码解析,绝对详细
ArrayList源码解析 简介 ArrayList是Java集合框架中非常常用的一种数据结构.继承自AbstractList,实现了List接口.底层基于数组来实现动态容量大小的控制,允许null值 ...
- 【java集合框架源码剖析系列】java源码剖析之HashMap
前言:之所以打算写java集合框架源码剖析系列博客是因为自己反思了一下阿里内推一面的失败(估计没过,因为写此博客已距阿里巴巴一面一个星期),当时面试完之后感觉自己回答的挺好的,而且据面试官最后说的这几 ...
随机推荐
- Vue 单元测试 使用mocha+jest
目录 Vue 单元测试 mocha+jest jest 实例 mocha expect方法断言 示例代码 Vue 单元测试 官网:https://vue-test-utils.vuejs.org/zh ...
- H5表单新特性
1.HTML5表单新特性之——新的input type <input type=" "> HTML5之前已有的input type: text.password.rad ...
- 多个div并排不换行
1.所有div的父元素不换行 white-space: nowrap; 2.所有div设置为行内元素 display: inline-block; 基于java记账管理系统[尚学堂·百战程序员]
- 五、MySQL系列之高级知识(五)
本篇 主要介绍MySQL的高级知识---视图.事件.索引等相关知识: 一.视图 在学习视图时我们需要什么是视图,视图有哪些好处以及视图的相关操作: 1.1 什么是视图? 关于视图通俗来讲就是一条se ...
- [ipsec][strongswan] strongswan源码分析--(四)plugin加载优先级原理
前言 如前所述, 我们知道,strongswan以插件功能来提供各种各样的功能.插件之间彼此相互提供功能,同时也有可能提供重复的功能. 这个时候,便需要一个优先级关系,来保证先后加载顺序. 方法 在配 ...
- 管理Linux软件——aptitude
https://help.ubuntu.com/lts/serverguide/aptitude.html.en
- Vim热键总结
最近学习linux环境,总结一下Vim的常用热键~~~
- python函数名的应用、闭包和迭代器
一.函数名的应用(第一类对象) 函数名是一个变量,但它是一个特殊的变量,与括号配合可以执行函数变量. 1.函数名的内存地址 def func(): print("哈哈") prin ...
- 关于部署 Kafka 的一些所得
前记 最近在做日志模块. 其中用到的日志信息传输的中间工具用的是的 Kafka,在自己的摸索中总是有一些问题的,在这里记录下来. Kafka 环境搭建 首先是下载需要的安装软件. JDK.Zooke ...
- Robot Framework--接口实例一
需求:api/car/detail/recommendcar.json 接口返回的车辆数量少于等于20且车辆不能重复 分析:统计接口中返回的列表的长度,再把carid拿出来组成一个新的列表,判断这 ...