首先,查看这两种数据结构在源码中是怎样定义和实现的
ArrayList源码解析:参考《ArrayList源码分析(基于JDK8)》;
LinkedList源码解析:参考《LinkedList源码分析(基于JDK8)》
我摘取了一些常见的方法的实现方式
ArrayList
成员变量
//父类AbstractList中的变量,此变量表示ArrayList集合的修改次数,如扩容次数 //protected transient int modCount = 0; // 默认初始的容量 private static final int DEFAULT_CAPACITY = 10; // 一个空数组对象 private static final Object[] EMPTY_ELEMENTDATA = {}; // 一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值 //区分开EMPTY_ELEMENTDATA,此数组用于在添加第一个元素时要扩容多少。 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 当前数据对象存放地方,当前对象不参与序列化 transient Object[] elementData; // 当前数组长度 private int size; // 数组最大长度 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
构造方法
//无参构造 public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;} //初始化时,elementData中的长度是1,size是0,当进行第一次add的时候,elementData会变成默认长度10 //带int类型的有参构造 //initialCapacity表示自定义的初始化长度 public ArrayList(int initialCapacity) { //如果初始化长度大于或等于0,就按照自定义的长度初始化 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { //如果小于0,抛出异常 throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); }
方法
add(E e)
(下面是我整理过的,把调用的方法写到一个里面了)
1)确保数组已使用长度(size)加1之后足够存储下一个数据
2)修改次数modCount 标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,将当前数组的长度变为原来容量的1.5倍(数组的复制)
3)确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上public boolean add(E e) { int minCapacity = size + 1; //确保添加的元素有地方存储,当第一次添加元素的时候this.size +1的值是1,所以第一次添加的时候会将当前elementData数组的长度变为10 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } //修改次数(modCount)自增1 modCount++; //判断是否需要扩充数组长度 //如果当前数组已使用长度(size)加1后的大于当前的数组长度 //会将当前数组的长度变为原来容量的1.5倍 if (minCapacity - elementData.length > 0){ int arg1 = this.elementData.length; int arg2 = arg1 + (arg1 >> 1); if (arg2 - arg0 < 0) { arg2 = arg0; } if (arg2 - 2147483639 > 0) { arg2 = hugeCapacity(arg0); } this.elementData = Arrays.copyOf(this.elementData, arg2); } elementData[size++] = e; return true; }
get
返回指定位置的元素
public E get(int index) { rangeCheck(index); //通过比较modCount,抛出并发修改异常 checkForComodification(); return ArrayList.this.elementData(offset + index); }
set
确保set的位置小于当前数组的长度(size)并且大于0,获取指定位置(index)元素,然后放到oldValue存放,将需要设置的元素放到指定的位置(index)上,然后将原来位置上的元素oldValue返回给用户。
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; }
remove
按照索引删除
1. 判断索引越界,自增修改次数,
2. 找到索引处元素保存到oldValue(便于返回);
3. 将索引后的元素都向前移动一位,将最后一个索引置空,便于GC回收。
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; }
按照对象删除
1. 循环遍历所有对象,得到对象所在索引位置index,
2. 调用fastRemove方法:先将index后面的元素往前面移动一位(System.arraycopy实现),然后将最后一个元素置空。
public void remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; }
LinkedList
它和ArrayList继承或实现的类和接口不同之处在于,它继承AbstractSequentialList,实现Deque接口,是双向队列
成员变量
//节点数量 transient int size = 0; //第一个节点 transient Node<E> first; //最后一个节点 transient Node<E> last;
在其内部还有一个header属性,用来标识起始位置,first和last指向header,因此形成了一个双向的链表结构
构造方法
//无参构造 public LinkedList() {}
方法
add(E e)
将新增的元素放置链表的最后面,然后链表的长度(size)加1,修改的次数(modCount)加1
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++; }
add(int index, E element)
指定位置(索引 index)往数组链表中添加元素
1. 检查index是否小于等于当前的长度链表size,且要求大于0
2. 如果index等于size,直接在链表的最后面添加元素,相当于调用add(E e)方法;否则先找到index索引,然后在index的位置前面添加新增的元素。
get
1. 判断索引越界
2. 遍历链表的元素。注意:先判断从头遍历还是从尾遍历,将索引的位置与当前链表长度的一半做对比,如果索引位置小于当前链表长度的一半就从头遍历,否则从结尾开始遍历
public E get(int index) { checkElementIndex(index); return node(index).item; } //返回指定元素索引处的(非null)节点。 //遍历链表元素 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; } }
set
检查索引越界,如果没有,将index位置的节点内容替换成新的内容element,同时返回旧值。
public E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal; }
remove
remove方法调用的removeFirst()
public E remove() {return removeFirst();}
removeFirst
移除第一个节点,将第一个节点置空,让下一节点变成第一节点,链表长度减1,修改次数加1,返回移除的第一个节点
public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return 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; }
总结
从上我们可以看出这两种数据结构的实现方式和对数据的操作方式的不同。
1. ArrayList是基于动态数组的数据结构,数据都在同一地址值上;LinkedList是基于实现链表的数据结构,每个数据都有各自的节点地址(Node)
2. 数据操作
2.1 查询
源码中,ArrayList的get(int index)方法是直接定位数组的索引查询;LinkedList的get(int index)先将索引与当前链表长度的一半比较,索引小于当前链表长度的一半从头遍历,否则从尾遍历。所以ArrayList的速度会优于LinkedList
(ArrayList实现了RandomAccess接口,使用二分查找法,使用的随机访问(random access)策略)
2.2 添加(删除同理)
ArrayList进行添加元素,需要判断是否需要扩容(初始容量为10),如需要则扩容为原容量的1.5倍,扩容方式为数组的复制,有一定开销;此外,对于数组中元素的位移,add(E e)方法是插入到最后,不需要位移;add(int index,E e)方法是插入到指定索引,位置越靠前,需要位移元素越多,开销越大,相反,插入位置越靠后的话,开销越小
LinkedList有标记头部和尾部的节点,所以额外提供有添加元素到头部和尾部的方法,且在其中间插入数据开销是固定的。
2.3 修改/更新
ArrayList只需要找到对应数组的索引进行更新;
LinkedList需要先遍历所有节点,在对应索引修改后,将指针修改指向新数据的索引。
2.4 选择
在需要频繁读取集合中的元素时,使用ArrayList效率较高,而在插入和删除操作较多时,使用LinkedList效率较高