Java:ArrayList与LinkedList区别与比较

7 篇文章 0 订阅
4 篇文章 0 订阅


前言

ArrayList与LinkedList的区别常常被人提及,清楚了解二者的区别有利于夯实自己的开发基本功,在合适的场合选择合适的数据结构,能够帮助自己写出更加优质的代码,本文结合二者的源码,对它们进行组成分析和区别分析。

一、基本结构

ArrayList

ArrayList基于动态数据(顺序表)进行实现,且默认存储的容量为10
在这里插入图片描述

LinkedList

LinkedList基于双向链表的数据结构进行实现,具有头尾指针,单个结点具有prev指针和next指针
在这里插入图片描述

在这里插入图片描述

二、增删改查(CRUD)

1.ArrayList

/**
     * This helper method split out from add(E) to keep method
     * bytecode size under 35 (the -XX:MaxInlineSize default value),
     * which helps when add(E) is called in a C1-compiled loop.
     */
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }

/**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     * @throws OutOfMemoryError if minCapacity is less than zero
     */
    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           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else {
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }

	private Object[] grow() {
        return grow(size + 1);
    }

众所周知,一个数组如果往里面添加值超过了边界,那么便要对这个数组进行扩容;那么我们假设有一个ArrayList,现在里面的元素已经满了,我们再试图往里面添加元素,这时候便要调用add()函数了,那么依照我上文添加的源码,add函数便会去调用grow()函数,那么我们便对grow函数进行一番分析。

那么我们看到,当调用add函数的时候,可能存在两种情况:
1.原有列表不存在元素(未初始化),直接生成一个新的顺序表,大小取默认大小和添加元素大小的最大值
2.原有列表存在元素,这时候需要完成两项工作

  • 对原有的动态数组进行扩容
  • 将原有的动态数组的元素添加至新的动态数组

第二步无非就是O(n)级的复制,这里我们重点关注扩容那一步发生了什么,也就是调用ArraysSupport.newLength()那一步发生了什么,来看看源码;

public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
        // assert oldLength >= 0
        // assert minGrowth > 0

        int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
        if (newLength - MAX_ARRAY_LENGTH <= 0) {
            return newLength;
        }
        return hugeLength(oldLength, minGrowth);
    }

    private static int hugeLength(int oldLength, int minGrowth) {
        int minLength = oldLength + minGrowth;
        if (minLength < 0) { // overflow
            throw new OutOfMemoryError("Required array length too large");
        }
        if (minLength <= MAX_ARRAY_LENGTH) {
            return MAX_ARRAY_LENGTH;
        }
        return Integer.MAX_VALUE;
    }

在这里插入图片描述
看到这里我们豁然开朗,也感慨Java语言设计者的良苦用心,联系上文grow()函数中传入的两个参数

  • minCapacity - oldCapacity
  • oldCapacity >> 1//右移得到原来长度的一半长度
    我们可以看到,这里生成新长度也分为两种情况
    先取新增长度为添加入元素之后的值和原有动态数组的长度一半的最大值
  • 若新增长度小于可分配的长度的值,则直接扩容新增长度大小
  • 若新增长度大于可分配的长度的值,则取最小分配长度minGrowth,比方说,若此时往数组里只增加1,但是按照第一种分配方式,将原动态数组扩容一半超出内存分配空间,则选择只minLength为1,结果就是将动态数组扩容至最大长度

那么什么情况是动态数组长度的最大值呢,我们这里可以看到是Integer.MAX_VALUE,也就是Integer长度的上界,至于原因是为什么呢,笔者的理解是动态数组是采用Integer作为下表采集的,若超出,则会发生不可预知的错误。
如果要改大,可以将Integer改为其他数据类型,重新生成自己的数据结构,当然这涉及到很多应用上的问题,这里笔者就不赘述了。

/**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     *
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    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 remove method that skips bounds checking and does not
     * return the value removed.
     */
    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;
    }

这里采用的是覆盖删除的方式,即把要删除的i结点右面的元素利用Copy的方式统一完成移动,时间复杂度为O(n);

查是比较简单的部分了,因为 ArrayList是基于索引的数据接口,它的底层是类似顺序表的动态数组。它可以以O(1)时间复杂度对元素进行随机访问。

其实改和查一样,基于顺序表的结构这俩个操作都十分简单,优化也没有大的优化方向了,直接看源码吧

public E set(int index, E element) {
            Objects.checkIndex(index, size);
            checkForComodification();
            E oldValue = root.elementData(offset + index);
            root.elementData[offset + index] = element;
            return oldValue;
        }

我们看到这里先对输入数据进行了合法性检验,而后利用checkForComodification()保证修改和查询两个线程的一致性,最后进行修改。

2.LinkedList

/**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#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++;
    }

这里插入因为具有head、tail指针的缘故,所以插入始终保持o(1)的复杂度;

public boolean remove(Object o) {
        if (o == null) {
            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;
    }
/**
     * 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;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

删除的源码因为要用到equals方法,所以对空指针进行了校验,同时保证是否存在空指针的元素,若存在,则对其进行删除;删除则是采用双端链表最基础的删除方式

public void set(E e) {
            if (lastReturned == null)
                throw new IllegalStateException();
            checkForComodification();
            lastReturned.item = e;
        }

因为其实LinkedList没有下标这个概念,所以其实set方法用的也不多,这里用到的lastReturned,是实现迭代器的一部分,可以理解为上一次访问到的位置,这里主要应用于提高访问效率

/**
     * Returns {@code true} if this list contains the specified element.
     * More formally, returns {@code true} if and only if this list contains
     * at least one element {@code e} such that
     * {@code Objects.equals(o, e)}.
     *
     * @param o element whose presence in this list is to be tested
     * @return {@code true} if this list contains the specified element
     */
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
/**
     * Returns the index of the first occurrence of the specified element
     * in this list, or -1 if this list does not contain the element.
     * More formally, returns the lowest index {@code i} such that
     * {@code Objects.equals(o, get(i))},
     * or -1 if there is no such index.
     *
     * @param o element to search for
     * @return the index of the first occurrence of the specified element in
     *         this list, or -1 if this list does not contain the element
     */
    public int indexOf(Object o) {
        int index = 0;
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null)
                    return index;
                index++;
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item))
                    return index;
                index++;
            }
        }
        return -1;
    }

查找便是从头节点一直查找到尾节点,引入空指针校验,时间复杂度为O(n)

三、基于底层数据结构的总结

综上,我们可以做出如下总结:

  • 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
  • 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
  • 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

四、ArrayList与Vector的比较

线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。补充:LinkedList同样是非线程安全,只可以在单线程环境下使用。
性能:ArrayList 在性能方面要优于 Vector。
扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。

而ArrayList的扩容机制我已经在上文的add()方法中书写得比较完整了,如果还有不清楚的uu可以去上文查看或留言询问。

五、ArrayList与Array的区别

Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值