java集合总结(二)List

本文为阅读https://blog.csdn.net/justloveyou_/article/details/52955619 所做的总结

(一)概述

1.List:

     包括List接口和List接口的所有实现类(ArrayList,LinkedList,Vector,Stack)


2.结构:

       1. AbstractList是一个抽象类,它实现了List接口并继承于AbstractCollection,AbstractList给出了iiterator方法的实现。

       2.AbstractSequentialList是一个抽象类,它继承于AbstractList

       3.ArrayList, LinkedList, Vector, Stack 是 List 的 4 个实现类

          1.ArrayList是一个动态数组。它由数组实现,随机访问效率高、随机插入、随机删除效率低

          2.LinkedList是一个双向链表。它随机访问效率低,但随机插入、随机删除的效高。

          3.vector是矢量队列,由数组实现,但ArrayList是非线程安全的,Vector是线程安全的

          4.stack是栈,它继承于Vector

3.List特性:

        1.如果使用泛型,则只可以容纳泛型指定的元素,如果不适用泛型,则可以容纳任何类型。

           List的容量可以动态扩展。

        2.List的元素是有序的,我们可以对某个元素在集合中的位置进行指定。

        3.List的元素是可以重复的,因为其有序的数据结构

        4.List是可以包括null的,即使使用了泛型


Header

ArrayList 也是非常常用的集合类。它是有序的并且可以存储重复元素的。 ArrayList 底层其实就是一个数组,并且会动态扩容的。

转自https://yuqirong.me/2018/01/21/ArrayList%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/

源码分析

构造方法

 
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 创建初始容量的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

