• Java集合之ArrayList与LinkList


    注:示例基于JDK1.8版本

    参考资料:Java知音公众号 

    本文超长,也是搬运的干货,希望小伙伴耐心看完。

    Collection集合体系

    List、Set、Map是集合体系的三个接口。

    其中List和Set继承了Collection接口。

    List有序且元素可以重复,默认大小为10;ArrayList、LinkedList和Vector是三个主要的实现类。

    Set元素不可以重复,HashSet和TreeSet是两个主要的实现类。

    Map也属于集合系统,但和Collection接口不同。Map是key-value键值对形式的集合,key值不能重复,value可以重复;HashMap、TreeMap和Hashtable是三个主要的实现类。

    --------------------------------------------------------------------------------------------------------------------

    一、ArrayList

    ArrayList基层是以数组实现的,可以存储任何类型的数据,但数据容量有限制,超出限制时会扩增50%容量,查找元素效率高。

    ArrayList是一个简单的数据结构,因超出容量会自动扩容,可认为它是常说的动态数组。

    源码分析

    A、属性分析

    /**
     * 默认初始化容量
     */
    private static final int DEFAULT_CAPACITY = 10;
    
    /**
     * 如果自定义容量为0,则会默认用它来初始化ArrayList。或者用于空数组替换。
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    /**
     * 如果没有自定义容量,则会使用它来初始化ArrayList。或者用于空数组比对。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    /**
     * 这就是ArrayList底层用到的数组
    
     * 非私有,以简化嵌套类访问
     * transient 在已经实现序列化的类中,不允许某变量序列化
     */
    transient Object[] elementData;
    
    /**
     * 实际ArrayList集合大小
     */
    private int size;
    
    /**
     * 可分配的最大容量
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    B、构造方法分析

    1、不带参数初始化,默认容量为10

        /**
         * Constructs an empty list with an initial capacity of ten.
         */
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }

    2、根据initialCapacity初始化一个空数组,如果值为0,则初始化一个空数组

    /**
     * 根据initialCapacity 初始化一个空数组
     */
    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);
        }
    }

    3、通过集合做参数的形式初始化,如果集合为空,则初始化为空数组

    /**
     * 通过集合做参数的形式初始化
     */
    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;
        }
    }

    C、主要方法

    1、trimToSize()方法:

    用来最小实例化存储,将容器大小调整为当前元素所占用的容量大小。

    /**
     * 这个方法用来最小化实例存储。
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

    判断size的值,若为0,则将elementData置为空集合,若大于0,则将一份数组容量大小的集合复制给elementData。

    2、clone()方法

    克隆一个新数组。

    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

    通过调用Objectclone()方法来得到一个新的ArrayList对象,然后将elementData复制给该对象并返回。

    3、add(E e)

    在ArrayList末尾添加元素。

    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
    }

    该方法首先调用了ensureCapacityInternal()方法,注意参数是size+1(数组已有参数个数+1个新参数),先来看看ensureCapacityInternal的源码:

    private void ensureCapacityInternal(int minCapacity) {
       if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
           minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
       }
       ensureExplicitCapacity(minCapacity);
    }

    方法说明:计算容量+确保容量(上上代码)

    计算容量:若elementData为空,则minCapacity值为默认容量和size+1(minCapacity)的最大值;若elementData不为空,minCapacity(size+1)不用进行操作

    确保容量:如果size+1 > elementData.length证明数组已经放满,则增加容量,调用grow()

    private void ensureExplicitCapacity(int minCapacity) {
       modCount++;//计算修改次数
    
       // overflow-conscious code
       if (minCapacity - elementData.length > 0)
           grow(minCapacity);
    }
    
    private void grow(int minCapacity) {
       // overflow-conscious code
       int oldCapacity = elementData.length;
    
       // 扩展为原来的1.5倍
       int newCapacity = oldCapacity + (oldCapacity >> 1);
    
       // 如果扩为1.5倍还不满足需求,直接扩为需求值
       if (newCapacity - minCapacity < 0)
           newCapacity = minCapacity;
       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);
    }

    增加容量grow():默认1.5倍扩容。

    1. 获取当前数组长度=>oldCapacity

    2. oldCapacity>>1 表示将oldCapacity右移一位(位运算),相当于除2。再加上1,相当于新容量扩容1.5倍。

    3. 如果newCapacity<mincapacity`,则`newcapacity mincapacity="size+1=2" elementdata="1" newcapacity="1+1""">&gt;1=1,1&lt;2所以如果不处理该情况,扩容将不能正确完成。

    4. 如果新容量比最大值还要大,则将新容量赋值为VM要求最大值。

    5. 将elementData拷贝到一个新的容量中。

    也就是说,当增加数据的时候,如果ArrayList的大小已经不满足需求时,那么就将数组变为原长度的1.5倍,之后的操作就是把老的数组拷到新的数组里面。

    例如,默认的数组大小是10,也就是说当我们add10个元素之后,再进行一次add时,就会发生自动扩容,数组长度由10变为了15具体情况如下所示:

    4、add(int index, E element)方法

    public void add(int index, E element) {
        rangeCheckForAdd(index);
    
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

    rangeCheckForAdd()是越界异常检测方法。

    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    System.arraycopy方法:

    public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
    • Object src : 原数组

    • int srcPos : 从元数据的起始位置开始

    • Object dest : 目标数组

    • int destPos : 目标数组的开始起始位置

    • int length : 要copy的数组的长度

     5、set(int index, E element)方法:

    用指定元素替换此列表中指定位置的元素。

    public E set(int index, E element) {
        rangeCheck(index);
    
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
    
    E elementData(int index) {
        return (E) elementData[index];
    }

    6、indexOf(Object o):

     返回数组中第一个与参数相等的值的索引,允许null。

    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

    7、get(int index)方法:

    返回指定下标处的元素的值。

    public E get(int index) {
        rangeCheck(index);
    
        return elementData(index);
    }

    rangeCheck(index)会检测index值是否合法,如果合法则返回索引对应的值。

    8、remove(int index):

    删除指定下标的元素。

    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);
        elementData[--size] = null; // clear to let GC do its work
    
        return oldValue;
    }

    ArrayList优缺点

    优点:

    1、因为其底层是数组,所以修改和查询效率高。

    2、自动扩容(1.5倍)。

    缺点:

    1、插入和删除效率不高。(文末对比LinkedList)

    2、线程不安全。

    二、LinkedList

    LinkList以双向链表实现,链表无容量限制,但双向链表本身使用了更多空间,每插入一个元素都要构造一个额外的Node对象,也需要额外的链表指针操作。允许元素为null,线程不安全。

    源码分析

    1、变量

    /**
     * 集合元素数量
     **/
    transient int size = 0;
    
    /**
     * 指向第一个节点的指针
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;
    
    /**
     * 指向最后一个节点的指针
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

    2、构造方法

    /**
     * 无参构造方法
     */
    public LinkedList() {
    }
    
    /**
     * 将集合c所有元素插入链表中
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

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

    Node既有prev也有next,所以证明它是一个双向链表。

    4、添加元素

    a、addAll(Collection c)方法

    将集合c添加到链表,如果不传index,则默认是添加到尾部。如果调用addAll(int index, Collection<? extends E> c)方法,则添加到index后面。

    /**
     * 将集合添加到链尾
     */
    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
    
    /** 
     * 
     */
    public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);
    
        // 拿到目标集合数组
        Object[] a = c.toArray();
        //新增元素的数量
        int numNew = a.length;
        //如果新增元素数量为0,则不增加,并返回false
        if (numNew == 0)
            return false;
    
        //定义index节点的前置节点,后置节点
        Node<E> pred, succ;
        // 判断是否是链表尾部,如果是:在链表尾部追加数据
        //尾部的后置节点一定是null,前置节点是队尾
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            // 如果不在链表末端(而在中间部位)
            // 取出index节点,并作为后继节点
            succ = node(index);
            // index节点的前节点 作为前驱节点
            pred = succ.prev;
        }
    
        // 链表批量增加,是靠for循环遍历原数组,依次执行插入节点操作
        for (Object o : a) {
            @SuppressWarnings("unchecked") 
            // 类型转换
            E e = (E) o;
            // 前置节点为pred,后置节点为null,当前节点值为e的节点newNode
            Node<E> newNode = new Node<>(pred, e, null);
            // 如果前置节点为空, 则newNode为头节点,否则为pred的next节点
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }
    
        // 循环结束后,如果后置节点是null,说明此时是在队尾追加的
        if (succ == null) {
            // 设置尾节点
            last = pred;
        } else {
        //否则是在队中插入的节点 ,更新前置节点 后置节点
            pred.next = succ;
            succ.prev = pred;
        }
    
        // 修改数量size
        size += numNew;
        //修改modCount
        modCount++;
        return true;
    }
    
    /**
      * 取出index节点
      */ 
    Node<E> node(int index) {
        // assert isElementIndex(index);
    
        // 如果index 小于 size/2,则从头部开始找
        if (index < (size >> 1)) {
            // 把头节点赋值给x
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                // x=x的下一个节点
                x = x.next;
            return x;
        } else {
            // 如果index 大与等于 size/2,则从后面开始找
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
    
    
    // 检测index位置是否合法
    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    // 检测index位置是否合法
    private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }  

    假设我们要在index=2处添加{1,2}到链表中,图解如下:

    第一步:拿到index=2的前驱节点 prev=ele1

    第二步:遍历集合prev.next=newNode,并实时更新prev节点以便下一次遍历:prev=newNode

    第三步:将index=2的节点ele2接上:prev.next=ele2,ele2.prev=prev

     

    注意node(index)方法:方法:寻找处于index的节点,有一个小优化,结点在前半段则从头开始遍历,在后半段则从尾开始遍历,这样就保证了只需要遍历最多一半结点就可以找到指定索引的结点。

    b、addFirst(E e)方法

    将e元素添加到链表并设置其为头节点(first)。

    public void addFirst(E e) {
        linkFirst(e);
    }
    
    //将e链接成列表的第一个元素
    private void linkFirst(E e) {
    
        final Node<E> f = first;
        // 前驱为空,值为e,后继为f
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        //若f为空,则表明列表中还没有元素,last也应该指向newNode
        if (f == null)
            last = newNode;
        else
        //否则,前first的前驱指向newNode
            f.prev = newNode;
        size++;
        modCount++;
    }
    1. 拿到first节点命名为f

    2. 新创建一个节点newNode设置其next节点为f节点

    3. 将newNode赋值给first

    4. 若f为空,则表明列表中还没有元素,last也应该指向newNode;否则,前first的前驱指向newNode。

    5. 图解如下:

     

    c、addLast(E e)方法

    将e元素添加到链表并设置其为尾节点(last)。

    public void addLast(E e) {
        linkLast(e);
    }
    /**
     * 将e链接成列表的last元素
     */
    void linkLast(E e) {
        final Node<E> l = last;
        // 前驱为前last,值为e,后继为null
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        //最后一个节点为空,说明列表中无元素
        if (l == null)
            //first同样指向此节点
            first = newNode;
        else
            //否则,前last的后继指向当前节点
            l.next = newNode;
        size++;
        modCount++;
    }

    过程与linkFirst()方法类似,这里略过。

    d、add(E e)方法

    在尾部追加元素e。

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
    void linkLast(E e) {
        final Node<E> l = last;
        // 前驱为前last,值为e,后继为null
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        //最后一个节点为空,说明列表中无元素
        if (l == null)
            //first同样指向此节点
            first = newNode;
        else
            //否则,前last的后继指向当前节点
            l.next = newNode;
        size++;
        modCount++;
    }

    e、add(int index, E element)方法

    在链表的index处添加元素element.

    public void add(int index, E element) {
        checkPositionIndex(index);
    
        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
    /**
     * 在succ节点前增加元素e(succ不能为空)
     */
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        // 拿到succ的前驱
        final Node<E> pred = succ.prev;
        // 新new节点:前驱为pred,值为e,后继为succ
        final Node<E> newNode = new Node<>(pred, e, succ);
        // 将succ的前驱指向当前节点
        succ.prev = newNode;
        // pred为空,说明此时succ为首节点
        if (pred == null)
            // 指向当前节点
            first = newNode;
        else
            // 否则,将succ之前的前驱的后继指向当前节点
            pred.next = newNode;
        size++;
        modCount++;
    }

    5、获取/查询元素

    a、get(int index)

    根据索引获取链表中的元素。

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    
    // 检测index合法性
    private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    // 根据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;
        }
    }

    b、getFirst()方法

    获取头节点。

    public E getFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return f.item;
    }

    c、getLast()方法

    获取尾节点。

    public E getLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return l.item;
    }

    6、删除元素

    a、remove(Object o)

    根据Object对象删除元素。

    public boolean remove(Object o) {
        // 如果o是空
        if (o == null) {
            // 遍历链表查找 item==null 并执行unlink(x)方法删除
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }
    
    E unlink(Node<E> x) {
        // assert x != null;
        // 保存x的元素值
        final E element = x.item;
        //保存x的后继
        final Node<E> next = x.next;
        //保存x的前驱
        final Node<E> prev = x.prev;
    
        //如果前驱为null,说明x为首节点,first指向x的后继
        if (prev == null) {
            first = next;
        } else {
            //x的前驱的后继指向x的后继,即略过了x
            prev.next = next;
            // x.prev已无用处,置空引用
            x.prev = null;
        }
    
        // 后继为null,说明x为尾节点
        if (next == null) {
            // last指向x的前驱
            last = prev;
        } else {
            // x的后继的前驱指向x的前驱,即略过了x
            next.prev = prev;
            // x.next已无用处,置空引用
            x.next = null;
        }
        // 引用置空
        x.item = null;
        size--;
        modCount++;
        // 返回所删除的节点的元素值
        return element;
    }
    1. 遍历链表查找 item==null 并执行unlink(x)方法删除

    2. 如果前驱为null,说明x为首节点,first指向x的后继,x的前驱的后继指向x的后继,即略过了x.

    3. 如果后继为null,说明x为尾节点,last指向x的前驱;否则x的后继的前驱指向x的前驱,即略过了x,置空x.next

    4. 引用置空:x.item = null

    5. 图解:

    b、remove(int index)方法

    根据链表的索引删除元素。

    public E remove(int index) {
        checkElementIndex(index);
        //node(index)会返回index对应的元素
        return unlink(node(index));
    }

    c、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;
        l.item = null;
        l.prev = null; // help GC
        // last指向前last的前驱,也就是列表中的倒数2号位
        last = prev;
        // 如果此时倒数2号位为空,那么列表中已无节点
        if (prev == null)
            // first指向null
            first = null;
        else
            // 尾节点无后继
            prev.next = null;
        size--;
        modCount++;
        // 返回尾节点保存的元素值
        return element;
    }

    7、修改元素

    修改元素比较简单,先找到index对应节点,然后对值进行修改。

    public E set(int index, E element) {
        checkElementIndex(index);
        // 获取到需要修改元素的节点
        Node<E> x = node(index);
        // 保存之前的值
        E oldVal = x.item;
        // 执行修改
        x.item = element;
        // 返回旧值
        return oldVal;
    }

    LinkedList优点:不需要扩容和预留空间,空间效率高。

    三、ArrayList与LinkedList插入和查找消耗时间测试对比

    参考链接:https://blog.csdn.net/dearKundy/article/details/84663512

    在ArrayList和LinkedList的头部、尾部和中间三个位置插入与查找100000个元素所消耗的时间来进行对比测试,下面是测试结果:(时间单位ms)

      插入 查找
    ArrayList尾部 26 12
    ArrayList头部 859 7
    ArrayList中间 1848 13
    LinkedList尾部 28 9
    LinkedList头部 15 11
    LinkedList中间 15981 34928

    测试结论:
    ArrayList的查找性能绝对是一流的,无论查询的是哪个位置的元素
    ArrayList除了尾部插入的性能较好外(位置越靠后性能越好),其他位置性能就不如人意了
    LinkedList在头尾查找、插入性能都是很棒的,但是在中间位置进行操作的话,性能就差很远了,而且跟ArrayList完全不是一个量级的

    根据源码分析所得结论:

    • 对于LinkedList来说,头部插入和尾部插入时间复杂度都是O(1)
    • 但是对于ArrayList来说,头部的每一次插入都需要移动size-1个元素,效率可想而知
    • 但是如果都是在最中间的位置插入的话,ArrayList速度比LinkedList的速度快将近10倍
  • 相关阅读:
    RTB交接
    awk命令详解
    Linux下的压缩解压缩命令详解
    inux下文件权限设置中的数字表示权限,比如777,677等,这个根据什么得来的
    jmeter接口测试教程
    kafka常用的操作命令
    hadoop常用的操作命令
    linux常用命令
    hive的常用命令
    用shell脚本写一个for循环
  • 原文地址:https://www.cnblogs.com/GG-Bond/p/10603071.html
Copyright © 2020-2023  润新知