比较全乎的ArrayList和LinkedList的区别

Array和List的区别

Array(数组)是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的。
Array获取数据的时间复杂度是O(1),但是要删除数据却是开销很大,因为这需要重排数组中的所有数据, (因为删除数据以后, 需要把后面所有的数据前移)
数组初始化必须指定初始化的长度, 否则报错。

List—是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式,它继承Collection。
List有两个重要的实现类:ArrayList和LinkedList
List是一个接口,不可以实例化, 不能写成如下:

List<Integer> list = new List<Integer>();//错误

ArrayList

ArrayList: 可以看作是能够自动增长容量的数组
ArrayList的toArray方法返回一个数组
ArrayList的asList方法返回一个列表
ArrayList底层的实现是Array, 数组扩容实现

  • 新增数据空间判断
  • 新增数据的时候需要判断当前是否有空闲空间存储
  • 扩容需要申请新的连续空间
  • 把老的数组复制过去
  • 新加的内容
  • 回收老的数组空间

LinkedList

LinkList是一个双向链表,在添加和删除元素时具有比ArrayList更好的性能.但在get与set方面弱于ArrayList。当然,这些对比都是指数据量很大或者操作很频繁。
链表不需要连续的空间, 大小不确定。

对比

操作数组链表
随机访问O(1)O(N)
头部插入O(N)O(1)
头部删除O(N)O(1)
尾部插入O(1)O(1)
尾部删除O(1)O(1)

小结:

  • 同样查找, 时间复杂度都是O(N), 但是数组要比链表快
    因为数组的连续内存, 会有一部分或者全部数据一起进入到CPU缓存, 而链表还需要在去内存中根据上下游标查找, CPU缓存比内存块太多
  • 数据大小固定, 不适合动态存储, 动态添加, 内存为一连续的地址, 可随机访问, 查询速度快
  • 链表代销可变, 扩展性强, 只能顺着指针的方向查询, 速度较慢

ArrayList的源码分析

ArrayList的主要成员变量

	private static final int DEFAULT_CAPACITY = 10;
		// ArrayList的默认长度是多少
    private static final Object[] EMPTY_ELEMENTDATA = {};
		// ArrayList的默认空元素链表
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
		// ArrayList存放的数据
    transient Object[] elementData; // non-private to simplify nested class access
		// ArrayList的长度
    private int size;

ArrayList的构造函数

// 构造一个初始化容量为10的空列表
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
// 初始化一个指定大小容量的列表
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);
        }
    }
// 构造一个包含指定集合的元素列表, 按照它们由集合迭代器返回的顺序
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;
        }
    }

扩容机制

ArrayList扩容的核心从ensureCapacityInternal方法说起。可以看到前面介绍成员变量的提到的ArrayList有两个默认的空数组:

DEFAULTCAPACITY_EMPTY_ELEMENTDATA:是用来使用默认构造方法时候返回的空数组。如果第一次添加数据的话那么数组扩容长度为DEFAULT_CAPACITY=10

EMPTY_ELEMENTDATA:出现在需要用到空数组的地方,其中一处就是使用自定义初始容量构造方法时候如果你指定初始容量为0的时候就会返回。

// 增加元素的方法
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

//判断当前数组是否是默认构造方法生成的空数组,如果是的话minCapacity=10反之则根据原来的值传入下一个方法去完成下一步的扩容判断
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
        }