public ArrayList() {
    // 默认为空数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        // 将集合中的元素复制到数组中
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

构造方法中的代码比较简短,大家都能理解的吧。

add()

 
public boolean add(E e) {
    // 确保数组的容量,保证可以添加该元素
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将该元素放入数组中
    elementData[size++] = e;
    return true;
}

发现在 add() 方法中,代码很简短。可以看出之前的预操作都放入了 ensureCapacityInternal 方法中,这个方法会去确保该元素在数组中有位置可以放入。

那么我们来看看这个方法:

 
private void ensureCapacityInternal(int minCapacity) {
    // 如果数组是空的,那么会初始化该数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // DEFAULT_CAPACITY 为 10 ,所以调用无参默认 ArrayList 构造方法初始化的话,默认的数组容量为 10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // 确保数组的容量,如果不够的话,调用 grow 方法扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

看了半天,扩容是在 grow 方法中完成的,所以我们接着跟进。

 
private void grow(int minCapacity) {
    // 当前数组的容量
    int oldCapacity = elementData.length;
    // 新数组扩容为原来容量的 1.5 倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果新数组扩容容量还是比最少需要的容量还要小的话,就设置扩充容量为最小需要的容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //判断新数组容量是否已经超出最大数组范围,MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    // 复制元素到新的数组中
    elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容方法其实就是新创建一个数组,然后将旧数组的元素都复制到新数组里面。

当然,add 还有一个重载的方法 add(int index, E element) ,顺便我们也来看一下。

 
public void add(int index, E element) {
    // 判断 index 有没有超出索引的范围
    rangeCheckForAdd(index);
    // 和之前的操作是一样的,都是保证数组的容量足够
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将指定位置及其后面数据向后移动一位
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 将该元素添加到指定的数组位置
    elementData[index] = element;
    // ArrayList 的大小改变
    size++;
}

好了,add 方法看的差不多了,剩下还有一个 addAll(Collection<? extends E> c) 方法也是换汤不换药的,可以自己回去看下,这里就不讲了。

get()

get 方法很简单,就是在数组中返回指定位置的元素即可。

 
public E get(int index) {
    // 检查 index 有没有超出索引的范围
    rangeCheck(index);
    // 返回指定位置的元素
    return elementData(index);
}

remove()

 
public E remove(int index) {
    // 检查 index 有没有超出索引的范围
    rangeCheck(index);

    modCount++;
    // 保存一下需要删除的数据
    E oldValue = elementData(index);
    // 让指定删除的位置后面的数据,向前移动一位
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 方便 gc 释放内存
    elementData[--size] = null; // clear to let GC do its work
    // 返回旧值
    return oldValue;
}

remove 中主要是将之后的元素都向前一位移动,然后将最后一位的值设置为空。最后,返回已经删除的值。

同样,remove 还有一个重载的方法 remove(Object o) 。

 
public boolean remove(Object o) {
    if (o == null) {
        // 如果有元素的值为 null 的话,移除该元素,fastRemove 的操作和上面的 remove(int index) 是类似的
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        // 如果有元素的值等于 o 的话,移除该元素
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

clear()

clear 方法无非就是遍历数组,然后把所有的值都置为 null 。

 
public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

Footer

至此,ArrayList 主要的几个方法就讲完了。ArrayList 的源码还是比较简单的,基本上都可以看得明白。

我们来总结一下:

  1. ArrayList底层是基于数组来实现的,因此在 get 的时候效率高,而 add 或者 remove 的时候,效率低;
  2. 调用默认的 ArrayList 无参构造方法的话,数组的初始容量为 10 ;
  3. ArrayList 会自动扩容,扩容的时候,会将容量扩至原来的 1.5 倍;
  4. ArrayList 不是线程安全的;

Fail-Fast机制:

          动机:在JAVA collection中,为了防止在某个线程对Collecion迭代时,其他线程对该Collecion的结构上的修改

          本质:Fail-Fast是JAVA集合的一种错误检测机制

           场景:在使用迭代器时,Collection 的结构发生变化,抛出 ConcurrentModificationException

          例如:  有两个线程(线程A,线程B),其中线程A负责遍历list、线程B修改list。线程A在遍历list过程的某个时候(此时expectedModCount = modCount=N),线程启动,同时线程B增加一个元素,这是modCount的值发生改变(modCount + 1 = N + 1)。线程A继续遍历执行next方法时,通告checkForComodification方法发现expectedModCount = N ,而modCount = N + 1,两者不等,这时触发 fail-fast机制

总结:

 三个不同的构造方法无参构造方法构造的ArrayList的容量默认为10; 带有Collection参数的构造方法的实现是将Collection转化为数组赋给ArrayList的实现数组elementData。

    ArrayList 基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此 查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低;
   
    在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理,ArrayList中允许元素为null。

 

LinkedList实现原理分析(Java源码剖析)转自https://www.jianshu.com/p/56c77c517e71

  • 本文对LinkedList的实现讨论都基于JDK8版本

Java中的LinkedList类实现了List接口和Deque接口,是一种链表类型的数据结构,支持高效的插入和删除操作,同时也实现了Deque接口,使得LinkedList类也具有队列的特性。LinkedList类的底层实现的数据结构是一个双端的链表。

LinkedList类中有一个内部私有类Node,这个类就代表双端链表的节点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;
        }
    }

注意这个节点的初始化方法,给定三个参数,分别前驱节点,本节点的值,后继结点。这个方法将在LinkedList的实现中多次调用。

下图是LinkedList内部结构的可视化,能够帮我们更好的理解LinkedList内部的结构。

image.png

双端链表由node组成,每个节点有两个reference指向前驱节点和后继结点,第一个节点的前驱节点为null,最后一个节点的后继节点为null。

LinkedList类有很多方法供我们调用。我们不会一一介绍,本文会详细介绍其中几个最核心最基本的方法,LinkedList的创建添加和删除基本都和这几个操作有关。

  • linkFirst() method
    首先我们介绍第一个方法,linkFirst(),顾名思义,这个方法是插入第一个节点,我们先直接上代码,看看它的具体实现
/**
     * Links e as first element.
     */
    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++;
    }

我们发现出现了两个变量,first和last这两个变量是LinkedList的成员变量,分别指向头结点和尾节点。他们是如下定义的:

/**
     * 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;

我们可以看到注释中的内容。first和last需要维持一个不变量,也就是first和last始终都要维持两种状态:
首先,如果双端链表为空的时候,两个都必须为null
如果链表不为空,那么first的前驱节点一定是null,first的item一定不为null,同理,last的后继节点一定是null,last的item一定不为null。

知道了first和last之后,我们就可以开始分析linkFirst的代码了。
linkFirst的作用就是在first节点的前面插入一个节点,插入完之后,还要更新first节点为新插入的节点,并且同时维持last节点的不变量。

我们开始分析代码,首先用f来临时保存未插入前的first节点,然后调用的node的构造函数新建一个值为e的新节点,这个节点插入之后将作为first节点,所以新节点的前驱节点为null,值为e,后继节点是f,也就是未插入前的first节点。
然后就是维持不变量,首先第一种情况,如果f==null,那就说明插入之前,链表是空的,那么新插入的节点不仅是first节点还是last节点,所以我们要更新last节点的状态,也就是last现在要指向新插入的newNode。
如果f!=null那么就说明last节点不变,但是要更新f的前驱节点为newNode,维持first节点的不变量。
最后size加一就完成了操作。

  • linkLast() method
    分析了linkFirst方法,对于 linkLast()的代码就很容易理解了,只不过是变成了插入到last节点的后面。我们直接看代码
/**
     * Links e as last element.
     */
    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++;
    }

到这里我们发现有这个两个方法,我们已经可以实现一个简单队列的插入操作,上面两个方法就可以理解为插入队头元素和队尾元素,这也说明了LinkedList是实现了Deque接口的。
从源码中也可以看出,addfirst和addLast这两个方法内部就是直接调用了linkFirst和LinkLast

/**
     * Inserts the specified element at the beginning of this list.
     *
     * @param e the element to add
     */
    public void addFirst(E e) {
        linkFirst(e);
    }

    /**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #add}.
     *
     * @param e the element to add
     */
    public void addLast(E e) {
        linkLast(e);
    }
  • linkBefore(E e, Node<E> succ)
    下面我们看一个linkBefore方法,从名字可以看出这个方法是在给定的节点前插入一个节点,可以说是linkFirst和linkLast方法的通用版。
/**
     * Inserts element e before non-null Node succ.
     */
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

我们可以看到代码的实现原理基本和前面的两个方法一致,这里是假设插入的这个节点的位置是非空的。

  • add(int index, E element)
    下面我们看add方法,这个方法就是最常用的,在指定下标插入一个节点。我们先来看下源码的实现,很简单
/**
     * Inserts the specified element at the specified position in this list.
     * Shifts the element currently at that position (if any) and any
     * subsequent elements to the right (adds one to their indices).
     *
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

首先判断给定的index是不是合法的,然后如果index==size,就说明要插入成为最后一个节点,直接调用linklast方法,否则就调用linkBefore方法,我们知道linkBefore需要给定两个参数,一个插入节点的值,一个指定的node,所以我们又调用了Node(index)去找到index的那个node。
我们看一下Node<E> node(int index)方法,这个方法就是找到给定index的node并返回,类似于数组的随机读取,但由于这里是链表,所以要进行查找

/**
     * Returns the (non-null) Node at the specified element index.
     */
    Node<E> node(int index) {
        // assert isElementIndex(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;
        }
    }

我们看到node的实现并不是像我们想象的那样直接就线性从头查找,而是折半查找,有一个小优化,先判断index在前半段还是后半段,如果在前半段就从头开始找,如果在后半段就从后开始找,这样最坏情况也只要找一半就可以了。

LinkedList的源码实现并不复杂,我们只介绍这几个方法,相信你一定对于它的内部实现原理有了一定的了解,并且也学习到了优秀的代码书写风格和优化。
对于remove操作,有兴趣的读者可以自行研究代码,它类似于add操作,也是基于三个基本方法来实现的。

  • unlinkFirst(Node<E> f)
  • unlinkLast(Node<E> l)
  • unlink(Node<E> x)

猜你喜欢

转载自blog.csdn.net/weixin_42173193/article/details/88221558