【JavaSE8 高级编程 集合框架】集合框架入门系列③Collection&List实现类LinkedList 2019_9_1


前言:很高兴,在昨晚收获了第一位不认识的粉丝。哈哈哈哈,为此特别纪念一下。
可能还是文章写的不够好,没有几个点赞的。哎,伤心。( 滑稽 掩面痛哭

使用感悟:LinkedList本体是双向Node节点对象,“向”字的含义是——指向;双向Node节点——Node节点对象,有1个指向前一个Node节点的属性,并且还有1个指向后一个Node节点的属性,当然Node还有一个存储值对象的item属性,一个是3个属性。

与ArrayList的区别:在物理层级上实物映射,Array是内存中连续的一个个基本存储元器件组成的区域,而一个Link是由多个分散的基本存储元器件组成,可相邻也可不相邻。
本体Node对象存储了后一个、前一个节点的地址,也就是通过Node对象本身能找到下一个、前一个节点对象(相邻节点)。而数组是通过独立索引找到一个指定任意位置的对象。
现实事物映射:Link软链条,Array硬壳条

LinkedList

本质:本体为Node节点对象

注意:LinkedList不存在 与 Array/ArrayList 相同概念的索引。LinkedList中index只是代表从头结点开始的 第index个 节点。不能直接通过a[index] 的方法直接获取对象。

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;
    }
}

用语约定

前向:相对于当前节点 Node node 的——前一个节点 Node prev

后向:相对于当前节点 Node node 的——后一个节点 Node next

前向双向链接:指的的是当前节点 node前一个节点 prev 的相互链接,一共2条相互指向的线【其实不是线方便理解】

后向双向链接:指的的是当前节点 node后一个节点 next 的相互链接,一共2条相互指向的线【其实不是线方便理解】

多向 / 多向链接:指的是,当前节点与前后节点 prev next 与当前节点 node 共存在4条 相互指向的线

父类

在这里插入图片描述
父类传递下的属性

//AbstractCollection顶级抽象父类 向下传递的 属性
public abstract class AbstractCollection<E> implements Collection<E> {
    //数组最大长度
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}

//AbstractList抽象父类 向下传递的 属性
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    //修改(增删改)计数器
    protected transient int modCount = 0;
}

Field

//基于链表数据结构实现的List对象
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{	
	//本体——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;
        }
    }
    //当前链表List中的Node 节点计数器
    transient int size = 0;

    /**
     * 保存(指向) 头/首节点
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     * 解析:头节点,只有两种状态:
     * 		1.链表List为空,头尾节点均为 Node first = null && Node last = null
     *		2.链表不为空,头节点的属性Node prev为null,但头节点的属性item不为null
     */
    transient Node<E> first;

    /**
     * 保存(指向) 尾节点
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     * 解析:尾节点,也只有两种状态:
     * 		1.链表List为空,头尾节点均为 Node first = null && Node last = null
     *		2.链表不为空,尾节点的属性Node next为null,但尾节点的属性item不为null
     */
    transient Node<E> last;

Method

用语简称:Collection与List简写为C&L,Deque简写为D,Queue简写为Q,Stack简写为S

构造器:2个LinkedList()【空,聚集】
迭代器:listIterator(),descendingIterator(),spliterator【本文省略迭代器部分】
其他:clone()

增:10,删:12,改:1,查:11
注意,这个数字是不完全的,这只是LinkedList源码中复写的。还有没在LinkedList复写的方法。
LinkedList是集成Collection体系中List、Queue两大子接口的实现类很强很无敌,但函数很多。

【Collection & List】

增*4:2个add,2个addAll

删*3:2个remove(),clear()

查*5:get(),contains(),size(),lindexOf(),astIndexOf()

改*1:set()

【Queue】

增*1:add()【C.add】 || offer()

删*2:remove()【Q.remove】 || poll()

查*2:element() || peek()

【Deque】

增*4:addFirst & addLast || offerFirst & offerLast

删*6:removeFirst & removeLast || pollFirst & pollLast,removeFirstOccurrence & removeLastOccurrence

查*4:getFirst & getLast || peekFirst & peekLast

【Stack】

增*1:push【向下推压】

删*1:pop【向上冒泡】

构造器*2

//1.空构造器
public LinkedList() {
}

//2.单参构造器(Collection)
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
//addAll【单参重载,尾部增添,一个Collection聚集进入LinkedList】
public boolean addAll(Collection<? extends E> c) {
    //末尾(size)增添
    return addAll(size, c);
}
//addAll【双参重载,插入增添,一个Collection聚集进入LinkedList,将原index位置的Node排后】
public boolean addAll(int index, Collection<? extends E> c) {
    //index非法抛出异常
    checkPositionIndex(index);
    
    //1.先将聚集C转换为 Object []数组
    Object[] a = c.toArray();
    //获取聚集C内元素总数
    int numNew = a.length;
    
    //2.【前节点 ←双向链接→ newNode[1,...,n]】重建立
    //中间插入/尾部增添 新节点( new Node(a[i])对象) 
    //情况1:如果聚集C内有0个元素
    if (numNew == 0)
        return false;
	//情况2:聚集C非空
    Node<E> pred, succ;
    //第一种:尾部增添
    if (index == size) {
        //将last节点存储至pred节点,新节点succ先赋值为null等待聚集C内元素再赋值
        succ = null;
        pred = last;
    }
    //第二种:中间插入
	else {
        //将插入位置index的Node节点赋给 succ 局部变量[index],
        succ = node(index);
        //将succ的前一个节点succ.prev赋值给 pred 局部变量[index.prev]
        pred = succ.prev;
    }
	//对聚集C转换的Object a[],进行遍历 构造新Node并与pred前节点 双向链接
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        //new Node<>(pred,e,null)
        Node<E> newNode = new Node<>(pred, e, null);
        //情况1.空链List
        if (pred == null)
            //将头/首 节点放入new Node
            first = newNode;
        //情况2.非空List
        else
            //通过pred对象的next属性,将前节点pred[index.prev] 与新节点newNode连接/联系起来
            pred.next = newNode;
        //将 【前】 节点对象 刷新为 newNode
        pred = newNode;
    }
	
    //3.【最后一个newNode ←双向链接→ 尾节点】
    //当succ为空,插入位置Node为空——即是插入尾节点.next null
    if (succ == null) {
        //将尾节点刷为 pred 即是 newNode
        last = pred;
    } else {
        //将newNode的next指向succ对象
        pred.next = succ;
        //并同时更换succ.prev的值为newNode
        succ.prev = pred;
    }
    //节点计数器+a.length
    size += numNew;
    //修改计数器+1
    modCount++;
    return true;
}
//checkPositionIndex【要增添位置的参数合法性检测,否抛出异常】
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//isPositionIndex【判断索引是否 >0 && <size 返回boolean】
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}
//node【通过索引index获取node对象】
Node<E> node(int index) {
    // assert isElementIndex(index);
	//前半搜索【二分加速搜索】
    if (index < (size >> 1)) {
        Node<E> x = first;
        //取前向第index节点
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } 
    //后半搜索
    else {
        Node<E> x = last;
        //取后向第index节点
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

addAll()函数图解

在这里插入图片描述

增*10

//【C&L 1】尾部增1
public boolean add(E e) {
    //具体添加操作委托给linkLast()
    linkLast(e);
    return true;
}
//linkLast【增加一个新节点 至 原尾节点l后(l.next),并将 新节点 替换 尾节点last原指向】
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

//【C&L 2】中间插1
public void add(int index, E element) {
    //索引合法性检查,可抛出异常
    checkPositionIndex(index);

    //1.尾部加 1
    if (index == size)
        linkLast(element);
    //2.中间插 1
    else
        //node()获取 index处的 node节点对象
        linkBefore(element, node(index));
}
//linkBefore【单个值对象在succ节点处插入,对比构造函数中的双参addAll()简化步骤】
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    //1.建立 pred(succ.prev) <- newNode <-> succ 的前向/左向链接
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    //2.1如果是 首/头节点前插入
    if (pred == null)
        //,则将原首节点first刷新为newNode(null,e,原first) <-> succ
        first = newNode;
    //2.2如果是 非首节点前插入
    else
        //pred(succ.prev) <-> newNode <-> succ
        pred.next = newNode;
    size++;
    modCount++;
}

//【C&L 3,4】尾部增添,中间插入聚集C
//构造器中已经详细说明了这里就不赘述了
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
    ...
}

//【D 5】插入新头节点
public void addFirst(E e) {
    //调用本类内部抽取出的常用私有函数linkFirst,对外开放调用接口addFirst
    linkFirst(e);
}
//linkFirst【插入新头节点】
private void linkFirst(E e) {
    final Node<E> f = first;
    //新头节点.prev必然是null,.next指向原头节点f
    final Node<E> newNode = new Node<>(null, e, f);
    //刷新头结点指向newNode
    first = newNode;
    //情况1,空链表List
    if (f == null)
        last = newNode;
    //情况2,非空链表List
    else
        f.prev = newNode;
    size++;
    modCount++;
}

//【D 6】新增尾节点
public void addLast(E e) {
    //调用本类内部抽取出的常用私有函数linkLast,对外开放调用接口addLast
    linkLast(e);
}
//linkLast【新增尾节点】
void linkLast(E e) {
    final Node<E> l = last;
    //新尾节点.next必然是null,.prev指向原尾节点l
    final Node<E> newNode = new Node<>(l, e, null);
    //刷新尾节点指向newNode
    last = newNode;
    //情况1,空链表List
    if (l == null)
        first = newNode;
    //情况2,非空链表List
    else
        l.next = newNode;
    size++;
    modCount++;
}

//【D 7】同D 5
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}
//【D 8】同D 6
public boolean offerLast(E e) {
    addLast(e);
    return true;
}

//【Q 9】同C&L 1
public boolean offer(E e) {
    return add(e);
}

//【S 10】同D 5
public void push(E e) {
    addFirst(e);
}

删*12

0+3
//【C&L 1】通过索引,删除节点,并返回节点值
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}
//checkElementIndex【检测要删除的元素索引合法性,否抛异常】与checkPositionIndex作用相同,都是抛出异常
private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//isElementIndex【检测要删除的元素索引合法性】
//与isPositionIndex相比,少了一个右侧的<= size等号,因为增添可以在size处null节点加入,但删除不能删除size处
private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}
//node()已解析过,在构造函数中,此处跳过
//unlink【多向解链并连新链,最后返回解链节点值item对象】
E unlink(Node<E> x) {
    // assert x != null;
    //取出node节点的3属性值保存
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;
    //1.处理(破链+新链)node.prev前向双向链接
	//情况1:解链node为头结点
    if (prev == null) {
        first = next;
    } 
    //情况2:非头结点
    else {
       	//将node的前节点.next链接 到 node的后节点对象next
        prev.next = next;
        //并把node.prev置空
        x.prev = null;
    }
	//2.处理(破链+新链)node.next后向双向链接
    //情况1:解链node为尾结点
    if (next == null) {
        last = prev;
    } 
    //情况2:非尾结点
    else {
        //将node的后节点对象next.prev链接 到 node的前节点对象prev
        next.prev = prev;
        //并把node.next置空
        x.next = null;
    }
    //3.置空node.item
    x.item = null;
    size--;
    modCount++;
    return element;
}
//unlink总结:
/*
从链表List中解除一个Node节点,总共需要做3件事情【除去改变元素、修改计数器】
1.处理node前向双向链接【破旧双链,连新prev -> next单链】
2.处理node后向双向链接【破旧双链,连新prev <- next单链】
3.置空node.item
*/

