前言:
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)。