来,进来的小伙伴们,咱们认识一下。html
我是俗世游子,在外流浪多年的Java程序猿java
这两天看到太多小伙伴在秀公司“10.24程序员节”的福利,我认可我酸了o(╥﹏╥)onode
上一节咱们聊过了ArrayList,对其底层结构和源码实现进行了了解,那这节咱们来聊一聊关于它的“兄弟集合”:LinkedList程序员
一样属于List的子类,那么也就一样拥有了其特色:api
能够看到,LinkedList除了实现List接口外,还实现了Queue接口,在Java中,该接口定义的是队列的,向咱们以后要聊到的数组
等等的都是属于该类的实现安全
基于这种方式,那么咱们的LinkedList也适合作队列的处理场景,好比:数据结构
LinkedList底层是基于双向链表的方式来存储的,那确定有人在想,什么是链表呢?咱们这就来聊一聊oracle
链表是一种在逻辑上连续,可是物理存储上非连续的存储结构,其保证逻辑连续是经过指针指向来肯定顺序的。ide
上面看到的是单向链表,能够看到:
Node
,其中包含两个部分
Node
还有一种双向链表的形式
看上图:
LinkedList就是采用的双向链表的形式,下面咱们来看具体的代码
背景:这里已双向链表为例
对链表操做,实际上就是修改指针的指向,好比
头尾的插入很是简单,直接指向 next 和 prev 就能够了,这里咱们看插入到中间
移除元素和插入元素很相似,无非就是将指定元素删除掉,而后将指针指向下一个节点
前面的链表介绍都是为以后作铺垫,咱们继续来看今天的主角:LinkedList
LinkedList底层是采用双向链表的结构来进行数据存储的,能够说,LinkedList全部的操做都是针对引用指向来进行操做的。下面来看具体的方法
LinkedList<String> linkedList = new LinkedList<>(); // Arrays.asList("item1", "item2"):为了方便演示 LinkedList<String> linkedList2 = new LinkedList<>(Arrays.asList("item1", "item2"));
一样,咱们仍是经过构造方法来看:
public LinkedList() { } public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
和ArrayList不一样,这里只有两个构造方法。
在LinkedList中,初始长度是不须要设置的,并且也不须要扩容操做。
不须要设置初始长度和底层存储结构有关,若是想不明白能够先去上一节看一看数组的介绍
不过也有说,LinkedList是存在最大容量的:不能超过Integer.MAX_VALUE
你们能够查找下资料,好好验证下该说法(本人没有在源码中发现对应的验证)
同时,咱们来看一看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; } }
双向链表节点:在代码中对应的具体实现。
这里我须要让你们考虑一个问题:
ArrayList和LinkedList一样存储了100W的数据,哪一种集合占用的空间更大?
前面也说到了,LinkedList除了实现List接口外,还实现了Queue接口,天然也重写其对应的方法,下面咱们一一来看看:
linkedList.add("item2"); linkedList.add(1, "item3"); linkedList.addFirst("item0"); linkedList.addLast("item9");
add(e)
和addLast(e)
的源代码public boolean add(E e) { linkLast(e); return true; } public void addLast(E e) { linkLast(e); } void linkLast(E e) { // 获得临时变量尾结点 final Node<E> l = last; // 新节点的上一个节点是以前的尾结点,下一个节点是null final Node<E> newNode = new Node<>(l, e, null); // 新的节点成为尾结点, last = newNode; // 若是尾结点是null,这个链表是空的,那么头结点也是新增的节点 if (l == null) first = newNode; else // 以前尾结点的next是新节点 l.next = newNode; size++; modCount++; }
linkLast(e)
方法,从字面和具体的实现方法上来看,默认的add(e)
方法是将元素添加到了链表的尾部,这里专业名词叫:尾插法addLast(e)
就更不用说了,直接将元素添加到尾部
linkLast(e)
方法注释都已经有了,其实就是在调整指针的指向,总体描述以下图:public void addFirst(E e) { linkFirst(e); } private void linkFirst(E e) { // 获得临时变量头结点 final Node<E> f = first; // 新节点 插入到头部,因此新节点的next指向f final Node<E> newNode = new Node<>(null, e, f); // 一样,新节点称为新的头 first = newNode; // 若是头结点是null,这个链表是空的,那么尾结点也是新增的节点 if (f == null) last = newNode; else // 不然的话,新节点的prev指向新节点 f.prev = newNode; size++; modCount++; }
addFirst(e)
和addLast(e)
方法正好相反这里就不给图了,就是上面插入尾部改为插入头部
这里咱们要看一下指定位置的插入:
public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } 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; } }
linkBefore()
方法就不贴出来了,也就是改变prev和next的指向,下面咱们看一下node(index)
方法
这里咱们举个例子
好比:LinkedList中存储了500条,咱们须要往250个位置上添加,那么node(index)
对应的遍历就是在后半段:
Node<E> x = last; for (int i = 500 - 1; i > 250; i--) x = x.prev; return x; // 好比这里是 400 Node<E> x = last; for (int i = 500 - 1; i > 400; i--) x = x.prev; return x;
虽然遍历采用简单二分法提高了总体遍历的性能,可是若是遍历的节点越靠近中间位置,检索的效率也就越低
这里给你们留一个试验:ArrayList的插入和LinkedList的插入,性能相好比何?须要考虑一下几个方面:
- ArrayList的扩容问题
- 插入到头部,中间,尾部的性能
linkedList.get(0); // 在LinkedList中已经记录了头节点和尾结点,这里就是获得当前的数据就好了 linkedList.getFirst(); linkedList.getLast();
get(index)
咱们上面已经介绍到了,就是经过node(index)
来获得指定索引的数据的public E get(int index) { checkElementIndex(index); return node(index).item; }
Iterator<String> iterator = linkedList.iterator();
这种模式咱们就不介绍了,iterator()
实现实际上是采用这种方式来作的:
public ListIterator<E> listIterator() { return listIterator(0); } public ListIterator<E> listIterator(final int index) { rangeCheckForAdd(index); return new ListItr(index); } private class ListItr implements ListIterator<E> { private Node<E> lastReturned; private Node<E> next; private int nextIndex; private int expectedModCount = modCount; ListItr(int index) { // assert isPositionIndex(index); next = (index == size) ? null : node(index); nextIndex = index; } }
这里默认传入的参数是:0,LinkedList为咱们开放了该方法:
ListIterator<String> listIterator = linkedList.listIterator(0); // == linkedList.iterator();
ListIterator和Iterator二者的对比咱们上节也介绍过了
那么,问题来了:在迭代LinkedList的时候咱们该采用那种方式?
咱们来作个实验进行验证:1W的数据,咱们来进行验证
int len = 1_0000; LinkedList<String> linkedList = new LinkedList<String>() {{ for (int i = 0; i < len; i++) { add("item" + i); } }}; long start = System.currentTimeMillis(); for (int i = 0; i < len; i++) { linkedList.get(i); } System.out.println("for i 耗时:" + (System.currentTimeMillis() - start)); start = System.currentTimeMillis(); Iterator<String> iterator = linkedList.iterator(); while (iterator.hasNext()) { iterator.next(); } System.out.println("iterator 耗时:" + (System.currentTimeMillis() - start));
猜一猜最终的结果如何?
咱们来想想为何:
get(index)
底层实现是:node(index)
,最外层每循环一次,node(index)
每次都会进行一次二分查找,而后循环迭代索引取出对应的值iterator()
的话,只会在构造方法中进行一次node(0)
的迭代取出第一个节点,由于双向链表的形式,因此经过item.next
来就能够取出对应的元素public E next() { checkForComodification(); if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; }
因此咱们在LinkedList的迭代的时候,最好采用iterator()
的方式
咱们都是经过remove()
来移除元素,那么对应其中的实现:
E unlink(Node<E> x) { // assert x != null; // 获得当前元素,当前next和prev的指向对象 final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; // 若是prev是null,说明是第一个节点 if (prev == null) { first = next; } else { // 不然就将当前节点的prev的下一个节点指向当前节点的next prev.next = next; x.prev = null; } // 若是next是null,说明是最后一个节点 if (next == null) { last = prev; } else { // 不然就将当前节点的prev的下一个节点指向当前节点的prev next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
这里其实就是修改引用的过程,这里就不展现图了,你们感兴趣的话我在后面给出一个站点,你们能够在那个站点上查看对应具体的过程
这里咱们就过了,下面基于LinkedList简单聊一点队列的东西
队列也是咱们经常使用的一种数据结构:并且只容许在一端进行插入操做,另外一端进行删除操做。
这不就是排排站,先插入进来的先被处理掉。
这种结构能够称为先进先出的方式,也就是咱们所说的:FIFO,具体以下:
那么,LinkedList又是怎么作的呢?
简单来两个方法来实现一下:
将元素推送到由此列表表示的堆栈上
底层实现也很是简单,就是调用以前的addFirst(e)
来操做的,都是上面介绍过的
public void push(E e) { addFirst(e); }
检索并删除此列表的最后一个元素
public E pollLast() { final Node<E> l = last; return (l == null) ? null : unlinkLast(l); } private E unlinkLast(Node<E> l) { // assert l == last && l != null; final E element = l.item; final Node<E> prev = l.prev; l.item = null; l.prev = null; // help GC last = prev; if (prev == null) first = null; else prev.next = null; size--; modCount++; return element; }
根据LinkedList提供的方法就能够有不少中实现方式,只要知足先进先出的方式
和ArrayList是同样的,若是只是当作局部变量来使用的话,是不存在线程问题的;可是若是当作共享资源来使用,那么必然是线程不安全的,针对解决方式:
更多关于LinkedList使用方法推荐查看其文档: