前言
集合是在我们写代码中常见的一个对象、也是我们存储数据的好帮手、但是你真的理解其中的内部实现吗。
Java版本1.8
idea查看继承图:Ctrl+Alt+u(光标放在要查看的类上。)
Collection接口
这个接口在我Collection接口简单说明这里简单说明了、这里就不做讲解
LinkedList集合说明
问题:带着问题来探究内部实现。
- 1、LinkedList存储元素的方式(数据结构)是什么?
- 2、LinkedList是否有和ArrayList一样的扩容机制?
如何创建一个LinkedList集合?
// 创建一个LinkedList的集合
LinkedList<Integer> list2 = new LinkedList<>();
// 也可以以多态的形式去创建一个LinkedList的集合
List<Integer> list2 = new LinkedList<>();
// 注意他们俩创建的对象还是有区别的、后面做说明。
属性
// 集合中元素个数元素
transient int size = 0;
// 头节点
transient Node<E> first;
// 尾节点
transient Node<E> last;
transient
这是一个关键字、作用是标记后改属性不被序列化、用于标记属性、不能标记方法以及对象
构造器
// 一个无参构造
public LinkedList() {
}
// 一个无参有构造、将一个某一个集合中的元素放入到LinkedList中
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
对集合CRUD简单说明
回到上面这个问题、区别是什么
// 创建一个LinkedList的集合
LinkedList<Integer> list2 = new LinkedList<>();
// 也可以以多态的形式去创建一个LinkedList的集合
List<Integer> list2 = new LinkedList<>();
// 注意他们俩创建的对象还是有区别的、后面做说明。
就是少了些LinkedList自己的方法
// 主要是采用里的多态的形式来创建实例对象
List<Integer> list2 = new LinkedList<>();
什么是多态:编译看左、运行看右。
下面来说说增删改查时先说一个对象
Node对象 Node:节点的意思
// 这个是LinkedList的一个私有的静态内部类
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;
}
}
增
追加到元素后linkLast(E e)
public boolean add(E e) {
linkLast(e); // 一般默认就是添加到元素最后
return true;
}
// 最后就是调用的这个方法
void linkLast(E e) {
// 获取LinkedList属性保存的尾节点
final Node<E> l = last; // 尾节点
// 创建一个新节点 上一个节点、当前元素、下一个节点
final Node<E> newNode = new Node<>(l, e, null);
// 将新节点赋值给尾节点保存
last = newNode;
if (l == null) // 如果新节点等于null的话就将新节点覆给头节点(一般属于第一次添加数据是成立)
first = newNode;
else
l.next = newNode; // 否则就是新节点赋给尾节点中的下一个节点中。
size++; // 元素加一
modCount++; // 操作加一
}
追加到元素前addFirst(E e)
private void linkFirst(E e) {
// 获取LinkedList属性保存的头节点
final Node<E> f = first;
// 创建一个新节点 上一个节点、当前元素、下一个节点
final Node<E> newNode = new Node<>(null, e, f);
// 将新节点赋值给头节点保存
first = newNode;
if (f == null) / 如果新节点等于null的话就将新节点覆给尾节点(一般属于第一次添加数据是成立)
last = newNode;
else
f.prev = newNode; // 否则就是新节点赋给头节点中的上一个节点中。
size++; // 元素加一
modCount++; // 操作加一
}
小结:
- 不管是(头)插还是(尾)插、第一次添加元素都会去验证一下(头)或(尾)的元素属性是不是为空、注意这边不是一个方法中都去验证头和尾是否为null、请自行理解。
- 问题:
- 1、LinkedList存储元素的方式(数据结构)是什么?
- 节点的形式保存起来、简单来说就是假设有3个节点、中间节点的元素有上一个节点和下一个节点的数据、这种数据结构叫做
双向链表
- 节点的形式保存起来、简单来说就是假设有3个节点、中间节点的元素有上一个节点和下一个节点的数据、这种数据结构叫做
- 2、LinkedList是否有和ArrayList一样的扩容机制?
-
没有、因为存储的数据是在内存中的、可以存放很多数据、要考虑的就是内存是否能装下这么多数据、数据太多会导致内存不足
-
- 1、LinkedList存储元素的方式(数据结构)是什么?
删
删除最后一个元素E removeLast()返回最后一个元素数据
public E removeLast() {
final Node<E> l = last;
if (l == null) // 如果没有数据则抛出异常
throw new NoSuchElementException();
return unlinkLast(l);
}
// 具体操作内容
private E unlinkLast(Node<E> l) { // 传递过来是集合最后一个元素(尾节点)
// assert l == last && l != null;
final E element = l.item; // 获取尾节点的当前元素
final Node<E> prev = l.prev; // 将尾节点中的上一个节点赋值给prev
l.item = null; // 将尾节点的当前元素置为null
l.prev = null; // help GC 为了帮助垃圾回收机制
last = prev; // 将尾节点的上一个节点赋值给LinkedList属性尾节点中
if (prev == null) // 如果尾节点的上一个节点等于null、就将LinkedList属性头节点置为null
first = null; // 一般执行这里都是集合中就有一个元素的时候
else
prev.next = null; // 否则就将尾节点中的下一个节点置为null
size--; // 元素减一
modCount++; // 操作次数加一
return element; // 返回当前元素
}
删除第一个元素E removeFirst()返回第一个元素数据
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; // 将头节点中的下一个节点赋值给next
f.item = null; // 将头节点的当前元素置为null
f.next = null; // help GC 为了帮助垃圾回收机制
first = next; // 将头节点的下一个节点赋值给LinkedList属性尾节点中
if (next == null) // 如果头节点的下一个节点等于null、就将LinkedList属性尾节点置为null
last = null; // 一般执行这里都是集合中就有一个元素的时候
else
next.prev = null; // 否则就将头节点中的上一个节点置为null
size--; // 元素减一
modCount++; // 操作次数加一
return element; // 返回当前元素
}
这种都是基本头(尾)删除元素、你们可以去探究一下remove(int index)、remove(Object o)、remove()这些方法的实现原理。
改
public E set(int index, E element) { // 返回之前被改元素
// 校验数据越界异常、主要还是通过size来做判断
checkElementIndex(index);
// 返回指定索引的当前节点
Node<E> x = node(index);
// 将旧节点保存到oldVal
E oldVal = x.item;
// 当数据赋值给当前节点中的当前元素
x.item = element;
return oldVal; // 返回之前被改元素
}
// 通过下标获取对应节点
Node<E> node(int index) {
// assert isElementIndex(index);
// >> 是右位移计算
/*
公式:size >> n²
*/
// 这种写法就类似于二分查找、有叫对半查找
// 如 8 < (15 >> 1) = 7 我要找第八个元素、总共有15元素位移1的话就是7不成立就执行下面的else
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;
}
}
查
查询第一个getFirst()
public E getFirst() {
final Node<E> f = first;
// 判断当前是否有元素
if (f == null)
throw new NoSuchElementException();
return f.item; // 返回头节点的当前元素
}
查询最后一个getLast()
public E getLast() {
final Node<E> l = last;
// 判断当前是否有元素
if (l == null)
throw new NoSuchElementException();
return l.item;// 返回尾节点的当前元素
}
根据下标查询get(int index)
public E get(int index) {
// 校验数据越界异常、主要还是通过size来做判断
checkElementIndex(index);
// 这个方法上面解释了。
return node(index).item;
}
图解
总结:
- LinkedList底层实现是链表结构、而链表的结构为双向链表结构、总体思想为第一个节点会保留下一个节点的位置、而下一个节点会保留下一个节点的位置、没有上一个节点或下一个节点就为null值
- LinkedList可以存放null值、非线程安全集合、可以存放重复元素
- LinkedList为链表结构所以不存在容量不足的问题、所以不存在扩容。
集合探究上一篇: ArrayList的简单源码探究欢迎前去阅读:Java集合系列-ArrayList内部探究
附赠鸡汤