• Java集合(2)——深入理解ArrayList、Vector和LinkedList


    回顾

    Java集合主要分为两个体系结构,Collection和Map。这篇博客主要介绍Collection子接口List下的三个经常使用的实现类:ArrayList、Vector和LinkedList。

    详细内容参见《Java基础——集合》

    先看下关系图:
    这里写图片描述

    1、ArrayList

    这是List最常用的实现类,想一想为什么他最常用?
    Array,在java中意为“数组”。猜想ArrayList和数组应该关系很密切,其实ArrayList可以看作是一个可以改变大小的数组。

    举个简单的例子吧,看下他的使用:

           ArrayList<String> list1 = new ArrayList<>();
            list1.add("a");
            list1.add("b");
            list1.add("c");
            list1.set(2, "d");
            Iterator<String> iter = list1.iterator();
            while (iter.hasNext()) {
                System.out.println(iter.next());
            }

    之后,我们就从源码看看他是如何设计的吧。

    1)构造函数

    提示:
    默认ArrayList长度为10;
    用于保存数据的elementData本身就是个Object[];
    ArrayList提供了三种构造函数,具体逻辑可以看下面的代码。

    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    {
    
       //默认容量为10
        private static final int DEFAULT_CAPACITY = 10;
    
        private static final Object[] EMPTY_ELEMENTDATA = {};
    
        //调用无参数构造函数时,给一个空数据
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
       //保存元素的数据
        transient Object[] elementData; 
    
        //1、无参数构造函数,默认空数组
         public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
        //2、给定初始容量的构造函数
         public ArrayList(int initialCapacity) {
            //大于0时,创建一个Object数据,长度为传入的容量值
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
                //等于0时,给一个空数组
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                //其他抛出容量不合法异常
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }
    
        //3、给定集合对象的构造函数
         public ArrayList(Collection<? extends E> c) {
            //放入数组
            elementData = c.toArray();
            //数组长度不等于0时,将进行复制
            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 {
                否则,返回空数组
                this.elementData = EMPTY_ELEMENTDATA;
            }
        }
    
    
    }

    2)动态扩充
    在添加元素时,会涉及到扩充的问题,其中核心方法是grow()。

    提示:

    “>>” 右移运算符:
    在二进制中比较容易理解,这里不是重点,大致相当于值的二分之一。因此,有的书上会说ArrayList扩容一次会增加原来的一半,就是从这里看出来的。

     private void grow(int minCapacity) {
            //获取原来的容量,即原来数组的长度
            int oldCapacity = elementData.length;
           //新容量
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
           //复制数组
            elementData = Arrays.copyOf(elementData, newCapacity);
        }

    2、Vector

    Vector和ArrayList几乎完全相同,直接看源码就知道了,瞬间有种剽窃的赶脚……
    但其实不是的,Vector是比ArrayList出现的要早的,也正是因为这样,很多方法都比较陈旧了,并不推荐适用男。

    这里贴了一小部分代码,和ArrayList几乎一样,就不占用空间了,有兴趣可以去java.util.Vector里面翻一翻。

    public class Vector<E>
        extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    {
    
       protected Object[] elementData;
       protected int elementCount;
       public Vector() {
            this(10);
        }
       public Vector(int initialCapacity, int capacityIncrement) {
            super();
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            this.elementData = new Object[initialCapacity];
            this.capacityIncrement = capacityIncrement;
        }

    当然Vector也有一些自己的特点,随便拿几个方法来看看。他的方法中几乎都用到了synchronized关键字,开销会比较大,而且运行会比较慢。

     public synchronized void copyInto(Object[] anArray) {
            System.arraycopy(elementData, 0, anArray, 0, elementCount);
        }
         public synchronized void setSize(int newSize) {
         }
          public synchronized int capacity() {
            return elementData.length;
        }
        //数组个数
          public synchronized int size() {
            return elementCount;
        }
        //获取元素
        public Enumeration<E> elements() {
            return new Enumeration<E>() {
                int count = 0;
    
                public boolean hasMoreElements() {
                    return count < elementCount;
                }
    
                public E nextElement() {
                    synchronized (Vector.this) {
                        if (count < elementCount) {
                            return elementData(count++);
                        }
                    }
                    throw new NoSuchElementException("Vector Enumeration");
                }
            };
        }

    3、LinkedList

    是一个双链表,在add和remove时比ArrayList性能好,但get和set时就特别慢了。

    恶补下:
    双向链表,链表的一种。每个数据结点中都有两个指针,分别指向直接前驱和直接后继。因此,我们可以方便的访问他的前驱结点和后继结点。


    下图是一个双向链表的图,element为元素,pre指向直接前驱(前一个 元素),next指向直接后继(后一个元素)。而LinkedList还不只是双向链表,他是双向循环链表。也就是第一个pre指针指向最后一个节点,最后一个节点的next指针指向第一个节点,形成一个环路,而不是下图中的Null。

    这里写图片描述

    下面来看下代码

    public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
        //元素个数
        transient int size = 0;
        //相当于pre指针
        transient Node<E> first;
        //相当于next指针
        transient Node<E> last;
    
        //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;
            }
        }
        ......
    }

    Tips:

    Deque,他是双端队列的接口,支持在两端插入和移除元素。

    public interface Deque extends Queue {}

    那么如何进行操作呢?这里以在中间位置添加元素为例。

    这里写图片描述

    代码实现如下

    
      //在指定位置添加元素
     public void add(int index, E element) {
            //检查索引是否有效
            checkPositionIndex(index);
    
            //索引值等于集合大小,直接添加在末尾
            if (index == size)
                linkLast(element);
            else
                linkBefore(element, node(index));
        }
    
        //返回索引处的非空Node
         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;
            }
        }
    
        //中间插入元素(核心方法!!!!)
         void linkBefore(E e, Node<E> succ) {
    
            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++;
        }
    
    

    对于性能这块,在大数据量重复试验下得出的结论才比较有说服力,小编就随手贴上自己的测试结果,有兴趣看看就可以了。

    测试代码差不多都是这样子的……

    ArrayList<Integer> arrayList = new ArrayList<Integer>();
            LinkedList<Integer> linkedList = new LinkedList<Integer>();
    
            // add操作
            int count = 100000;
    
            System.out.println("ArrayList--add()-------------------");
            long startTime = System.nanoTime();
            for (int i = 0; i < count; i++) {
                arrayList.add(i);
            }
            long endTime = System.nanoTime();
            System.out.println(endTime - startTime + "--------");
    
    
            System.out.println("LinkedList--add()-------------------");
            startTime = System.nanoTime();
            for (int i = 0; i < count; i++) {
                linkedList.add(i);
            }
            endTime = System.nanoTime();
            System.out.println(endTime - startTime + "--------");

    循环1W次

    这里写图片描述

    循环10W次

    这里写图片描述

    循环100W次

    等一下……
    等一下……
    再等一下……
    出去接杯水……

    结果还是这个样子,直接给个图吧,LinkedList还没get出来(⊙﹏⊙)b

    这里写图片描述

    嗯……这篇差不多了,over

  • 相关阅读:
    最小生成树算法
    并查集
    背包问题
    木桶排序
    STL之vector
    STL中的queue用法与stack用法对比
    快速幂求模
    归并排序+典型例题(逆序对)
    负进制转换
    冒泡排序
  • 原文地址:https://www.cnblogs.com/saixing/p/6730214.html
Copyright © 2020-2023  润新知