前言:
ArrayList底层是依靠数组实现的,而LinkedList的实现是含前驱后继节点的双向列表。平时刷题时会经常使用到这两个集合类,这两者的区别在我眼中主要是ArrayList读取节点平均时间复杂度是O(1)级别的,插入删除节点是O(n);LinkedList读取节点时间复杂度是O(n),插入节点是O(1)。
本文记录我对jdk1.8下的ArrayList和LinkedList源码中主要内容的学习。
1、ArrayList
1.1 主要成员变量
1 //默认容量
2 private static final int DEFAULT_CAPACITY = 10;
3 //空的数组
4 private static final Object[] EMPTY_ELEMENTDATA = {};
5
6 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
7 //数据数组
8 transient Object[] elementData; // non-private to simplify nested class access
9 //当前大小
10 private int size;
主要成员变量如上,最重要的就是size和elementData,其中elementData的修饰transient一开始很令我费解,查阅资料后豁然开朗,transient是为了序列化ArrayList时不用Java自带的序列化机制,而用ArrayList定义的两个方法(writeObject、readObject),实现自己可控制的序列化操作,防止数组中大量NULL元素被序列化。
1.2 主要方法
1.2.1 构造方法
构造方法源码其实很简单,不过在此提及是为了给后面扩容引出一个思考。
1 public ArrayList(int initialCapacity) {
2 if (initialCapacity > 0) {
3 this.elementData = new Object[initialCapacity];
4 } else if (initialCapacity == 0) {
5 this.elementData = EMPTY_ELEMENTDATA;
6 } else {
7 throw new IllegalArgumentException("Illegal Capacity: "+
8 initialCapacity);
9 }
10 }
11
12
13 public ArrayList() {
14 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
15 }
源码如上,一个不带参数的构造器,以及带容量参数的构造器。
1.2.2 add方法
1 public boolean add(E e) {
2 ensureCapacityInternal(size + 1); // Increments modCount!!
3 elementData[size++] = e;//加到末尾
4 return true;
5 }
6
7 private void ensureCapacityInternal(int minCapacity) {
8 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
9 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
10 }
11
12 ensureExplicitCapacity(minCapacity);
13 }
14
15 //判断是否需要扩容
16 private void ensureExplicitCapacity(int minCapacity) {
17 modCount++;
18
19 // overflow-conscious code
20 if (minCapacity - elementData.length > 0)
21 grow(minCapacity);
22 }
add方法中先用ensureCapacityInternal方法,首先判断是否位第一次add,也就是初始化。如果数组位空,那么DEFAULT_CAPACITY就是为10。然后判断是否需要扩容,如果原size+1比数组的length大就需要扩容。(扩容)后把要加的元素加到末尾即可。
1.2.3 扩容方法(grow)
1 private void grow(int minCapacity) {
2 // overflow-conscious code
3 int oldCapacity = elementData.length;
4 int newCapacity = oldCapacity + (oldCapacity >> 1);
5 if (newCapacity - minCapacity < 0)
6 newCapacity = minCapacity;
7 if (newCapacity - MAX_ARRAY_SIZE > 0)
8 newCapacity = hugeCapacity(minCapacity);
9 // minCapacity is usually close to size, so this is a win:
10 elementData = Arrays.copyOf(elementData, newCapacity);
11 }
扩容方法如上,hugeCapacity判断minCapacity是否大于ArrayList上限,如果大于就返回ArrayList的容量上限。用Arrays.copyof新生成一个数组,而newCapacity = oldCapacity + (oldCapacity >> 1)则是将容量变为原来的1.5倍。
因为ArrayList默认初始容量为10,每次扩容将容量变为1.5倍,而如果使用ArrayList时要一次性add100个元素,则会频繁用调用扩容方法,因此可以在初始化ArrayList时使用带参的构造函数,定一个合适的容量值。
1.2.4 remove方法
1 public E remove(int index) {
2 rangeCheck(index);
3
4 modCount++;
5 E oldValue = elementData(index);
6
7 int numMoved = size - index - 1;
8 if (numMoved > 0)
9 System.arraycopy(elementData, index+1, elementData, index,
10 numMoved);
11 elementData[--size] = null; // clear to let GC do its work
12
13 return oldValue;
14 }
15
16 public boolean remove(Object o) {
17 if (o == null) {
18 for (int index = 0; index < size; index++)
19 if (elementData[index] == null) {
20 fastRemove(index);
21 return true;
22 }
23 } else {
24 for (int index = 0; index < size; index++)
25 if (o.equals(elementData[index])) {
26 fastRemove(index);
27 return true;
28 }
29 }
30 return false;
31 }
remove方法主要有两种,一种是根据下标remove,另一种是根据传入的元素匹配删除第一个遇到的该元素,值得一提的是可以删除null元素(总感觉怪怪的)。
2、LinkedList
LinkedList是一个双向链表,可以当(双端)队列用。
2.1 主要成员变量
1 transient int size = 0;
2
3 //头节点
4 transient Node<E> first;
5
6 //尾节点
7 transient Node<E> last;
8
9 //Node节点
10 private static class Node<E> {
11 E item;
12 Node<E> next;//前驱
13 Node<E> prev;//后继
14
15 Node(Node<E> prev, E element, Node<E> next) {
16 this.item = element;
17 this.next = next;
18 this.prev = prev;
19 }
20 }
带首尾的双向列表,加一个size变量记录当前节点数量,transient修饰和ArrayList中修饰数组的原因是一样的,同样实现了writeObject和readObject,自己实现把size和每一个节点都序列化和反序列化了。
2.2 主要方法
2.2.1 add方法
1 //add方法添加元素到末尾
2 public boolean add(E e) {
3 linkLast(e);
4 return true;
5 }
6
7 //添加元素至末尾
8 void linkLast(E e) {
9 final Node<E> l = last;
10 final Node<E> newNode = new Node<>(l, e, null);//新建元素,把前驱节点置为原来的last节点
11 last = newNode;
12 if (l == null)//如果尾节点是空(说明头节点也是空的),就把头节点设置成新节点
13 first = newNode;
14 else//原来尾节点的后继设置成新节点
15 l.next = newNode;
16 size++;
17 modCount++;
18 }
一种add就是上面代码的加到末尾,分析都在注释中了。另一种则是添加到指定index,add的平均时间复杂度为O(n)。
1 public void add(int index, E element) {
2 checkPositionIndex(index);
3
4 if (index == size)
5 linkLast(element);//index是最后一个就直接插到最后
6 else
7 linkBefore(element, node(index));
8 }
9
10 //将节点插入到目标节点前面
11 void linkBefore(E e, Node<E> succ) {
12 // assert succ != null;
13 final Node<E> pred = succ.prev;
14 final Node<E> newNode = new Node<>(pred, e, succ);//将插入节点的前驱设置成目标节点的前驱
15 succ.prev = newNode;
16 if (pred == null)//同linkLast中设置后驱节点为目标节点
17 first = newNode;
18 else
19 pred.next = newNode;
20 size++;
21 modCount++;
22 }
2.2.2 remove方法
remove方法和add方法类似,由于是双端队列,因此需要改变删除节点的前驱和后继节点的后继和前驱。在此不再展开描述。
2.2.3 get方法
get方法在此不贴源码了,由于是双端队列,因此如果查找的下标大于size的一半,就从后面往前遍历,虽然时间复杂度还是o(n)级别的,不过也算是一个小优化吧。
本篇简略的对jdk1.8下的ArrayList和LinkedList源码实现进行了分析,期间被几个命名奇怪的方法勾引走了,比如ArrayList的trimToSize,可以将数组多余的(大于size)的部分“删掉”。也学到了不少(emmm,好像没有特别多)东西。本篇博客算是对学习过程的一个记录吧。(才不会说是好久没更新博客了要懒死了QAQ)。
来源:oschina
链接:https://my.oschina.net/u/4396372/blog/3913091