ArrayList和LinkedList的底层分析和比较

一.ArrayList

1.1核心成员变量

ArrayList的底层是一个动态数组,默认大小为10。

transient Object[] elementData;//动态数组
 
private static final int DEFAULT_CAPACITY = 10;//默认大小

private int size;//已经存在的元素个数

 protected transient int modCount = 0;//modCount 用来记录 ArrayList 结构发生变化的次数。

需要注意的是modCount用来记录ArrayList结构变化的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。主要应用是迭代器的fail-fast中运用。modCount如果出现了预料之外的改变,会抛出ConcurrentModificationException异常。后面会讲到。

1.2核心方法

1.2.1 add()

//add第一个方法  private类型
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)//如果超过了数组大小,需要扩容
            elementData = grow();//扩容
        elementData[s] = e;
        size = s + 1;
    }
//add第二方法 public类型
    public boolean add(E e) {
        modCount++;//修改次数+1
        add(e, elementData, size);//调用add的另一个方法
        return true;
    }
//add第三个方法,用于特定位置插入
public void add(int index, E element) {
        rangeCheckForAdd(index);//判断index是否在数组大小范围内
        modCount++;
        final int s;
        Object[] elementData;
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow();
/*
arraycopy的参数为
	 @param      src      the source array. 原数组
     @param      srcPos   starting position in the source array. 原数组下标
     @param      dest     the destination array.  目标数组
     @param      destPos  starting position in the destination data. 目标数组下标
     @param      length   the number of array elements to be copied.
     移动的元素个数
*/           
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);//把elementData中index开始到结尾的元素全部后移一位
        elementData[index] = element;
        size = s + 1;
    }

1.2.2 grow()扩容(了解一下,扩容1.5倍)

注意扩容的时候会重新建一个数组,然后调用copyof(),时间复杂度为O(n),所以初始化的时候,尽量估计数组大小,减少扩容的次数

 private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth,正常情况也等于旧容量的一半。 */
                    oldCapacity >> 1           /*旧容量的一半*/);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else {
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }
    public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
        // assert oldLength >= 0
        // assert minGrowth > 0

		//这里其实就是扩容1.5倍
        int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
        if (newLength - MAX_ARRAY_LENGTH <= 0) {
            return newLength;
        }
        return hugeLength(oldLength, minGrowth);
    }

1.2.3 remove()

同样要把后面的元素全部前移,复杂度比较高。

    public E remove(int index) {
        Objects.checkIndex(index, size);
        final Object[] es = elementData;

        @SuppressWarnings("unchecked") E oldValue = (E) es[index];
        fastRemove(es, index);

        return oldValue;
    }
    private void fastRemove(Object[] es, int i) {
        modCount++;
        final int newSize;
        if ((newSize = size - 1) > i)
            System.arraycopy(es, i + 1, es, i, newSize - i);
        es[size = newSize] = null;
    }

1.2.4 get()

支持下标查找,时间复杂度为O(1)

    public E get(int index) {
        Objects.checkIndex(index, size);
        return elementData(index);
    }

1.3ArrayList和Vector的区别

  • Vector使用Synchronize同步,是线程安全的,ArrayList线程不安全。
  • Vector扩容时扩容两倍,ArrayList扩容1.5倍

1.4如何实现安全的ArrayList

  1. 使用 Collections.synchronizedList();
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
  1. 使用concurrent 并发包下的 CopyOnWriteArrayList 类
List<String> list = new CopyOnWriteArrayList<>();

读写分离
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后需要把原始数组指向新的复制数组。

    public boolean add(E e) {
        synchronized (lock) {
            Object[] es = getArray();
            int len = es.length;
            es = Arrays.copyOf(es, len + 1);
            es[len] = e;
            setArray(es);
            return true;
        }
    }

    public E get(int index) {
        return elementAt(getArray(), index);
    }

适用场景
CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。但是 CopyOnWriteArrayList 有其缺陷:

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
    所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

二.LinkedList

2.1核心成员变量

LinkList的底层是一个双向链表,记录了链表的头节点,尾节点和元素个数

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

    transient int size = 0;
    /**
     * Pointer to first node.
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     */
    transient Node<E> last;

2.2 核心方法

2.2.1 add()

比较简单,无非就是链到尾端或者前面,增加modCount,增加size

    public boolean add(E e) {
        linkLast(e);//链接到尾端
        return true;
    }
    
    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++;
    }
   public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
    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++;
    }

2.2.2 get()

需要去遍历链表,时间复杂度为O(n)

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
 Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {//小于size的一半,从前往后找
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {//大于size的一半,从后往前找
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

三.ArrayList和LinkList的区别

1.ArrayList底层是动态数组,支持下标访问,但是删除要把index后面的元素往前移,增加可能需要扩容,扩容大小为1.5倍,而且要把旧数组复制到新的数组,时间上不友好。LinkedList的底层是一个双向链表,不支持下标访问,查找不够友好,但是删除和增加只需要改变一下链接节点即可,速度很快。
2.ArrayList和LinkList遍历的选择有所差异。ArrayList支持下标访问,所以用for循环通过下标遍历速度最快,LinkedList底层双向链表,通过迭代器iterator速度更快。

推荐阅读ArrayList和LinkedList的三种遍历方式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值