List 源码解析

457 阅读5分钟

List 源码解析

ArrayList

是基于数组实现的。 区别:在ArrayList管理下插入数据时按需动态扩容、数据拷贝等操作。

初始化

  1. 空构造函数初始化; new ArrayList(); 默认是一个空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  1. 指定长度初始化,在已知元素个数的情况下,指定size,可以提供性能,减少ArrayList中的拷贝操作,直接初始化一个预先设定好的长度。
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);
    }
}
  1. Collection参构造函数初始化
public ArrayList(Collection<? extends E> c) {
    // 1. 先转为数组
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            // 2. 在拷贝到新数组
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // replace with empty array.
        elementData = EMPTY_ELEMENTDATA;
    }
}
  1. Collections.ncopies Collections.nCopies 是集合方法中用于生成多少份某个指定元素的方法,接下 来就用它来初始化 ArrayList,如下: // 核心使用了这个:Arrays.fill(a, 0, n, element); 创建了数组长度为n,默认元素为element ArrayList list = new ArrayList(Collections.nCopies(10, 0));
public class Collections {
    private static class CopiesList<E> extends AbstractList<E>  implements RandomAccess, Serializable {
        final int n;
        final E element;
        
        CopiesList(int n, E e) {
            assert n >= 0;
            this.n = n;
            element = e;
        }

        public Object[] toArray() {
            final Object[] a = new Object[n];
            if (element != null)
                Arrays.fill(a, 0, n, element);
            return a;
        }
    }
}

炫技:

// 普通方式
ArrayList list = new ArrayList();
list.add("11");
list.add("22");

// 匿名内部类方式
ArrayList list = new ArrayList(){{add("11");add("22");}};

// 用是Arrays.asList
ArrayList<String> list = new ArrayList<String>(Arrays.asList("aaa", "bbb", "ccc"));

/**
* 1.8 中list1.toArray()
* 使用的是:Arrays.copyOf(a, a.length, Class<?extends T[]>);导致类型比较不一致
* 1.9之后修复了这个bug,使用的是:Arrays.copyOf(a, a.length, Object[].class);
* Arrays.asList()得到的ArrayList是Arrays下的内部类,和 java.util下的ArrayList不是* 同一个,Arrays中的ArrayLis不可进行不能赋值给 ArrayList,新增元素,删除元素操作。
*/
List<Integer> list1 = Arrays.asList(1, 2, 3);
System.out.println("通过数组转换: " + (list1.toArray().getClass() == Object[].class));
ArrayList<Integer> list2 = new ArrayList<Integer>(Arrays.asList(1, 2, 3));
System.out.println("通过集合转换:  " + (list2.toArray().getClass() == Object[].class));

插入元素

  1. 判断长度充足;ensureCapacityInternal(size + 1);
  2. 当判断长度不足时,则通过扩大函数,进行扩容;grow(int minCapacity)
  3. 扩容的长度计算;int newCapacity = oldCapacity +(oldCapacity >> 1);,旧容量 + 旧容量右移 1 位,这相当于扩容了原来容量的(int)3/2。 4. 10,扩容时:1010 + 1010 >> 1= 1010 + 0101 = 10 + 5 =15 2. 7,扩容时:0111 + 0111 >> 1 = 0111 + 0011 = 7 + 3 = 10
  4. 当扩容完以后,就需要进行把数组中的数据拷贝到新数组中,这个过程会用到 Arrays.copyOf(elementData, newCapacity);,但他的底层用到的 是;System.arraycopy

>>1 右移一位相当于对原来的数据进行除于2的操作 宽容到了:x + x/2 = 3x/2 所以是扩容到了原来的3/2

return elementData = Arrays.copyOf(elementData,
                                   newCapacity(minCapacity));

指定位置插入

list.add(2, "1");

public void add(int index, E element) {
    rangeCheckForAdd(index);
    modCount++;
    final int s;
    Object[] elementData;
    if ((s = size) == (elementData = this.elementData).length)
        elementData = grow();
    // 核心方法
    System.arraycopy(elementData, index,
                     elementData, index + 1,
                     s - index);
    elementData[index] = element;
    size = s + 1;
}

