一,Arraylist 在我们开发中经常用到,今天就来研究一下它的实现原理,由源码可以看出,其实它内部维护了一个数组
/** * Shared empty array instance used for empty instances. */ private static final Object[] EMPTY_ELEMENTDATA = {};
所以,它的增加,删除,修改,查询都是对这个数组的操作。
1.下面来研究一下add方法:
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
ensureCapacityInternal(size + 1);是对数组的容量进行检测和扩容
private void ensureCapacityInternal(int 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); }
int newCapacity = oldCapacity + (oldCapacity >> 1);由这里可以看出,其扩容规则是把原来容量再加上原来容量的一半。
顺便提了一下扩容规则,在进行容量检测完毕后调用了elementData[size++] = e;意思就是把新加进来的数据放到数组的最后一位。
2.把数据添加到指定位置add(int index,E e)
把数据添加到指定位置的思路是把index后面的所有数据copy到index+1的位置,相当于把index后面的数据统一往后移一位, 然后再把新的数据添加到index的位置。
public void add(int index, E element) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); //和add方法一样,首先先进行容量检测及扩容操作 ensureCapacityInternal(size + 1); // Increments modCount!! //和add方法的区别 把size - index(即index后面)的所有数据copy到数组的index + 1的位置。 System.arraycopy(elementData, index, elementData, index + 1, size - index); //把新数据放到index的位置 elementData[index] = element; size++; }
3.删除指定位置数据 remove(int index)
原理和把数据添加到指定位置差不多,把index位置的数据删除,把index位置后面的所有数据往前移动一位
public E remove(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); modCount++; //获取到index位置的数据 E oldValue = (E) elementData[index]; //index后面所有数据的开始下标 int numMoved = size - index - 1; //如果要删除的不是最后一位数据 if (numMoved > 0) //开始删除并移动数据 System.arraycopy(elementData, index+1, elementData, index, numMoved); //把数组的size-1并把最后一位置空 elementData[--size] = null; // clear to let GC do its work return oldValue; }
4.删除指定数据 remove(Object object)
思路就是通过for循环遍历数据,取出每一个值和object进行比较,如果匹配到要删除的数组则把index位置的数据删除,把 index位置后面的所有数据往前移动一位。
public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { //通过for循环遍历数组 for (int index = 0; index < size; index++) //比较是否是需要删除的数据 if (o.equals(elementData[index])) { //匹配成功开始删除 fastRemove(index); return true; } } return false; } /* * Private remove method that skips bounds checking and does not * return the value removed. */ //这里和删除指定位置的数据操作一样 private void fastRemove(int index) { modCount++; 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 }
其它方法也都是基于对数组的操作,这里就不分析了,由于Arraylist是基于数组实现的,所以它的查询效率相对比较高,但是删除和添加效率会比较低,因为涉及到数据的复制移动。
二,分析完Arraylist再来看一下LinkedList吧
LinkedList和Arraylist不同,它是一个双向链表,里面有我们要存储的数据节点,每个节点有三个区域 《前指针域--数据域--后指针域》前指针域指向前一个节点,后指针域指向后一个节点,这样相邻的两个数据都相互存在指向。
如图,A B C代表三个节点,假如要删除b节点那么只需要把A节点的后指针域指向C,把C的前指针域指向A,那么B与A和C的连接将自动断开链表中就不再有B这个元素,明白了这个原理那添加也不难了,只是对指针指向进行操作。
下面来看一下源码:
LinkedList会初始化第一个结点和最后一个结点,当添加或删除的时候会作为参照使用。
/** * Pointer to first node. * Invariant: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient Node<E> first; /** * Pointer to last node. * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last;
1. 从尾部添加
/** * Links e as last element. */ void linkLast(E e) { //拿到尾部的结点 final Node<E> l = last; //创建一个新结点,并把它的前指针域设置为l,把它的后指针域设置为空 final Node<E> newNode = new Node<>(l, e, null); //更新last为新的最后一个结点 last = newNode; //如果l为空说明列表是空的,直接把新添加的结点置为first,不为空就把l(旧的尾结点)的后指针域指向新的尾结点。 if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
2. 从中间添加
首先看一下add方法
public void add(int index, E element) { checkPositionIndex(index); //这里会判断下传入的index,如果index==size说明可以直接调用linkLast方法加到尾部 if (index == size) linkLast(element); else //这里通过node(index)方法拿到index位置的结点 linkBefore(element, node(index)); }
//否则才会执行从中间添加
/** * Inserts element e before non-null Node succ. */ void linkBefore(E e, Node<E> succ) { // assert succ != null; //拿到index位置结点的前指针域指向 final Node<E> pred = succ.prev; //新建结点,并把新结点的前指针域指向index结点的前指针域,把它的后指针域指向index结点。 final Node<E> newNode = new Node<>(pred, e, succ); //把index结点的前指针域指向新结点 succ.prev = newNode; //如果pred==null说明需要加入的位置为链表的第一个位置,则直接把新结点置为first,否则把index结点的前指针域结点的后指针域指 //向新结点 //这样新节点就被插入了index结点的前面,成为了index结点 if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
3. 从头部添加
看了上面的下面这个就很好理解了
/** * Links e as first element. */ private void linkFirst(E e) { //首先拿到链表的第一个结点 final Node<E> f = first; //新建结点,并把它的前指针域指向空,因为第一个结点前面没有结点了,并把它的后指针域指向第一个结点 final Node<E> newNode = new Node<>(null, e, f); //把新结点置为first first = newNode; //当f == null时说明链表为空,那么新加进来一个后 last 和 first指向同一个结点,否则把f。前置指针域指向新节点,那么f就变成 //了第二个结点 if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; }
4. 删除第一个结点
/** * Unlinks non-null first node f. */ private E unlinkFirst(Node<E> f) { // assert f == first && f != null; //获取到一个结点的数据 final E element = f.item; //获取到一个结点的后指针域指向(即第二个结点) final Node<E> next = f.next; //把它的数据指向置为null f.item = null; //断开它与下一个结点的连接 f.next = null; // help GC //把第二个结点置为first first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; }
添加的原理是建立指向,删除的原理就是断开指向,其他就不一一讲解了。
三,关于单向链表和双向链表
单向链表只有一个指针域,只能指向下一个结点,如下是它的结点模型:
如果想删除B只需要把A的指针指向C即可。
单向链表查询只能从前向后,而双向链表还可以从后往前,因为它有两个指针域,查询时可根据index值在size中的位置来确定是从前面查还是从后面查,这样可以提高查询效率。