• ArrayList 与 LinkedList


    前言

    ArrayList 与 LinkedList 估计在java面试汇总肯定会问到,一般需要掌握的点有.
    比如还应该了解的有:

    1. 都是List,都java集合Collection接口的实现类.
    2. ArrayList基于数组实现,长度固定,但可以扩容.
    3. ArrayList查询快,增删慢,因为需要移动数据
    4. LinkedList增删快,不用移动数据
    5. LinkedList 提供了更多的方法来操作其中的数据,但是这并不意味着要优先使用LinkedList ,因为这两种容器底层实现的数据结构截然不同。
    6. 二者线程多不安全.为什么?

    所以本文将试图去源码上找答案.

    ArrayList

    ArrayList是可变长度数组-当元素个数超过数组的长度时,会产生一个新的数组,将原数组的数据复制到新数组,再将新的元素添加到新数组中。

    方法及源码

    • add
        //底册Object类型的数组
        transient Object[] elementData; // non-private to simplify nested class access
    
        /**
         * Appends the specified element to the end of this list.
         *
         * @param e element to be appended to this list
         * @return <tt>true</tt> (as specified by {@link Collection#add})
         */
        public boolean add(E e) { 
            ensureCapacityInternal(size + 1);  // 扩容
            elementData[size++] = e;  //扩容后直接index赋值
            return true;
        }
    

    先看是否越界,越界就扩容,
    扩容源码如下

        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            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); //通过拷贝放入新数组进行扩容
        }
    

    这里Arrays.copyOf 底层使用System.arraycopy拷贝到新数组(len+1)里.

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

    get直接通过index获取值,so快.

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

    没啥好讲的,直接覆盖.

    • 删除
        public E remove(int index) {
            rangeCheck(index); //检查边界
    
            modCount++;
            E oldValue = elementData(index);
    
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index, //拷贝new数组
                                 numMoved);
            elementData[--size] = null; // clear to let GC do its work
    
            return oldValue;
        }
    

    删除后把index+1前移.然后末尾置为null,这里注释交给GC不太懂. 有一次拷贝和移动数组.

    线程不安全

    • 为什么会线程不安全
    1. 因为size++属于非原子操作,这里在多线程下可能会被其他线拿到,又没有做扩容处理 可能会值覆盖或者越界即ArrayIndexOutOfBoundsException.
        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true; 
        }
    
    1. 如果两个线程擦操作同一个下标数据,可能会覆盖. 原因是a线程已经扩容了,b线程没有get到size的变化,直接坐了覆盖操作.
    • 如何保证线程安全

    首先理解线程安全,就是保证多个线程下数据

    1. Collections.synchronizedList
        public static <T> List<T> synchronizedList(List<T> list) {
            return (list instanceof RandomAccess ?
                    new SynchronizedRandomAccessList<>(list) :
                    new SynchronizedList<>(list));
        }
    
    
            public void add(int index, E element) {
                synchronized (mutex) {list.add(index, element);} //同步
            }
    

    但是源码注释有写到,遍历集合的时候需要手动同步

         * It is imperative that the user manually synchronize on the returned
         * list when iterating over it:
         * <pre>
         *  List list = Collections.synchronizedList(new ArrayList());
         *      ...
         *  synchronized (list) {
         *      Iterator i = list.iterator(); // Must be in synchronized block
         *      while (i.hasNext())
         *          foo(i.next());
         *  }
         * </pre>
    

    所以为了防止忘记,建议第二种.

    1. 使用CopyOnWriteArrayList
        public boolean add(E e) {
            final ReentrantLock lock = this.lock; //你懂的
            lock.lock();
            try {
                Object[] elements = getArray();
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len + 1);
                newElements[len] = e;
                setArray(newElements);
                return true;
            } finally {
                lock.unlock();
            }
        }
    

    LinkedList

    LinkedList 底层的数据结构是基于双向循环链表的,且头结点中不存放数据,如下:
    image

    • add
        public boolean add(E e) {
            linkLast(e);
            return true;
        }
        
        
        
        /**
         * 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++;
        }
    
    
    • remove
        public E remove(int index) {
            checkElementIndex(index);
            return unlink(node(index));
        }
    
        /**
         * Unlinks non-null node x.
         */
        E unlink(Node<E> x) {
            // assert x != null;
            final E element = x.item;
            final Node<E> next = x.next;
            final Node<E> prev = x.prev;
    
            if (prev == null) {
                first = next;
            } else {
                prev.next = next;  // 将前一节点的next引用赋值为x的下一节点
                x.prev = null; //解除了x节点对前节点的引用
            }
    
            if (next == null) {
                last = prev;
            } else {
                next.prev = prev; // 将x的下一节点的previous赋值为x的上一节点
                x.next = null; //解除了x节点对后节点的引用
            }
    
            x.item = null; // 将被移除的节点的内容设为null
            size--;
            modCount++;
            return element;
        }
    

    删除过程基本上就是:

    1. 前节点指向后节点
    2. 当前pre为空
    3. 后节点的pre指向前节点
    4. 当前节点next为空
    5. 当前节点为空 交给gc处理
    6. size--

    与ArrayList比较而言,LinkedList的删除动作不需要“移动”很多数据,从而效率更高。

    • get
        public E get(int index) {
            checkElementIndex(index);
            return node(index).item;
        }
    
        Node<E> node(int index) {
            // assert isElementIndex(index);
           // 获取index处的节点。    
            // 若index < 双向链表长度的1/2,则从前先后查找;    
            // 否则,从后向前查找。  
            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;
            }
        }
    

    index与长度size的一半比较,要么从头开始,要么从尾部开始. 但是都需要挨个查找,所以这里体验出LinkedList比ArrayList查询效率慢了

    多线程下

    LinkedList源码中发现,同样会出现非原子性操作 size++/size--问题,所以是线程不安全的.

    1. Collections.synchronizedList 通ArrayList同步方法
    2. ConcurrentLinkedQueue 这个源码下详解

    总结

    存储结构

    ArrayList是基于数组实现的;按照顺序将元素存储(从下表为0开始),删除元素时,删除操作完成后,需要使部分元素移位,默认的初始容量都是10.

    LinkedList是基于双向链表实现的(含有头结点)。

    线程安全性

    ArrayList 与 LinkedList 因为都不是原子操作,都不能保证线程安全.
    Collections.synchronizedList,但是迭代list需要自己手动加锁

    ps:
    Vector实现线程安全的,即它大部分的方法都包含关键字synchronized,但是Vector的效率没有ArraykList和LinkedList高。

    效率

    ArrayList 从指定的位置检索一个对象,或在集合的末尾插入、删除一个元素的时间是一样的,时间复杂度都是O(1)

    LinkedList中,在插入、删除任何位置的元素所花费的时间都是一样的,时间复杂度都为O(1),但是他在检索一个元素的时间复杂度为O(n).

    ps: 事实上lindlinst并不会节省空间,只是相比增删节省时间而已.

  • 相关阅读:
    顶点着色器详解 (Vertex Shaders)
    Texture Filter中的Bilinear、Trilinear以及Anistropic Filtering
    mysql远程访问1045的问题解决
    在eclipse上集成安装阿里巴巴代码规约P3C插件
    Mybatis Generator主要配置详解
    redis cluster最简配置
    springboot +redis配置
    redis cluster的conf配置文件配置
    MySQL与Oracle的主要区别
    java EE中使用PO和VO的注意事项
  • 原文地址:https://www.cnblogs.com/psyco/p/13646503.html
Copyright © 2020-2023  润新知