//minCapacitt表示修改后的数组容量,minCapacity = size + 1
private void ensureCapacityInternal(int minCapacity) {
        //判断看看是否需要扩容
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

下面谈谈ensureExplicitCapacity方法(modCount设计到Java的快速报错机制后面会谈到),可以看到如果修改后的数组容量大于当前的数组长度那么就需要调用grow进行扩容,反之则不需要。

//判断当前ArrayList是否需要进行扩容
private void ensureExplicitCapacity(int minCapacity) {
  modCount++;

  // overflow-conscious code
  // int[] a = new int[5]; 数组创建的时候是多大, a.length就等于5
  if (minCapacity - elementData.length > 0)
    grow(minCapacity);
}

最后看下ArrayList扩容的核心方法grow(),下面将针对三种情况对该方法进行解析:

  1. 当前数组是由默认构造方法生成的空数组并且第一次添加数据。此时minCapacity等于默认的容量(10)那么根据下面逻辑可以看到最后数组的容量会从0扩容成10。而后的数组扩容才是按照当前容量的1.5倍进行扩容;

  2. 当前数组是由自定义初始容量构造方法创建并且指定初始容量为0。此时minCapacity等于1那么根据下面逻辑可以看到最后数组的容量会从0变成1。这边可以看到一个严重的问题,一旦我们执行了初始容量为0,那么根据下面的算法前四次扩容每次都 +1,在第5次添加数据进行扩容的时候才是按照当前容量的1.5倍进行扩容。

  3. 当扩容量(newCapacity)大于ArrayList数组定义的最大值后会调用hugeCapacity来进行判断。如果minCapacity已经大于Integer的最大值(溢出为负数)那么抛出OutOfMemoryError(内存溢出)否则的话根据与MAX_ARRAY_SIZE的比较情况确定是返回Integer最大值还是MAX_ARRAY_SIZE。这边也可以看到ArrayList允许的最大容量就是Integer的最大值(-2的31次方~2的31次方减1)

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

LinkedList的源码分析

Node类构成


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

看完这个构成后可以知道size大小就是一开始声明时候的0。我们先来关注下LinkedList#add方法,即末位添加,究竟干了些什么

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

可以看到,add方法只是对外暴露的方法,真正执行时候交给linkLast方法。

  1. 首先会获取这个list对象内部的Node类型成员last,即末位节点,以该节点作为新插入元素的前驱节点并创建新节点
  2. 然后把新节点作为该list对象的最后一个节点
  3. 紧接处理原先的末位节点,第一个情况分析如果这个list本来就是一个空的链表,ok,我们把新节点作为首节点。如果链表内部已经有元素,那么,现在把原来的末位节点的后继指向新节点,完成链表修改
  4. 最后修改当前list的size,并记录该list对象被执行修改的次数

末位添加方法的操作步骤就相当于,目标节点创建后寻找前驱节点, 前驱节点存在就修改前驱节点的后继,指向目标节点
接着我们可以来看下指定位置插入的add方法,内容如下

public void add(int index, E element) {
        checkPositionIndex(index);
        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
 
private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
 
private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }
 
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++;
    }
  1. 检查插入位置是否合法,毕竟不能插个-1进去或者插一个不存在的位置吧
  2. 如果是插入位置是末位,那还是上面我们末位添加的逻辑;反之,说明当前list内指定位置节点存在开始寻找
  3. 这里查找我们可以看到,for,马上想到遍历这个list的节点元素,并访问节点的后继以找到下一个节点,这就是链表,而双向链接则是当前节点知道自己的后继是后面的节点后,只需要保证后面节点知道其前驱是自己,就可以实现互通。但这里要注意,LinkedList插入时候寻找的点并不一定是从链表表头开始,而是根据目标位置和list中间下标的大小来确定寻找方向是要往更高位置寻找,还是更低位置寻找,这种方式我们称之为二分查找
  4. 找到目标节点后,还是创建新节点,告诉新节点其前驱为原位置节点的前驱pred,其后继来为原位置节点succ。
  5. 那在这个时候就会发现,除了原位置节点succ,还有新节点newNode的前驱都指向了原位置前驱节点pred,也就是说,newNode知道自己的前后,而succ也节点知道的前后,最麻烦的是他们都说自己的前面是同一个节点,而且pred现在只知道自己的后继是succ,不认newNode
  6. 接下来要做的,就是告诉原位置节点,你现在的前面是新节点newNode,这样做,只是让原节点可以通过前驱来找到newNode,还需要告诉pred节点,其后继现在是newNode。从而实现双向链表的互通,至此,插入操作完成
  7. 最后还是修改当前list的size,并记录该list对象被执行修改的次数。

以上就是指定位置添加的逻辑,先操作新节点,紧接修改原有节点的前驱属性,最后再修改前驱节点的后继属性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值