CRUD工程师——基础容器LinkedList
- LinkedList双向链表,实现了List的双向队列接口,实现了所有list可选择性操作,允许存储任何元素(包括null值)
- 所有的操作都可以表现为双向性的,遍历的时候会从首部到尾部进行遍历,直到找到最近的元素位置
- 注意这个实现不是线程安全的, 如果多个线程并发访问链表,并且至少其中的一个线程修改了链表的结构,那么这个链表必须进行外部加锁。(结构化的操作指的是任何添加或者删除至少一个元素的操作,仅仅对已有元素的值进行修改不是结构化的操作)。
- List list = Collections.synchronizedList(new LinkedList(…)),可以用这种链表做同步访问,但是最好在创建的时间就这样做,避免意外的非同步对链表的访问(加锁)
- 迭代器返回的iterators 和 listIterator方法会造成fail-fast机制:如果链表在生成迭代器之后被结构化的修改了,除了使用iterator独有的remove方法外,都会抛出并发修改的异常。因此,在面对并发修改的时候,这个迭代器能够快速失败,从而避免非确定性的问题。
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { //链表节点的个数 transient int size = 0; //将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。 ////链表首节点 transient Node<E> first; //链表尾节点 transient Node<E> last; //无参数构造方法 public LinkedList() { } //首先会调用无参数的构造方法,然后调用addAll方法将集合内元素全部加入到链表中,addAll方法我们后面会详细的分析。 从上述的俩个构造方法可以看出LinkedList是一个无界链表,不存在容量不足的问题。 public LinkedList(Collection<? extends E> c) { this(); addAll(c); } //(1)定义一个Node类型的变量f,指向链表的第一个结点。 //(2)定义一个新的Node类型的变量newNode,通过Node类的带参数构造函数将插入的元素的值放在变量中,因为头插法,所以新结点的上一个域为null //(3)更改第一个结点的值,将newNode的值赋给成员变量first //(4)如果f(链表的第一个结点)是null,那么最后一个结点也是newNode,否则将f的prev域指向newNode //(5)更改size的大小和modCount的大小 private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; } //(1) 定义一个Node类型的变量pred,用来指向链表的头结点 //(2) 定义一个Node类型的变量newNode,用来存储新的将要插入的结点信息 //(3) 将当前链表的头结点赋值为newNode结点 //(4) 如果pred为空,即原来链表的头结点为空,表明原链表没有元素,那么刚插入的结点就是链表的第一个结点 //(5) 最后要修改链表的size大小和modCount的值 // 这个和上一次linkFirst不同之处在于必须要有succ节点不然会出错,一个不需要succ节点。succ节点是待加入元素的后继节点 void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; } //(1) 定义一个E类型的element变量,其值等于链表中第一个结点的值 //(2) 定义一个Node类型的next变量,其值等于链表中第一个结点的next域 //(3) 将链表中第一个结点的值赋值为null,并将该结点的next域赋值为null //(4) 将next值赋值给first,如果next为空,那么链表中不再有元素,否则将链表中第一个结点删除,即赋值next的prev域为null //(5) 更改链表的size值,更改modCount的值 //(6) 返回链表的第一个结点 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基于链表的数据结构。 (LinkedList是双向链表,有next也有previous)
2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
时间复杂度比较:
首先一点关键的是,ArrayList的内部实现是基于基础的对象数组的,因此,它使用get方法访问列表中的任意一个元素时(random access),它的速度要比LinkedList快(O1)。LinkedList中的get方法是按照顺序从列表的一端开始检查,直到另外一端(On)。对LinkedList而言,访问列表中的某个指定元素没有更快的方法了
但在某些情况下LinkedList的表现要优于ArrayList,有些算法在LinkedList中实现时效率更高。比方说,利用Collections.reverse方法对列表进行反转时,其性能就要好些。当要对list进行大量的插入和删除操作时,LinkedList也是一个较好的选择。
总结
ArrayList和LinkedList在性能上各有优缺点,都有各自所适用的地方,总的说来可以描述如下:
1.对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
2.在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
3.LinkedList不支持高效的随机元素访问。
4.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
可以这样说:当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了