//【C&L 2】通过item值对象,删节点,返回boolean
public boolean remove(Object o) {
    //情况1,删除null值对象
    if (o == null) {
        //遍历寻找第一个为node.item==null的节点x
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                //多向解链并连新链
                unlink(x);
                return true;
            }
        }
    } 
    //情况2,删除非null值对象
    else {
        for (Node<E> x = first; x != null; x = x.next) {
            //使用equals进行值对象匹配
            if (o.equals(x.item)) {
                //多向解链并连新链
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

//【C&L 3】删除所有Node节点
public void clear() {
	//1.遍历所有节点,并依次置空全部节点属性(3个)
    for (Node<E> x = first; x != null; ) {
        Node<E> next = x.next;
        x.item = null;
        x.next = null;
        x.prev = null;
        x = next;
    }
    //2.最后再置空 头尾节点引用
    first = last = null;
    size = 0;
    modCount++;
}

3+6
//【D 1】删队首,并返回队首.值对象item,抛异常
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
//unlinkFirst【将 头节点与next节点 进行双向解链,将next.prev置空next刷入first,并返回 头节点.值对象】
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    //1.取 头节点.item
    final E element = f.item;
    //2.令 头结点全部(3)属性置空【断开与其他节点的单向连接】,方便GC回收
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    //情况1,链表List只有被删除的头节点一个节点,则last尾节点置空且first一并置空,由上一句代码
    if (next == null)
        last = null;
    //情况2,链表List还有多个节点,将下一个next节点的prev置空,断开与原头节点f的单向连接
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

//【D 2】删队尾,并返回队尾.值对象item,抛异常
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    //解链尾节点
    return unlinkLast(l);
}
//unlinkLast【解链尾节点】
private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    //解链 prev <- last
    l.item = null;
    l.prev = null; // help GC
    last = prev;
    //情况1,只有1个节点,即是尾节点又是头结点
    if (prev == null)
        first = null;
    //解链 prev -> last
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}

//【D 3】镜像D 1 null方法
public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

//【D 4】镜像D 2 null方法
 public E pollLast() {
     final Node<E> l = last;
     return (l == null) ? null : unlinkLast(l);
 }

//【D 5】同C&L 2 first -> last
public boolean removeFirstOccurrence(Object o) {
	return remove(o);
}

//【D 6】删除 first <- last 后向前第一个o对象
public boolean removeLastOccurrence(Object o) {
    //情况1,删除item == null的节点
    if (o == null) {
        //后向前遍历查找
        for (Node<E> x = last; x != null; x = x.prev) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } 
    //情况2,删除非null值对象的节点
    else {
        //后向前遍历查找
        for (Node<E> x = last; x != null; x = x.prev) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

3+6+2
//【Q 1】同D 1
public E remove() {
    return removeFirst();
}

//【Q 2】镜像D 1 null方法
public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

3+6+2+1
//【S 1】同D 1
public E pop() {
    return removeFirst();
}

改*1

//1.根据索引修改节点值对象item
public E set(int index, E element) {
    //索引合法性检测,可抛异常
    checkElementIndex(index);
    //node()返回index处节点对象
    Node<E> x = node(index);
    //先保存旧值对象
    E oldVal = x.item;
    //将新值对象赋入
    x.item = element;
    return oldVal;
}

查*11

0+5
//【C&L 1】通过索引,查询值对象
public E get(int index) {
    //索引合法性检测,可抛异常
    checkElementIndex(index);
    return node(index).item;
}

//【C&L 2】查询 是否存在 某个值对象
public boolean contains(Object o) {
    //本质就是indexOf(循环匹配值对象)
    return indexOf(o) != -1;
}

//【C&L 3】查询元素总数
public int size() {
    return size;
}

//【C&L 4】通过对象o,前向后查询索引,first -> last 
public int indexOf(Object o) {
    //初始化索引对象【Linked没有Array的索引概念,只是简单的计数罢了】
    int index = 0;
    //情况1,查找值对象item为null的节点的索引
    if (o == null) {
        //循环遍历 ==
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            //计数器本质
            index++;
        }
    } 
    //情况2,查找值对象item非null的节点的索引
    else {
        //循环遍历 eqauls
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}
//总结:为什么LinkedList的遍历查找都要分两种item为null及非null,是因为要用不同的匹配方法: == 与 equals

//【C&L 5】通过对象o,后向前查询索引,first <- last
public int lastIndexOf(Object o) {
    int index = size;
    if (o == null) {
        for (Node<E> x = last; x != null; x = x.prev) {
            index--;
            if (x.item == null)
                return index;
        }
    } else {
        for (Node<E> x = last; x != null; x = x.prev) {
            index--;
            if (o.equals(x.item))
                return index;
        }
    }
    return -1;
}

0+5+2
//【Q 1】查询队首.值对象item,抛异常
public E element() {
    return getFirst();
}

//【Q 2】镜像Q 1 null方法
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

0+5+2+4
//【D 1】获取 队首.值对象item,抛异常
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

//【D 2】获取 队尾.值对象item,抛异常
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

//【D 3】镜像D 1 null方法
public E peekFirst() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

//【D 4】镜像D 2 null方法
public E peekLast() {
    final Node<E> l = last;
    return (l == null) ? null : l.item;
}

转换*2

//1.转换为Object[]
public Object[] toArray() {
    Object[] result = new Object[size];
    int i = 0;
    //遍历所有Node节点,获取Node.item值对象赋入new Object[]并返回
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;
    return result;
}
//2.转换为T[]
public <T> T[] toArray(T[] a) {
    //1.如果传入的T[] a长度不够,则通过反射new一个新的够长的T[] a
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.newInstance(
        a.getClass().getComponentType(), size);
    int i = 0;
    //2.T强转Object,令Node.item可以随意赋入a[]
    Object[] result = a;
    //3.遍历所有Node节点,获取Node.item值对象赋入Object[] a
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;
    //4.设置结束标志【为防止a[]对象内部有脏数据】
    if (a.length > size)
        a[size] = null;
    //5.返回T[] a 
    return a;
}

其他函数*1

//clone()方法,清零
public Object clone() {
    //克隆一个 新LinkedList对象
    LinkedList<E> clone = superClone();

    //初始化LinkedList的4个属性
    clone.first = clone.last = null;
    clone.size = 0;
    clone.modCount = 0;

    //新LinkedList对象循环添加add(Object item)——>复制被克隆对象的全部节点
    for (Node<E> x = first; x != null; x = x.next)
        clone.add(x.item);
	//返回 新LinkedList对象
    return clone;
}
//superClone【调用父类clone方法】
private LinkedList<E> superClone() {
    //强转类型,抛出异常
    try {
        return (LinkedList<E>) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new InternalError(e);
    }
}
//Object.clone();
public class Object {
    protected native Object clone() throws CloneNotSupportedException;
}

补充

关于Object父类的equals与hasCode方法的重写问题

问题1:为什么要重写equals?
原因:Object.equals()使用的是“ == ”比较的是内存地址。所以,如果任意两个对象调用equals()结果 都是 false,不论这两个对象的所有属性值是否完全相同。为了比较两对象的内容是否完全相同,我们需要重写equals方法。

问题2:为什么要一同复写hasCode方法?
原因:在集合框架中Hash系列的实现类(HashSet,HashMap)是通过hasCode()作为索引值进行对象存储的。int table[Object.hasCode()] = Object,此时如果不复写hasCode方法,我们就无法识别——内容完全相同的对象,有可能导致HashSet、HashMap不能正常的运作。当然这个并不是最主要的原因,这只是在使用时的部分影响。

核心因素:Object.hashCode的通用约定
1.如果一个对象的equals方法做比较所用到的信息没有被修改的话,那么,该对象无论调用多少次hashCode方法,hashCode()必须始终如一地返回 同一个整数。
2.如果两个对象根据equals()方法是相等的,那么调用这两个对象中的hashCode方法必须产生相等的整数结果。
3.如果两个对象根据equals()方法是不等的,那么不要求产生不同的整数结果。但需要意识到的是,对于不相等的对象产生截然不同的整数结果,有可能提高散列表(Hash家族)的性能。

参考:木叶之荣——java为什么要重写hashCode和equals方法

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值