美文网首页
Java常用集合类ArrayList/LinkedList

Java常用集合类ArrayList/LinkedList

作者: 嘎嘣脆糖 | 来源:发表于2020-02-09 17:32 被阅读0次

    ArrayList

    ArrayList是一种变长集合,基于定长数组实现,允许空值和重复元素,是线程不安全的集合。

    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    
        /**
         * Default initial capacity.
         */
        private static final int DEFAULT_CAPACITY = 10;
    

    构造函数

        /**
         * Constructs an empty list with the specified initial capacity.
         *
         * @param  initialCapacity  the initial capacity of the list
         * @throws IllegalArgumentException if the specified initial capacity
         *         is negative
         */
        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);
            }
        }
    
        /**
         * Constructs an empty list with an initial capacity of ten.
         */
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
    
        /**
         * Constructs a list containing the elements of the specified
         * collection, in the order they are returned by the collection's
         * iterator.
         *
         * @param c the collection whose elements are to be placed into this list
         * @throws NullPointerException if the specified collection is null
         */
        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;
            }
        }
    

    查找 get()

        public E get(int index) {
            if (index >= size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    
            return (E) elementData[index];
        }
    

    ArrayList的查找比较简单,因为是数组,所以时间复杂度是O(1)的。

    插入 add()

    插入提供了两个API分别是插入元素序列的队尾,另一个是插入指定位置

    /** 在元素序列尾部插入 */
    public boolean add(E e) {
        // 1. 检测是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 2. 将新元素插入序列尾部
        elementData[size++] = e;
        return true;
    }
    
    /** 在元素序列 index 位置处插入 */
    public void add(int index, E element) {
        rangeCheckForAdd(index);
    
        // 1. 检测是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 2. 将 index 及其之后的所有元素都向后移一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 3. 将新元素插入至 index 处
        elementData[index] = element;
        size++;
    }
    
    

    在插入队尾的时候:

    • 是否需要扩容
    • 插入队尾

    如果是插入指定位置,则

    • 判断容量是否足够
    • 把需要插入位置后面的元素向后移动一位
    • 将新元素插入

    判断扩容以及扩容逻辑

    /** 计算最小容量 */
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
    /** 扩容的入口方法 */
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    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;
        // newCapacity = oldCapacity + oldCapacity / 2 = oldCapacity * 1.5
        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);
    }
    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        // 如果最小容量超过 MAX_ARRAY_SIZE,则将数组容量扩容至 Integer.MAX_VALUE
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    

    可以看出,当需要扩容的时候则按照原先数组的1.5倍进行扩容( oldCapacity + (oldCapacity >> 1))

    删除元素

    /** 删除指定位置的元素 */
    public E remove(int index) {
        rangeCheck(index);
    
        modCount++;
        // 返回被删除的元素值
        E oldValue = elementData(index);
    
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 将 index + 1 及之后的元素向前移动一位,覆盖被删除值
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 将最后一个元素置空,并将 size 值减1                
        elementData[--size] = null; // clear to let GC do its work
    
        return oldValue;
    }
    
    E elementData(int index) {
        return (E) elementData[index];
    }
    
    /** 删除指定元素,若元素重复,则只删除下标最小的元素 */
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            // 遍历数组,查找要删除元素的位置
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    
    /** 快速删除,不做边界检查,也不返回删除的元素值 */
    private void fastRemove(int index) {
        modCount++;
        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
    }
    
    • 获取指定位置 index 处的元素值
    • 将 index + 1 及之后的元素向前移动一位
    • 将最后一个元素置空,并将 size 值减 1
    • 返回被删除值,完成删除操作

    当添加大量元素后,紧接着删除了大量元素后,Arraylist并没有对于多余的数组长度进行缩减,但是对外暴露了api进行释放多余空间,提高空间的利用率

    /** 将数组容量缩小至元素数量 */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }
    

    最后,在遍历过程中删除添加元素,有可能导致意料之外的结果,所以应该使用迭代器方法进行删除添加,这点有很多方法和介绍可以在网络上找到答案

    LinkedList

    LinkedList相对于ArrayList继承关系就复杂的多


    LinkedList

    LinkedList继承自AbstractSequentialList,AbstractSequentialList提供了一套用于顺序访问的接口,通过继承此类,仅需要实现部分代码就可以拥有完整的一套访问某种序列表(链表)的接口

    public E get(int index) {
        try {
            return listIterator(index).next();
        } catch (NoSuchElementException exc) {
            throw new IndexOutOfBoundsException("Index: "+index);
        }
    }
    
    public void add(int index, E element) {
        try {
            listIterator(index).add(element);
        } catch (NoSuchElementException exc) {
            throw new IndexOutOfBoundsException("Index: "+index);
        }
    }
    
    // 留给子类实现
    public abstract ListIterator<E> listIterator(int index);
    

    所以只要继承类实现了 listIterator 方法,它不需要再额外实现什么即可使用。对于随机访问集合类一般建议继承 AbstractList 而不是 AbstractSequentialList。LinkedList 和其父类一样,也是基于顺序访问。所以 LinkedList 继承了 AbstractSequentialList,但 LinkedList 并没有直接使用父类的方法,而是重新实现了一套的方法。

    另外,LinkedList 还实现了 Deque (double ended queue),Deque 又继承自 Queue 接口。这样 LinkedList 就具备了队列的功能。比如,我们可以这样使用:

    Queue<T> queue = new LinkedList<>();
    

    除此之外我们还可以实现一些其他的数据结构,比如栈

    查找 get()

    LinkedList基于链表,所以查找的时间复杂度为O(N).

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    
    Node<E> node(int index) {
        /*
         * 则从头节点开始查找,否则从尾节点查找
         * 查找位置 index 如果小于节点数量的一半,
         */    
        if (index < (size >> 1)) {
            Node<E> x = first;
            // 循环向后查找,直至 i == index
            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处于的位置,如果处于前半部分则查找前半部分,如果是处于后半部分,则从后半部分开始查找。由于随机访问的效率很低,应该避免使用LinkedLsit来进行大数据量下的随机访问。

    插入

    /** 在链表尾部插入元素 */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
    /** 在链表指定位置插入元素 */
    public void add(int index, E element) {
        checkPositionIndex(index);
    
        // 判断 index 是不是链表尾部位置,如果是,直接将元素节点插入链表尾部即可
        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
    
    /** 将元素节点插入到链表尾部 */
    void linkLast(E e) {
        final Node<E> l = last;
        // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
        final Node<E> newNode = new Node<>(l, e, null);
        // 将 last 引用指向新节点
        last = newNode;
        // 判断尾节点是否为空,为空表示当前链表还没有节点
        if (l == null)
            first = newNode;
        else
            l.next = newNode;    // 让原尾节点后继引用 next 指向新的尾节点
        size++;
        modCount++;
    }
    
    /** 将元素节点插入到 succ 之前的位置 */
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        // 1. 初始化节点,并指明前驱和后继节点
        final Node<E> newNode = new Node<>(pred, e, succ);
        // 2. 将 succ 节点前驱引用 prev 指向新节点
        succ.prev = newNode;
        // 判断尾节点是否为空,为空表示当前链表还没有节点    
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;   // 3. succ 节点前驱的后继引用指向新节点
        size++;
        modCount++;
    }
    

    LinkedList的插入操作实际就是链表的插入操作,我们知道往链表中插入一个数据的话,需要断开链然后使前一个节点连接到新节点,然后用新节点连接后一个节点,这样就成功插入,删除的原理类似。同样将节点处的连接断开再连接前后节点就实现了删除操作

    相关文章

      网友评论

          本文标题:Java常用集合类ArrayList/LinkedList

          本文链接:https://www.haomeiwen.com/subject/imbgxhtx.html