private void rangeCheckForAdd(int index) {
    // 下标是否大于当前容器size,如果大于抛异常
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

删除

public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    found: {
        if (o == null) {
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        return false;
    }
    fastRemove(es, i);
    return true;
}

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

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

总结

ArrayList是基于数组实现的,在遍历和查询的效率高,但是在插入和删除操作时,涉及到元素的迁移,性能就不是那么高了。

LinkedList

底层是基于链表实现的,由双向链条next、prev,把数据节点穿插起来。

初始化

// 常规操作
LinkedList<Integer> list1 = new LinkedList<>();
list1.add(1);
list1.add(2);
list1.add(3);
System.out.println(list1);

// 使用Arrays.asList
LinkedList<Integer> list2 = new LinkedList<>(Arrays.asList(1,2,3));
System.out.println(list2);

// 使用匿名内部类
LinkedList<Integer> list3 = new LinkedList<>(){{add(1);add(2);add(3);}};
System.out.println(list3);

// 使用Collections.nCopies初始化
LinkedList<Integer> list4 = new LinkedList<>(Collections.nCopies(10,0));
System.out.println(list4);

插入

头插

public void addFirst(E e) {
    linkFirst(e);
}

private void linkFirst(E e) {
    // first,首节点会一直被记录,这样就非常方便头插。
    final Node<E> f = first;
    // 插入时候会创建新的节点元素,紧接着把新的头元素赋值给 first
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    // 之后判断 f 节点是否存在,不存在则把头插节点作为最后一个节点、存在则用 f 节点的上一个链条 prev 链接
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    // 最后记录 size 大小、和元素数量 modCount。modCount 用在遍历时做校验,modCount != expectedModCount
    size++;
    modCount++;
}
// ArrayList 和 LinkedList 头插性能比较
// ArrayList 需要做大量的位移和复制操作非常耗时,而LinkedList 的优势就体现出来了,耗时只是实例化一个对象。
public static void main(String[] args) {
    int total = 10000000;
    testLinkedList(total);
    testArrayList(total);
}


private static void testArrayList(int total) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start("testArrayList");
    ArrayList<Integer> list = new ArrayList<>();
    for (int i = 0; i < total; i++) {
        list.add(0, i);
    }
    stopWatch.stop();
    System.out.println(stopWatch.getLastTaskName() + "耗时:" + stopWatch.getTotalTimeMillis());
}

private static void testLinkedList(int total) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start("testLinkedList");
    LinkedList<Integer> list = new LinkedList<>();
    for (int i = 0; i < total; i++) {
        list.addFirst(i);
    }
    stopWatch.stop();
    System.out.println(stopWatch.getLastTaskName() + "耗时:" + stopWatch.getTotalTimeMillis());
}

尾插

public void addLast(E e) {
    linkLast(e);
}
void linkLast(E e) {
    // 与头插代码相比几乎没有什么区别,只是 first 换成 last
    final Node<E> l = last;
    // 耗时点只是在创建节点上,Node<E>
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
/**
* ArrayList 和 LinkedList 尾插性能比较
* ArrayList 不需要做位移拷贝也就不那么耗时了,而 LinkedList 则需要创建大量的对象。所以这里 ArrayList 尾插的效果更好一些。
*/
public static void main(String[] args) {
    int total = 10000000;
    testLinkedList(total);
    testArrayList(total);
}


private static void testArrayList(int total) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start("testArrayList");
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < total; i++) {
        list.add( i);
    }
    stopWatch.stop();
    System.out.println(stopWatch.getLastTaskName() + "耗时:" + stopWatch.getTotalTimeMillis());
}

private static void testLinkedList(int total) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start("testLinkedList");
    LinkedList<Integer> list = new LinkedList<>();
    for (int i = 0; i < total; i++) {
        list.addLast(i);
    }
    stopWatch.stop();
    System.out.println(stopWatch.getLastTaskName() + "耗时:" + stopWatch.getTotalTimeMillis());
}

中间插

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) {
    // 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++;
}

image.png

总结: ArraysList的耗时操作在扩容和元素拷贝, LinkedList的耗时在遍历寻找元素位置上。

删除

步骤: 1.确定出要删除的元素 x,把前后的链接进行替换。 2. 如果是删除首尾元素,操作起来会更加容易,这也就是为什么说插入和删除快。但中间位置删除,需要遍历找到对应位置。

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

// 解链
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扣减
    size--;
    // modCount标识修改的次数,防止并发修改,存在并发问题会抛ConcurrentModificationException
    modCount++;
    return element;
}

if (modCount != expectedModCount)
    throw new ConcurrentModificationException();

遍历

  1. 按下标遍历,(不推荐,LinkedList遍历操作比较耗时,时间复杂度O(n))
  2. 普通for循环
  3. 增强for循环
  4. Iterator遍历,迭代器遍历
  5. forEach循环
  6. stream流

总结

  1. ArrayList 与 LinkedList 都有自己的使用场景,如果你不能很好的确定,那么就使用ArrayList。但如果你能确定你会在集合的首位有大量的插入、删除以及获取操作,那么可以使用 LinkedList,因为它都有相应的方法;addFirst、addLast、removeFirst、removeLast、getFirst、getLast,这些操作的时间复杂度都是 O(1),非常高效。
  2. LinkedList 的链表结构不一定会比 ArrayList 节省空间,首先它所占用的内存不是连续的,其次他还需要大量的实例化对象创造节点。虽然不一定节省空间,但链表结构也是非常优秀的数据结构,它能在你的程序设计中起着非常优秀的作用,例如可视化的链路追踪图,就是需要链表结构,并需要每个节点自旋一次,用于串联业务。
  3. 程序的精髓往往就是数据结构的设计,这能为你的程序开发提供出非常高的效率改变。可能目前你还不能用到,但万一有一天你需要去造🚀火箭了呢?