Java集合ArrayList与LinkedList详解
作者:亚雷
前言
对于Java程序员,可以说对于ArrayList
和LinkedList
可谓是十分熟悉了
对于ArrayList和LinkedList,他们都是List接口的一个实现类,并且我们知道他们的实现方式各不相同,例如ArrayList底层实现是一个数组,而LinkedList底层实现是链表,对于数组来说,插入慢但是查询快,而对于链表来说查询慢,插入快
今天我们就来分析分析他们的源码
ArrayList
我们先看一看ArrayList的类图。他继承于AbstractList,而这个类是List类的的骨架,可以说这个类是List类的基石
成员属性
/** 序列化ID **/ private static final long serialVersionUID = 8683452581122892189L; /** 默认容量 **/ private static final int DEFAULT_CAPACITY = 10; /** 如果传入的容量为0,那么将使用这个空元素数组 **/ private static final Object[] EMPTY_ELEMENTDATA = {}; /** 默认空元素数组,长度为0,传入元素时会初始化为默认元素大小 **/ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** 真正存储数据的数组 **/ transient Object[] elementData; /** 列表中元素的个数 **/ private int size;
这里需要主要关注的成员属性为size
和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; } /** 传入一个集合,将该集合的元素初始化到ArrayList种 **/ public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
扩容机制
我们知道ArrayList是一个动态数组,但是他的底层实现是一个数组,那他是怎样保证动态的呢?
ArrayList每次添加元素之前,都会检查添加元素后的元素个数是否超过数组长度,如果超出,那么就会进行扩容,而数组扩容通过一个公开的方法实现,我们也可以手动进行扩容
public void ensureCapacity(int minCapacity) { //判断数组是否等于默认空数组,不等于则给minExpand赋值为10 int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0 : DEFAULT_CAPACITY; //判断参数是否大于minExpand大于的时候才会去扩容 if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } } private void ensureExplicitCapacity(int minCapacity) { modCount++;//记录数组修改次数 + 1 if (minCapacity - elementData.length > 0) grow(minCapacity); } //真正扩容的方法 private void grow(int minCapacity) { //获取原来的容量 int oldCapacity = elementData.length; //计算新容量,新容量大小 = 旧容量大小的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); //如果需要的容量比新容量还小就用需要的容量 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //如果新容量大于最大容量就用最大的容量 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); //数据拷贝 elementData = Arrays.copyOf(elementData, newCapacity); }
我们可以发现每次扩容,ArrayList都会扩容1.5倍,通过位运算完成计算扩容大小的,我们知道,扩容之后进行数据迁移这个操作是很费时间的,比如我们有10w条数据,这样的话,进行数据迁移的时候,我们会耗费很长时间,所以我们建议再初始化的时候就定义一个容量,这样可以让ArrayList的效率提高很多
add方法
//添加元素到尾部 public boolean add(E e) { //检查是否需要扩容 ensureCapacityInternal(size + 1); //将元素添加到数组末尾,并把size++ elementData[size++] = e; return true; } //添加元素到指定索引 public void add(int index, E element) { //检查index是否越界 rangeCheckForAdd(index); //检查是否需要扩容 ensureCapacityInternal(size + 1); //将index以及之后的元素向后挪动一位 //这样index就能添加元素了 System.arraycopy(elementData, index, elementData, index + 1, size - index); //添加元素到index的位置 elementData[index] = element; size++; } //将集合c的元素添加到list中 public boolean addAll(Collection<? extends E> c) { //把c转换为数组 Object[] a = c.toArray(); //拿到c的长度 int numNew = a.length; //检查是否需要扩容 ensureCapacityInternal(size + numNew); // Increments modCount //将a数组拷贝到原来数组的末尾 System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; } //将集合c的元素从index位置开始添加到list中 public boolean addAll(int index, Collection<? extends E> c) { //检查index是否越界 rangeCheckForAdd(index); //将c转换为数组 Object[] a = c.toArray(); //获取数组a的长度 int numNew = a.length; //检查是否需要扩容 ensureCapacityInternal(size + numNew); // Increments modCount //计算需要移动的长度 int numMoved = size - index; if (numMoved > 0) //向后移动 System.arraycopy(elementData, index, elementData, index + numNew, numMoved); 将数组a拷贝到原数组中 System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; }
get方法
public E get(int index) { //检查index是否越界 rangeCheck(index); //返回对应数组中指定索引的元素 return elementData(index); }
remove方法
//删除指定索引的元素 public E remove(int index) { //判断index是否越界 rangeCheck(index); //将数组修改次数+1 modCount++; //拿到对应索引的元素 E oldValue = elementData(index); 计算移动位置 int numMoved = size - index - 1; if (numMoved > 0) //移动数组 System.arraycopy(elementData, index+1, elementData, index, numMoved); //size-1,并把尾部元素置为null,这里把尾部置为null主要是为了让GC起作用 elementData[--size] = null; return oldValue; } //删除第一个满足o.equals(elementData[index]的元素 public boolean remove(Object o) { if (o == null) { //如果o为null使用==进行判断 //从索引为0的元素开始寻找 for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { //否则使用equals判断 //从索引为0的元素开始寻找 for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; }
小结
- 如果我们的数据量很大,我们可以给List估算一个容量,然后进行初始化,否则会对效率有影响,毕竟一直进行数据迁移。
- 最开始Arraylist的容量为10,每次扩容为原先容量的1.5倍,但是如果我们已经扩容的数组,是不能进行缩容的,例如我们添加了10个元素,这个时候已经扩容了,但是我们删除最后一个元素,他是不会进行缩容的
- 我们并没有看到他方法上有synchronized关键字,所以他也不是线程安全的,我们可以使用Vector或者使用
Collections.synchronizedList(list)
LinkedList
对于LinkedList,它同样继承与AbstractList,在前面已经说过了,AbstractList是List的骨架,我们还可以看到它实现了Deque,所以LinkedList既可以看做一个链表也可以看做是一个队列,同样也可以看做一个栈,所以LinkedList比较全能
Node类
我们知道LinkedList是一个双向链表,那么肯定有一个个的Node,所以我们先来看一看Node类
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
这部分代码比较简单就简单说一下,Node节点有三个成员属性,分别是value,前驱指针,后继指针,以及一个全参构造方法
成员属性
/** 元素个数 */ transient int size = 0; /** 指向链表头结点 */ transient Node<E> first; /** 指向链表尾节点 */ transient Node<E> last; /** 序列化ID */ private static final long serialVersionUID = 876323262645176354L;
构造函数
//空参构造,没什么好说的 public LinkedList() { } //将集合c的元素添加到链表中 public LinkedList(Collection<? extends E> c) { //调用空参构造 this(); //调用addAll()这个方法在下面讲述 addAll(c); }
添加
public boolean add(E e) { linkLast(e);//添加元素到末尾 return true; } //将元素添加到指定index位置 public void add(int index, E element) { //检查索引是否大于size或者小于0 checkPositionIndex(index); //如果索引位置和size相等添加到末尾 if (index == size) linkLast(element); else //添加到指定位置 linkBefore(element, node(index)); } public void addFirst(E e) { linkFirst(e);//添加元素到头部 } public void addLast(E e) { linkLast(e);//添加元素到末尾 } //添加到头部 private void linkFirst(E e) { //拿到头指针 final Node<E> f = first; //new一个Node节点,值为e,next为头指针,pre为null final Node<E> newNode = new Node<>(null, e, f); //将头指针替换为新的 first = newNode; //将f的pre修改为现在的头指针 if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; } //添加到末尾 void linkLast(E e) { //拿到尾指针 final Node<E> l = last; //new一个Node节点,值为e,next为null,pre为尾指针 final Node<E> newNode = new Node<>(l, e, null); //替换尾指针 last = newNode; //将l的next修改为现在的尾指针 if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } //将集合c的元素添加到list中 public boolean addAll(Collection<? extends E> c) { //从尾部开始添加 return addAll(size, c); } //真正的addAll方法 public boolean addAll(int index, Collection<? extends E> c) { //检查index正确 checkPositionIndex(index); //先把c转换为数组 Object[] a = c.toArray(); //拿到c的长度 int numNew = a.length; if (numNew == 0) return false; //定义两个指针,一个前驱一个后继 Node<E> pred, succ; if (index == size) { succ = null; pred = last; } else { succ = node(index); pred = succ.prev; } //遍历数组a for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; //new一个Node节点,值为e,前驱为pred,后继为null Node<E> newNode = new Node<>(pred, e, null); //pred为null,证明前驱为null,当前节点为链表的头结点 if (pred == null) first = newNode; else //前驱节点后继指针指向头结点 pred.next = newNode; //前驱节点后移 pred = newNode; } //succ为null证明index索引位于链表最后 if (succ == null) { last = pred; } else { //pred的后继节点为succ pred.next = succ; //succ的前驱节点为pred succ.prev = pred; } size += numNew; modCount++; return true; }
获取
//这个方法是Node类的方法,我们可以看到上面的addAll方法也使用了这个方法 //这个方法作用是返回指定索引的非空节点 Node<E> node(int 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; } } //获取index位置的元素 public E get(int index) { //检查索引是否正常 checkElementIndex(index); return node(index).item; } //获取头部元素 public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item; } //获取尾部元素 public E getLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return l.item; }
删除
//删除index位置的元素 public E remove(int index) { //检查索引是否大于size或者小于0 checkElementIndex(index); //删除index位置的元素 return unlink(node(index)); } //删除头部元素,这个方法是Node类的方法 private E unlinkFirst(Node<E> f) { //拿到头节点的值 final E element = f.item; //拿到头结点的next值 final Node<E> next = f.next; //将头结点的值置为null(帮助GC) f.item = null; //将头结点的next置为null(帮助GC) f.next = null; //将头结点修改为next first = next; if (next == null) //此时链表没有元素了 last = null; else next.prev = null; size--; modCount++; return element; } //删除尾部元素,这个方法是Node类的方法 private E unlinkLast(Node<E> l) { //拿到尾节点的值 final E element = l.item; //拿到尾节点的前驱 final Node<E> prev = l.prev; //将尾结点的值置为null(帮助GC) l.item = null; //将尾结点的next置为null(帮助GC) l.prev = null; //将尾指针修改为prev last = prev; if (prev == null) //此时链表没有元素了 first = null; else prev.next = null; size--; modCount++; return element; }
小结
- 虽然说链表的删除的效率为O(1),但是在LinkedList中我们需要先利用node方法查询到指定节点的位置,然后再去删除,所以千万不要误认为LinkedList的remove方法是O(1)
- LinkedList删除头部和尾部的元素效率为O(1)
到此这篇关于Java集合ArrayList与LinkedList详解的文章就介绍到这了,更多相关Java ArrayList与LinkedList内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
您可能感兴趣的文章:
- JAVA LinkedList和ArrayList的使用及性能分析
- java中ArrayList和LinkedList的区别详解
- Java中ArrayList和LinkedList的遍历与性能分析
- java 集合之实现类ArrayList和LinkedList的方法
- java 中ArrayList与LinkedList性能比较
- java中ArrayList与LinkedList对比详情
- 区分Java中的ArrayList和LinkedList
- Java面试岗常见问题之ArrayList和LinkedList的区别
- Java中ArrayList和LinkedList区别
- Java ArrayList与LinkedList使用方法详解
- Java中ArrayList和LinkedList的区别