一直很少用LinkedList,一般都用ArrayList.有时碰到面试问起二者区别。于是仔细研究了一下。发现二者实现方式上相差很大。
ArrayList内部是通过一动态数据存储的。所以查找数据很快。根据INDEX,查数据效率高。数据存储在数组中。
LinkedList内数据根本没有固定的容器存储。而是通过对象关联引用,一层一层深入下去的。简单的说,就是保存在一个对象的无限引用中,引用链有多深取决数据量的大小。
LinkedList类声明如下:
1
2
|
public
class
LinkedList<E>
extends
AbstractSequentialList<E>
implements
List<E>, Deque<E>, Cloneable, java.io.Serializable
|
可以发现 LinkedList继承了 AbstractSequentialList抽象类,而不是像 ArrayList和 Vector那样实现 AbstractList,实际上,java类库中只有 LinkedList继承了这个抽象类,正如其名,它提供了对序列的连续访问的抽象:
LinkedList的底层是 Deque双向链表,实现了 Deque接口,而 Deque接口继承于 Queue接口,因此,在java中,如果要实现队列,一般都使用 LinkedList来实现。
Node结点
Node节点 一共有三个属性:item代表节点值,prev代表节点的前一个节点,next代表节点的后一个节点。
LinkedList中关于链表结点的定义如下:
1
2
3
4
5
6
7
8
9
10
|
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中也定义了两个变量分别指向链表中的第一个和最后一个结点:
1
|
transient
Node<E> first;
transient
Node<E> last;
|
添加(add)操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public
boolean
add(E e) {
linkLast(e);
return
true
;
}
void
linkLast(E e) {
//链表最后一个节点
final
Node<E> l = last;
//建立节点对象,前一个(perv属性)是last,后一个(next属性)是null,中间item数据是输入的参数e
final
Node<E> newNode =
new
Node<>(l, e,
null
);
last = newNode;
//如果最后一个节点 为null表示链表为空,则添加数据时将新加的数据作为第一个节点
if
(l ==
null
)
first = newNode;
else
//如果链表不为空则将最后一个链表的next属性指向新添加的节点
l.next = newNode;
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总结
LInkedList添加操作时每个新添加的对象都会被放到新建的Node对象中,Node对象中包含加入的对象和前后指针属性(pred和next,pred和next其实都是Node对象,直接存放的就是当前对象的前一个节点对象和后一个节点对象,最后一个及节点的next值为null),然后将原来最后一个last节点的next指针指向新加入的对象,即可
addAll操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
public
boolean
addAll(
int
index, Collection<?
extends
E> c) {
//判断index是否越界,越界则抛出异常
checkPositionIndex(index);
Object[] a = c.toArray();
//要插入的集合的长度
int
numNew = a.length;
if
(numNew ==
0
)
return
false
;
//声明pred和succ两个Node对象,用于标识要插入元素的前一个节点和最后一个节点
Node<E> pred, succ;
//如果size等于原数组长度则表示在结尾添加
if
(index == size) {
succ =
null
;
pred = last;
}
else
{
//index位置上的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
);
//如果要插入的位置的前一个节点为null表示是第一个节点,则直接将newNode赋给第一个节点
if
(pred ==
null
)
first = newNode;
else
//将要插入的集合元素节点对象赋给此位置原节点对象的前一个对象的后一个
//即更改前一个节点对象的next指针指到新插入的节点上
pred.next = newNode;
//更改指向后将新节点对象赋给pred作为下次循环中新插入节点的前一个对象节点,依次循环
pred = newNode;
}
//此时pred代表集合元素的插入完后的最后一个节点对象
//结尾添加的话在添加完集合元素后将最后一个集合的节点对象pred作为last
if
(succ ==
null
) {
last = pred;
}
else
{
//将集合元素的最后一个节点对象的next指针指向原index位置上的Node对象
pred.next = succ;
//将原index位置上的pred指针对象指向集合的最后一个对象
succ.prev = pred;
}
size += numNew;
modCount++;
return
true
;
}
Node<E> node(
int
index) {
//判断index是否小于size的一半,如果小于则从头遍历节点,否则从结尾遍历节点
if
(index < (size >>
1
)) {
Node<E> x = first;
for
(
int
i =
0
; i < index; i++)
//从first第一个节点开始,依次将后一个节点赋给x
x = x.next;
return
x;
//返回index位置上的Node对象,下同理
}
else
{
Node<E> x = last;
for
(
int
i = size -
1
; i > index; i--)
x = x.prev;
return
x;
}
}
|
总结
LinkedList在某个位置插入元素是通过将原位置节点的前一个节点的后一个指针指向新插入的元素,然后将原位置的节点的前一个指针指向新元素来实现的,相当于插入元素后后面的元素后移了,但是不是像ArrayList那样将所有插入位置后面的元素都后移,此处只是改变其前后节点的指向
删除操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public
E remove(
int
index) {
checkElementIndex(index);
return
unlink(node(index));
}
E unlink(Node<E> x) {
final
E element = x.item;
final
Node<E> next = x.next;
final
Node<E> prev = x.prev;
if
(prev ==
null
) {
first = next;
}
else
{
//将传入的节点的下一个节点对象赋给其之前前一个节点的下一个节点对象
//即将传入的节点对象跳过
prev.next = next;
//将传入对象的前一个节点对象置空使其前一个指针不指向任何元素
x.prev =
null
;
}
if
(next ==
null
) {
//如果next为null表示是最后一个元素,直接将pred节点赋给last
last = prev;
}
else
{
next.prev = prev;
//将传入节点的后一个节点置空,使其后一个节点指针不指向任何元素
x.next =
null
;
}
//将传入的节点对象上的对象置空,也就是整个Node对象中的属性都为空了,后面会被GC回收
x.item =
null
;
size--;
modCount++;
return
element;
}
|
总结:
LinkedList删除操作是通过将index位置上的前一个节点的next执行index位置的后一个节点,同时将后一个节点的pred指向前一个节点,然后将index位置上的Node节点对象属性都置空来实现的,置空后的Node对象会被GC垃圾回收期回收掉。
修改操作
1
2
3
4
5
6
7
8
9
10
|
public
E set(
int
index, E element) {
//检查索引越界情况
checkElementIndex(index);
//获取index位置上的节点对象
Node<E> x = node(index);
E oldVal = x.item;
//将新插入的元素直接赋给此位置上节点对象的item属性即可
x.item = element;
return
oldVal;
}
|
总结:
LinkedList中的实际数据保存在节点对象的item属性中
查询操作
1
2
3
4
5
6
|
public
E get(
int
index) {
//检查索引越界情况
checkElementIndex(index);
//返回该index位置上的节点对象的item属性,即该位置上的实际值
return
node(index).item;
}
|
总结:
插叙操作是通过node方法实现的,而node方法是通过先判断index位置是在整个链表的前一半还是后一半来遍历前一半或者后一半元素来获取index位置上的元素
LinkedList和ArrayList的大致区别
1、ArrayList继承于 AbstractList, LinkedList继承于 AbstractSequentialList;
2、ArrayList基于数组, LinkedList基于双向链表,对于随机访问, ArrayList比较占优势,LinkedList插入、删除元素比较快,如果只要调整指针的指向那么时间复杂度是O(1),但是如果针对特定位置需要遍历时,时间复杂度是O(n),也就是LinkedList在随机访问元素的话比较慢;
3、LinkedList没有实现自己的 Iterator,但是有 ListIterator和 DescendingIterator;
4、LinkedList需要更多的内存,因为 ArrayList的每个索引的位置是实际的数据,而 LinkedList中的每个节点中存储的是实际的数据和前后节点的位置;
5、ArrayList 和 LinkedList都是非同步的集合。
6、和ArrayList一样,LinkedList也是非线程安全的,只有在单线程下才可以使用。为了防止非同步访问,可以采用如下方式创建LinkedList:List list= Collections.synchronizedList(new LinkedList());
7、LinkedList基于双向链表实现,元素都可以为null。