LinkedList是List 接口的链接列表实现。实现List接口所有可选的列表操作,并且允许添加任何元素(包括 null)。除了实现 List 接口外,LinkedList 类还为在列表的开头及结尾 get、remove 和 insert 元素提供了统一的命名方法(如:getFirst、removeLast等)。这些操作允许将链接列表用作堆栈、队列或双端队列。
LinkedList还实现了 Deque 接口,为 add、poll 提供先进先出队列操作,以及其他堆栈和双端队列操作。所有操作都是按照双重链接列表的需要执行的。在列表中编索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。
注意,LinkedList不是同步的。如果多个线程同时访问一个链接列表,而其中至少一个线程从结构上修改了该列表,则它必须保持外部同步。(结构修改指添加或删除一个或多个元素的任何操作;仅设置元素的值不是结构修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedList 方法来“包装”该列表。最好在创建时完成这一操作,以防止对列表进行意外的不同步访问,如下所示:
List list = Collections.synchronizedList(new LinkedList(...));
LinkedList的 iterator 和 listIterator 方法返回的迭代器是快速失败(fail-fast)的:在迭代器创建之后,如果从结构上对列表进行修改,除非通过迭代器自身的 remove 或 add 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。
所谓fail-fast是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
同样的我们从LinkedList的源码深入来分析LinkedList的结构及应用场景:
1.LinkedList的成员变量
与ArrayList相同LinkedList依旧只有两个成员变量:
private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0; //列表大小
既然LinkedList是一个链表实现,那么header必然就是这个链表的头部,其中Entry即为节点对象。
size与ArrayList实现一样是这个列表的大小(元素个数)。
2.Entry节点元素
与ArrayList不同,LinkedList的内部是利用Entry这个内部类来实现的,下面是Entry源代码:
private static class Entry<E> {
//当前元素
E element;
//下一个元素
Entry<E> next;
//上一个元素
Entry<E> previous;
Entry(E element, Entry<E> next, Entry<E> previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
Entry类只定义了三个属性:previous(前一个元素)、element(当前元素)、next(后一个元素)。这样获取到当前位置节点元素后就可以很容易的找到它之前和之后的两个节点,这样在不同的Entry间就建立了联系,从而就实现了双向的链表结构。
3.LinkedList的使用
使用LinkedList也是需要先实例化:
List list=new LinkedList();
实例化时会调用LinkedList的默认构造方法:
/**
* 构建一个空的list实例
*/
public LinkedList() {
header.next = header.previous = header;
}
默认构造方法不接受任何参数,只是将header节点的前一节点和后一节点都设置为自身,这样整个链表其实就只有header一个节点,用于表示一个空的链表。
实例化LinkedList之后,利用add方法向其添加元素:
此时LinkedList其内部结构如图:
此时是列表中只有一个已添加元素的状态,新添加节点元素的previous与next均指向header,那么是如何通过代码来实现此种结构的呢?就需要我们来查看一下add方法的相关源代码:
/**
* 添加指定元素至此list尾部
*/
public boolean add(E e) {
addBefore(e, header);
return true;
}
private Entry<E> addBefore(E e, Entry<E> entry) {
Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size++;
modCount++;
return newEntry;
}
add方法中并没有实际处理的逻辑,而是调用的另一个私有方法addBefore(e, header),其中E e参数为要添加的元素,Entry<E> entry为header。
进入addBefore方法后:
首先,创建了一个新的节点元素 newEntry,newEntry的next指向header,newEntry的previous指向header的previous。
newEntry的next指向header不难理解,新添加的节点元素必然是在整个链表的尾部,既然LinkedList是一个双向循环链表,那么newEntry的下一个节点将回到整个链表的“原点”header。进而header的上一个节点元素必然就是整个链表的尾部节点,所以newEntry的previous指向header的previous,这样newEntry的上一个节点就指向了原来整个链表的最后一个节点,进而newEntry成为了链表新的尾节点。
然后,将newEntry的前一个节点的next指向自己,因为链表原来的尾部节点指向的是“原点”header,将其next指向自己后就将两个节点连接了起来。
再将 newEntry的后一个节点(header)的previous 指向自己,这样header就与newEntry也连接到了一起。
最后,整个链表就将新添加的节点元素newEntry添加到了尾部。
以下是添加了三个元素后的结构图示:
通过图例可以清晰的看到在向LinkedList中添加了三个元素之后,这三个元素与header组成了一个双向循环的链表结构。通过每一个节点元素的previous与next可以访问到前一个与后一个节点元素。
4.获取元素
在ArrayList中通过get(index)方法可以直接返回该下标位置元素,因为ArrayList的内部实现为数组,所以get方法等效与array[index],所以性能非常高。
那么LinkedList的内部是如何获取指定位置元素的呢?以下是相关方法的源代码:
/**
* 返回指定位置元素
*/
public E get(int index) {
return entry(index).element;
}
/**
* 返回指定位置元素
*/
private Entry<E> entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
Entry<E> e = header;
if (index < (size >> 1)) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
get方法中引用的是另一个方法Entry<E> entry(int index),通过此方法可以返回index位置的元素,所以entry方法才是关键。
entry方法中首先是下标越界的判断;然后判断index是否小于(size >> 1),这里size>>1等同于size/2(size除以2),只不过用位移性能更高。这里是要判断index位于链表的前半部分还是后半部分,从而判断从那个方向遍历。最后就是用循环方式遍历到index那个位置,得到该位置的元素并返回。
从源代码中可以看出,LinkedList获取元素采用的是遍历链表的方式,虽然最多只会循环列表大小的一半,但其性能也是比较低的。
5.LinkedList移除元素
如果对于添加操作已经理解清晰,那么对于移除元素即便不看源代码也应该大致了解其内部处理方式,无非就是改变相关节点元素的previous与next指向问题。
以下为LinkedList中remove方法的源代码:
/**
* 移除指定位置元素
*/
public E remove(int index) {
return remove(entry(index));
}
private E remove(Entry<E> e) {
if (e == header)
throw new NoSuchElementException();
E result = e.element;
e.previous.next = e.next;
e.next.previous = e.previous;
e.next = e.previous = null;
e.element = null;
size--;
modCount++;
return result;
}
remove方法调用了两个相关其他方法,一个是根据index返回指定位置元素的Entry<E> entry(int index)方法,另一个是删除指定元素的方法E remove(Entry<E> e)。
remove方法的核心就是找到index位置元素,将此元素上一个和下一个节点元素的指向连接,进而达到移除的目的:
6.LinkedList的一些特殊操作及用途
本文最开始已经介绍了很多LinkedList不同于ArrayList的用途,因为LinkedList的实现结构为链表形式,所以LinkedList可以用于堆栈、队列或双端队列等形式结构。
所以LinkedList提供了一些*First,*Last类似的操作,如:getFirst,getLast;offerFirst,offerLast等。
1)用LinkedList实现堆栈结构
LinkedList中提供了pop和push两个方法:
E pop()
从此列表所表示的堆栈处弹出一个元素。
void push(E e)
将元素推入此列表所表示的堆栈。
通过这两个方法就可以模拟内存堆栈的结构,从而对一些特殊场景业务进行处理,以下是一个简单实例:
LinkedList list = new LinkedList();
list.push("栈帧1");
list.push("栈帧2");
list.push("栈帧3");
list.push("栈帧4");
for (int i = 0, j = list.size(); i < j; i++) {
System.out.println("entry:"+list.pop());
System.out.println("size :"+list.size());
}
//打印结果
entry:栈帧4
size :3
entry:栈帧3
size :2
entry:栈帧2
size :1
entry:栈帧1
size :0
首先,将四个“栈帧”压入了LinkedList中,然后,循环将所有“栈帧”弹出,随着“栈帧”的弹出,list的大小也在减小,直至为0。这样就实现了一个简单的栈结构列表,实现了后进先出的原则。
2)用LinkedList实现队列结构
队列通常(但并非一定)以 FIFO(先进先出)的方式排序各个元素。不过优先级队列和 LIFO 队列(或堆栈)例外,前者根据提供的比较器或元素的自然顺序对元素进行排序,后者按 LIFO(后进先出)的方式对元素进行排序。无论使用哪种排序方式,队列的头 都是调用 remove() 或 poll() 所移除的元素。在 FIFO 队列中,所有的新元素都插入队列的末尾。其他种类的队列可能使用不同的元素放置规则。
在生活中我们也会经常接触到队列,如车站买票,进站安检,排队购物等等:
在结构上队列如图所示:
LinkedList类实现了Queue(队列)接口,在Queue接口中提供了offer,peek和poll三个方法用于添加和获取元素:
boolean offer(E e)
将指定的元素插入此队列(如果立即可行且不会违反容量限制),当使用有容量限制的队列时,此方法通常要优于 add(E),后者可能无法插入元素,而只是抛出一个异常。
E peek()
获取但不移除此队列的头;如果此队列为空,则返回 null。
E poll()
获取并移除此队列的头,如果此队列为空,则返回 null。
所以我们可以利用这几个相关方法结合LinkedList来实现一个队列结构:
LinkedList list = new LinkedList();
list.offer("成员1");
list.offer("成员2");
list.offer("成员3");
list.offer("成员4");
for (int i = 0, j = list.size(); i < j; i++) {
System.out.println("entry:" + list.peek());
System.out.println("size :" + list.size());
}
for (int i = 0, j = list.size(); i < j; i++) {
System.out.println("entry:" + list.poll());
System.out.println("size :" + list.size());
}
// 打印结果
entry:成员1
size :4
entry:成员1
size :4
entry:成员1
size :4
entry:成员1
size :4
entry:成员1
size :3
entry:成员2
size :2
entry:成员3
size :1
entry:成员4
size :0
使用peek方法可以获取到该队列的头,但是不会移除头元素,所以打印结果中出现了4次“成员一”。
使用poll方法也可以获取到该队列的头,但是会移除该元素,所以可以通过poll方法依次将队列中的成员取出进行操作。
LinkedList还可以实现一些其他的特殊结构,这里就不再赘述了,可以根据自己的实际情况去处理和编写相应代码。
分享到:
相关推荐
### List-LinkedList 单链表就地反转 #### 概述 本文主要介绍单链表的就地反转算法实现,并通过具体的C语言代码示例来解释这一过程。单链表是一种常见的线性数据结构,其中每个元素包含一个指向下一个元素的指针。...
在这个"C++数据结构实现之Linkedlist.zip"压缩包中,我们可以预见到包含的是关于C++实现链表(Linked List)的详细教程或代码示例。 链表是一种线性数据结构,与数组不同,它不连续存储元素。链表由一系列节点组成...
为了更好地理解这个实现,你可以打开并阅读这三个文件,查看它们如何协作以实现LinkedList的功能。这将有助于你深入理解C++中的链表数据结构以及如何在实际编程中应用它。同时,通过分析和实践这个实现,你可以...
常见的List实现类有ArrayList和LinkedList。 **ArrayList类** `ArrayList`是基于数组实现的List,它提供了一个动态增长的数组来存储元素。由于其底层是数组,所以它的随机访问(通过索引)速度非常快。但是,插入和...
在本文中,我们将深入探讨LinkedList如何实现List接口的一些主要方法,并了解其内部工作原理。 1. **添加元素**: LinkedList提供了`add(int index, E element)`和`add(E e)`方法来添加元素。`add(int index, E ...
首先,LinkedList实现了List接口,因此它是一个有序列表。它还实现了Deque接口,这意味着LinkedList支持双端队列的操作。除了这些接口之外,LinkedList还是Cloneable和java.io.Serializable的,表示它可以被克隆以及...
ArrayList、LinkedList、Vector 是 Java 中常用的数据结构实现类,它们都实现了 List 接口,但它们在存储方式、性能、线程安全性等方面有着不同特点。 首先,ArrayList 和 Vector 都是采用数组方式存储数据的,这种...
在Java编程语言中,`LinkedList` 是一个常用的集合类,它实现了`List`接口,并且提供了额外的功能,如双端操作。本篇文章将探讨如何利用`LinkedList`来实现队列和栈这两种数据结构,以及其背后的原理和源码分析。 #...
List接口的实现类主要有ArrayList、LinkedList和Vector。 2. **ArrayList** - **实现原理**:ArrayList基于动态数组实现,它提供快速的按索引访问,因为数组支持直接通过索引获取元素。 - **添加和删除**:对于在...
首先,LinkedList类位于Java的`java.util`包中,它实现了List接口,允许我们存储和操作一系列元素。LinkedList内部维护了一个双向链表,每个元素都是一个Node对象,包含元素值以及指向前后节点的引用。由于...
ArrayList和LinkedList是List接口的两种主要实现,它们各有优缺点,适用于不同的场景。此外,匿名类的概念在Java中用于简化代码结构,尤其是在实现接口时。 1. **List接口** List接口继承自Set接口,它不仅提供了...
- LinkedList 实现了 List 接口,因此可以执行所有 List 相关的操作,如添加、删除、查找元素等。 - LinkedList 同时实现了 Deque 接口,允许作为双端队列使用,可以从前端或后端添加和移除元素。 使用 LinkedList ...
在Java编程语言中,LinkedList是一个常用的集合类,它实现了List接口,同时也提供了双向链表的实现。LinkedList不仅可以作为列表使用,还可以被巧妙地利用来构建栈(Stack)和队列(Queue)这两种基本数据结构。在本...
在Java编程语言中,`LinkedList`是`java.util`包下的一个接口实现,它继承了`List`接口,内部使用双向链表来存储元素。与`ArrayList`相比,`LinkedList`更适用于频繁插入或删除元素的场景。本文将详细介绍`...
本篇文章将深入探讨`List`集合的各种操作,包括增、删、改、查,以及`ArrayList`和`LinkedList`两种实现`List`接口的类之间的区别。同时,我们还将讨论如何利用`LinkedList`实现堆栈和队列的功能,并了解`List`集合...
在Go语言中,由于其内置的`container/list`包提供了简单易用的链表实现,但它是单链表,不支持直接的前向和后向遍历。因此,自定义双链表的实现可以弥补这一不足,提供更高效的双向操作。 在实际项目中,双链表可能...
在Java集合框架中,`LinkedList`类是一种基于链表实现的线性数据结构,继承自`AbstractSequentialList`抽象类,并实现了`List`接口与`Deque`接口。由于其内部是通过双向链表来存储元素,因此在元素的增删操作上具有...
在Java编程中,LinkedList是一个非常重要的数据结构,它实现了List接口,允许我们在列表的任何位置进行插入和删除操作。LinkedList内部使用双向链表实现,因此它的遍历速度比ArrayList快,但随机访问性能较差。本...
`LinkedList`类是实现`List`接口的一个具体类,它提供了链表结构的数据存储方式,允许在列表的任何位置进行插入和删除操作,而不需要移动其他元素。 `LinkedList`的主要特点包括: 1. **内部结构**:`LinkedList`...
在Java编程语言中,LinkedList是一种线性数据结构,属于集合框架的一部分,实现了List接口和Deque接口。LinkedList以链表的形式存储元素,这使得它在添加和删除元素时具有较高的效率,尤其是在列表的中间或开头进行...