LinkedList实现原理以及源码解析(1.7)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/dingjianmin/article/details/79811236
LinkedList实现原理以及源码解析(1.7)


在1.7之后,oracle将LinkedList做了一些优化, 将1.6中的环形结构优化为了直线型了链表结构。

1、LinkedList定义:
	public class LinkedList<E>
		extends AbstractSequentialList<E>
		implements List<E>, Deque<E>, Cloneable, java.io.Serializable
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
LinkedList 实现 List 接口,能对它进行队列操作。
LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
LinkedList 是非同步的。 如果多个线程同时访问一个链接列表,而其中至少一个线程从结构上修改了该列表,则它必须 保持外部同步。(结构修改指添加或删除一个或多个元素的任何操作;仅设置元素的值不是结构修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedList 方法来“包装”该列表。List list = Collections.synchronizedList(new LinkedList(...));获取线程安全的list。

AbstractSequentialList 实现了get(int index)、set(int index, E element)、add(int index, E element) 和 remove(int index)这些骨干性函数。降低了List接口的复杂度。这些接口都是随机访问List的,LinkedList是双向链表;既然它继承于AbstractSequentialList,就相当于已经实现了“get(int index)这些接口”。
我们若需要通过AbstractSequentialList自己实现一个列表,只需要扩展此类,并提供 listIterator() 和 size() 方法的实现即可。若要实现不可修改的列表,则需要实现列表迭代器的 hasNext、next、hasPrevious、previous 和 index 方法即可。

2、数据结构:
双向链表,存在一种数据结构——我们可以称之为节点,节点实例保存业务数据,前一个节点的位置信息和后一个节点位置信息。

节点与节点之间相连,构成了我们LinkedList的基本数据结构,也是LinkedList的核心。

        



3、LinkedList的构造方法
    transient int size = 0;

    /**
     * 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;
LinkedList包含3个全局参数,
size存放当前链表有多少个节点。
first为指向链表的第一个节点的引用。
last为指向链表的最后一个节点的引用。

LinkedList构造方法有两个,一个是无参构造,一个是传入Collection对象的构造。

无参构造为空实现。有参构造传入Collection对象,将对象转为数组,并按遍历顺序将数组首尾相连,全局变量first和last分别指向这个链表的第一个和最后一个。

	// 什么都没做,是一个空实现
	public LinkedList() {
	}

	public LinkedList(Collection<? extends E> c) {
		this();
		addAll(c);
	}

	public boolean addAll(Collection<? extends E> c) {
		return addAll(size, c);
	}


	public boolean addAll(int index, Collection<? extends E> c) {
		// 检查传入的索引值是否在合理范围内
		checkPositionIndex(index);
		// 将给定的Collection对象转为Object数组
		Object[] a = c.toArray();
		int numNew = a.length;
		// 数组为空的话,直接返回false
		if (numNew == 0)
			return false;
		// 数组不为空
		Node<E> pred, succ;
		if (index == size) {
			// 构造方法调用的时候,index = size = 0,进入这个条件。
			succ = null;
			pred = last;
		} else {
			// 链表非空时调用,node方法返回给定索引位置的节点对象
			succ = node(index);
			pred = succ.prev;
		}
		// 遍历数组,将数组的对象插入到节点中
		for (Object o : a) {
			@SuppressWarnings("unchecked")
			E e = (E) o;
			Node<E> newNode = new Node<>(pred, e, null);
			if (pred == null)
				first = newNode;
			else
				pred.next = newNode;
			pred = newNode;
		}


		if (succ == null) {
			last = pred; // 将当前链表最后一个节点赋值给last
		} else {
			// 链表非空时,将断开的部分连接上
			pred.next = succ;
			succ.prev = pred;
		}
		// 记录当前节点个数
		size += numNew;
		modCount++;
		return true;
	}
Node是LinkedList的内部私有类,它的组成很简单,只有一个构造方法。
Node节点 一共有三个属性:item代表节点值,prev代表节点的前一个节点,next代表节点的后一个节点。
构造方法的参数顺序是:前继节点的引用,数据,后继节点的引用。
	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;  
		}  
	}
4、LinkedList方法分析
addFirst/addLast:
	public void addFirst(E e) {  
		linkFirst(e);  
	}  
	  
	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++;  
	}  
加入一个新的节点,看方法名就能知道,是在现在的链表的头部加一个节点,既然是头结点,那么头结点的前继必然为null,所以这也是Node<E> newNode = new Node<>(null, e, f);这样写的原因。
之后将first指向了当前链表的头结点,之后对之前的头节点进行了判断,若在插入元素之前头结点为null,则当前加入的元素就是第一个几点,也就是头结点,所以当前的状况就是:头结点=刚刚加入的节点=尾节点。若在插入元素之前头结点不为null,则证明之前的链表是有值的,那么我们只需要把新加入的节点的后继指向原来的头结点,而尾节点则没有发生变化。这样一来,原来的头结点就变成了第二个节点了。达到了我们的目的。
addLast方法在实现上是个addFirst是一致的,这里就不在赘述了。有兴趣的朋友可以看看源代码。
其实,LinkedList中add系列的方法都是大同小异的,都是创建新的节点,改变之前的节点的指向关系。

getFirst/getLast方法分析
	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;  
	}  
add方法:
	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);//建立节点对象,前一个(perv属性)是last,后一个(next属性)是null,中间item数据是输入的参数e
		last = newNode;
		if (l == null)
			first = newNode; //如果最后一个节点 为null表示链表为空,则添加数据时将新加的数据作为第一个节点
		else
			l.next = newNode; //如果链表不为空则将最后一个链表的next属性指向新添加的节点
		size++; //链表长度自增1
		modCount++;
	}


	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;
		}
	}
LInkedList添加操作时每个新添加的对象都会被放到新建的Node对象中,Node对象中包含加入的对象和前后指针属性(pred和next,pred和next其实都是Node对象,直接存放的就是当前对象的前一个节点对象和后一个节点对象,最后一个及节点的next值为null),然后将原来最后一个last节点的next指针指向新加入的对象。

get方法:
	public E get(int index) {  
		// 校验给定的索引值是否在合理范围内  
		checkElementIndex(index);  
		return node(index).item;  
	}  
	  
	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;  
		}  
	}  
判断给定的索引值,若索引值大于整个链表长度的一半,则从后往前找,若索引值小于整个链表的长度的一般,则从前往后找。 这样就可以保证,不管链表长度有多大,搜索的时候最多只搜索链表长度的一半就可以找到,大大提升了效率。

removeFirst/removeLast方法:
	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;  
	}  	
摘掉头结点,将原来的第二个节点变为头结点,改变frist的指向,若之前仅剩一个节点,移除之后全部置为了null。

addAll操作:
	public boolean addAll(int index, Collection<? extends E> c) {
		checkPositionIndex(index);//判断index是否越界,越界则抛出异常
		Object[] a = c.toArray();//转成数组
		int numNew = a.length;//要插入的集合的长度
		if (numNew == 0)
			return false;
		Node<E> pred, succ;//声明pred和succ两个Node对象,用于标识要插入元素的前一个节点和最后一个节点
		if (index == size) { //如果size等于原数组长度则表示在结尾添加
			succ = null;
			pred = last;
		} else {
			succ = node(index);//index位置上的Node对象
			pred = succ.prev;
		}
		for (Object o : a) { //遍历要插入的集合
			@SuppressWarnings("unchecked") E e = (E) o;
			Node<E> newNode = new Node<>(pred, e, null);
			if (pred == null)
				first = newNode; //如果要插入的位置的前一个节点为null表示是第一个节点,则直接将newNode赋给第一个节点
			else
				pred.next = newNode; //将要插入的集合元素节点对象赋给此位置原节点对象的前一个对象的后一个,即更改前一个节点对象的next指针指到新插入的节点上
			pred = newNode;//更改指向后将新节点对象赋给pred作为下次循环中新插入节点的前一个对象节点,依次循环
		}
		//此时pred代表集合元素的插入完后的最后一个节点对象
		if (succ == null) { //结尾添加的话在添加完集合元素后将最后一个集合的节点对象pred作为last
			last = pred;
		} else {
			pred.next = succ;//将集合元素的最后一个节点对象的next指针指向原index位置上的Node对象
			succ.prev = pred;//将原index位置上的pred指针对象指向集合的最后一个对象
		}
		size += numNew;
		modCount++;
		return true;
	}
LinkedList在某个位置插入元素是通过将原位置节点的前一个节点的后一个指针指向新插入的元素,然后将原位置的节点的前一个指针指向新元素来实现的,相当于插入元素后后面的元素后移了,但是不是像ArrayList那样将所有插入位置后面的元素都后移,此处只是改变其前后节点的指向。

5、addAll函数集合参数转数组:
在addAll函数中,传入一个集合参数和插入位置,然后将集合转化为数组,然后再遍历数组,挨个添加数组的元素,为什么要先转化为数组再进行遍历,而不是直接遍历集合呢?

这样是为了避免在putAll过程中Collection的内容又发生了改变。除了多线程外,还有一种可能是,你传入的Collection的内容又间接依赖了正在被putAll的list。
1. 如果直接遍历集合的话,那么在遍历过程中需要插入元素,在堆上分配内存空间,修改指针域,这个过程中就会一直占用着这个集合,考虑正确同步的话,其他线程只能一直等待。
2. 如果转化为数组,只需要遍历数组,而遍历集合过程中不需要额外的操作,所以占用的时间相对是较短的,这样就利于其他线程尽快的使用这个集合。
说白了,就是有利于提高多线程访问该集合的效率,尽可能短时间的阻塞。

6、总结:
1:LinkedList的实现是基于双向循环链表,实现的 List和Deque 接口。实现所有可选的列表操作,并允许所有元素(包括null)。
2:LinkedList是非线程安全的,只在单线程下适合使用。
3:这个类的iterator和返回的迭代器listIterator方法是fail-fast ,要注意ConcurrentModificationException 。
4:LinkedList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了Cloneable接口,能被克隆。
5:在查找和删除某元素时,都分为该元素为null和不为null两种情况来处理,LinkedList中允许元素为null。
6:由于是基于列表的,LinkedList的没有扩容方法!默认加入元素是尾部自动扩容!
7:LinkedList还实现了栈和队列的操作方法,因此也可以作为栈、队列和双端队列来使用,如peek 、push、pop等方法。
8:LinkedList是基于链表实现的,因此插入删除效率高,查找效率低!(因为查找需要遍历整个链表)
9:LinkedList和ArrayList一样实现了List接口,但是它执行插入和删除操作时比ArrayList更加高效,因为它是基于链表的。基于链表也决定了它在随机访问方面要比ArrayList逊色一点。
10:LinkedList继承了 AbstractSequentialList抽象类,而不是像 ArrayList和 Vector那样实现 AbstractList,实际上,java类库中只有 LinkedList继承了这个抽象类,正如其名,它提供了对序列的连续访问的抽象










参考资料:
JDK API LinkedList
LinkedList 源代码 
java源码分析之LinkedList
深入Java集合学习系列:LinkedList的实现原理
http://blog.csdn.net/seu_calvin/article/details/53012654


每天努力一点,每天都在进步。

猜你喜欢

转载自blog.csdn.net/dingjianmin/article/details/79811236