List集合类源码查看

一、博客背景

list接口下有两个主要常用地的实现子类ArrayList和LinkedList,今天我们就一起来查看学习下,使用的jdk版本为1.7.0_04

二、ArrayList

在查看源码之前我们思考一下下面的问题,然后带着问题去学习

  1. ArrayList底层实现原理

  2. ArrayList的默认容量大小?

  3. ArrayList的插入或删除一定慢吗?

  4. ArrayList底层就是数组,访问速度本身就比较快,为什么还要实现RandomAccess接口?

  5. ArrayList增加数据时,为何不会数组越界?

  6. ArrayList是如何扩容的?

我们先看下类声明:public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList实现了RandmoAccess接口,即提供了随机访问功能。RandomAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们可以通过元素的序号快速获取元素对象,这就是快速随机访问;实现了Cloneable接口,能被克隆;实现了Serializable接口,因此它支持序列化,能够通过序列化传输。

1.构造函数



    private transient Object[] elementData;

   
    private int size;

    //自己指定新建的ArrayList的长度
    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    //空参的构造函数
    public ArrayList() {
        this(10);
    }

   //使用其他集合来新建ArrayList
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        size = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

通过上面的构造函数我们就可以得到

Q1:ArrayList底层实现原理

Q2:ArrayList的默认容量大小?

两个问题的答案了

ArrayList是一个Object类型数组,相当于动态数组。与Java中的数组相比,它的容量能动态增长。默认容量为10

需要注意的java8对于ArrayList的空参构造函数做了改变

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

可以看到创建ArrayList对象时,仅仅给成员变量elementData赋值了一个{},记住这里:java8的构造器并没有对成员变量elementData进行开辟默认长度是10的数组

A)Java7->8的变化(总结)

 JDK7JDK8
构造器实例化即开辟数组长度为10的内存空间实例化时不开辟内存空间
add()方法直接添加第一次调用add方法时,开辟数组长度为10的内存空间

2.add和remove方法

 A)add(E e)方法  

在数组尾部添加一个元素

   //在数组尾部添加一个元素
    public boolean add(E e) {

        //判断数组容量大小,容量不够扩容
        ensureCapacityInternal(size + 1); 
       
        elementData[size++] = e;
        return true;
    }
    

    //判断数组容量大小,容量不够扩容
    private void ensureCapacityInternal(int minCapacity) {
        // 增加修改次数
        modCount++;

        // 若此时minCapacity(ArrayList中要存储的元素个数) > elementData原始的容量,则要按照minCapacity进行扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

     
     //扩容
     private void grow(int minCapacity) {
        // overflow-conscious code
        //获取数组原先容量大小
        int oldCapacity = elementData.length;

         // 计算新的容量
        // 若原数组长度为偶数,那么新数组长度就恰好是原数组长度的1.5倍
        // 若原数组长度为奇数,那么新数组长度就恰好是原数组长度的1.5倍 - 1
        int newCapacity = oldCapacity + (oldCapacity >> 1);

        //若扩容后的数组容量小于插入新数据后的数组容量大小,则数组容量大小直接使用插入后数据容量大小
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;

        //如果扩容后数组容量大小大于最大值的全局变量(为int类型最大值-8),则将新数组长度更改为最大正数:Integer.MAX_VALUE
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);

        // minCapacity is usually close to size, so this is a win:
        //按照新的容量newCapacity创建一个新数组,然后再将原数组中的内容copy到新数组中
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    

B) add(int index, E element)    

在数组elementData指定位置index处添加元素

//在指定位置添加元素
public void add(int index, E element) {
    // 判断下标index的合法性
    rangeCheckForAdd(index);
 
    // 数组容量判断,容量不够扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 数组拷贝,将index到末尾的元素拷贝到index + 1到末尾的位置,将index的位置留出来
    System.arraycopy(elementData, index, elementData, index + 1,
                        size - index);
    elementData[index] = element;
    size++;
}

// 判断下标index的合法性
private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}


C)remove(int index)

根据index下标删除元素

public E remove(int index) {
    // 下标合法性检验
    rangeCheck(index);
 
    // 修改次数加1
    modCount++;
    // 获取旧的元素值
    E oldValue = elementData(index);
 
    // 计算需要移动的元素个数
    int numMoved = size - index - 1;
    // 将元素向前移动
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    // 将最后的元素值设置为null
    elementData[--size] = null; // clear to let GC do its work
 
    return oldValue;
}

private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

D)remove(Object o)

删除某个元素

public boolean remove(Object o) {
    // 若删除的元素为null
    if (o == null) {
        for (int index = 0; index < size; index++)
            // 若数组元素为null,则调用fastRemove方法快速删除
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } 
    // 若删除的元素不为null
    else {
        for (int index = 0; index < size; index++)
            // 找到要删除的元素,调用fastRemove方法快速删除
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int index) {
    // 修改次数加1
    modCount++;
    // 计算需要移动的元素数目
    int numMoved = size - index - 1;
    // 将index之后的元素向前移动一位
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    // 将数组最后一位置为null
    elementData[--size] = null; // clear to let GC do its work
}

ArrayList删除元素时,是分为元素为null和不为null两种方式来判断的,这也说明ArrayList允许添加null元素;同时,如果这个元素在ArrayList中存在多个,则只会删除最先出现的那个。

看了上面的增加和删除的方法我们就可以得到Q3,Q5,Q6的答案了

Q3:ArrayList的插入或删除一定慢吗?

插入删除的快慢取取决于插入或删除的元素距离有多远和list的容量大小,如果不是最后一个元素,则在插入或者删除时,需要移动该位置往后的元素,

在插入时且在数组的末端,如果底层数组的容量已经小于当前list容量,则根据ArrayList的扩容机制需要增大1.5倍的容量,并初始化一个新的数组,将原有的数据复制到新的数组中去,比较耗费资源,如果不是末端,还需要移动该位置之后的元素。

Q5:ArrayList增加数据时,为何不会数组越界

当给ArrayList增加一个对象时,首先会检查该ArrayList是否有足够的容量来存储这个新对象,如果没有足够的容量时,会建一个新的更长的数组,是旧数组容量的1.5倍,旧的数组会使用Arrays.copyOf方法被复制到新的数组中去。现有的数组引用指向新的数组。

Q6:ArrayList是如何扩容的

扩容机制为判断原有数组容量大小是否能够存入新的元素,若无法存入,则进行扩容,一次扩容1.5倍

Q4:ArrayList底层就是数组,访问速度本身就比较快,为什么还要实现RandomAccess接口

RandomAccess是一个标记接口       (Marker interface), 被用于List接口的实现类, 表明这个实现类支持快速随机访问功能(如ArrayList). 当程序在遍历这中List的实现类时, 可以根据这个标识来选择更高效的遍历方式

具体详情请查看这篇博客:https://blog.csdn.net/weixin_39148512/article/details/79234817

三、fail-fast机制

在ArrayList,LinkedList,HashMap等等的内部实现增,删,改中我们总能看到modCount的身影,modCount字面意思就是修改次数,但为什么要记录modCount的修改次数呢?

这个字段的用途,在ArrayList的父类AbstractList源码中有注释,说的很清楚:

该字段表示list结构上被修改的次数。结构上的修改指的是那些改变了list的长度大小或者使得遍历过程中产生不正确的结果的其它方式。

该字段被Iterator以及ListIterator的实现类所使用,如果该值被意外更改,Iterator或者ListIterator 将抛出ConcurrentModificationException异常,

这是jdk在面对迭代遍历的时候为了避免不确定性而采取的快速失败原则。

子类对此字段的使用是可选的,如果子类希望支持快速失败,只需要覆盖该字段相关的所有方法即可。单线程调用不能添加删除terator正在遍历的对象,

否则将可能抛出ConcurrentModificationException异常,如果子类不希望支持快速失败,该字段可以直接忽略。

现在可以明白modCount是用来实现fail-fast机制的,fail-fast机制是Java集合中的一种错误机制,当多个线程对同一个集合的内容进行操作时,就会发生fail-fast事件,它是一种错误检测机制,只能被用来检测错误,因为JDK并不一定保证fail-fast机制一定会发生。fail-fast机制会尽最大努力来抛出ConcurrentModificationException异常。

迭代器在调用next()、remove()等方法时都要调用checkForComodification()方法:

int expectedModCount = modCount;

//很多方法都会调用这个方法,检查modCount是否改变,如果在操作当前方法的时候,modcount改变,则抛出异常
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

该方法主要是检测modCount是否等于expectedModCount,若不等于,则抛出ConcurrentModificationException异常。

在创建迭代器时,会将modCount的值赋给expectedModCount,所以在迭代期间,expectedModCount不会改变,在ArrayList中,无论add、remove还是clear方法,只要改变了ArrayList的元素个数,都会导致modCount改变,从而可能导致fail-fast产生。

四、LinkedList

在查看源码之前我们同样思考一下下面的问题,然后带着问题去学习

  1. LinkedListt的实现原理
  2. 为什么说相比于ArrayList来说linkedlist插入,删除快,而查找慢
  3. ArrayList和LinkedList有什么区别

Linkedlist基于链表的动态数组(双向链表): 可以被当作堆栈(后进先出)、队列(先进先出)或双端队列进行操作。

1.node类

与ArrayLuist的数组类型为Object不同,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;
        }
}

2.数据结构示意图

node结构示意

如上图,LinkedList是由很多个这样的节点构成

  • prev存储的是上一个节点的引用
  • item存储的是具体内容
  • next存储的是下一个节点的引用

linkeedlist结构示意图

3.构造方法

// 双向队列元素个数
transient int size = 0;
// 双向队列首节点
transient Node<E> first;
// 双向队列尾节点
transient Node<E> last;


// 默认构造方法
public LinkedList() { }

// 根据其他集合创建LinkedList
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
}

public boolean addAll(int index, Collection<? extends E> c) {
        //检查插入的节点位置是否合理
        checkPositionIndex(index);

        //将其他集合转为数组
        Object[] a = c.toArray();
        int numNew = a.length;
        //如果新插入的集合大小为0,直接返回false
        if (numNew == 0)
            return false;

        //定义前驱节点,后继节点
        Node<E> pred, succ;
        //判断要插入的节点位置是不是和链表的长度相等,相等则代表插入到链表末尾
        if (index == size) {
            //将后继节点置为null
            succ = null;
            //将末尾节点赋值给前驱节点
            pred = last;
        } else {
           //不是在链表的末尾处
           //将未插入前位于当前位置的节点设置为当前节点的下一个节点
            succ = node(index);
           //将后继节点的前一个节点设置为前驱节点
            pred = succ.prev;
        }
        
       
        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
             //遍历集合中的每一个元素,新建node实例
            Node<E> newNode = new Node<>(pred, e, null);
            //如果前驱节点为空,说明是新建节点是头结点
            if (pred == null)
                first = newNode;
            else
                //将新建的实例节点设置为前驱节点的下一个节点
                pred.next = newNode;
            pred = newNode;
        }
        
        
        if (succ == null) {
            //能走到这里,说明是在末尾链表末尾插入的数据,这时赋值新的尾节点
            last = pred;
        } else {
            //是在列表中部插入数据
            //更新插入节点的下一个几点为后继节点
            pred.next = succ;
            //更新后继节点的前置节点为当前插入节点
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
 }

看完上面的构造函数,我们可以得到Q1的答案,

Q1:LinkedListt的实现原理

LinkedList的底层是通过链表来实现的

4.add和remove方法

A)add(E e)

添加指定元素,调用此方法是将元素添加到链表的末尾

public boolean add(E e) {
    linkLast(e);
    return true;
}

void linkLast(E e) {
    // 获取尾节点
    final Node<E> l = last;
    // 创建新节点,新节点的前驱节点是l,后继节点是null
    final Node<E> newNode = new Node<>(l, e, null);
    // 更新尾节点
    last = newNode;
    // 若原始链表为空,则初始化首节点
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    // 更新元素个数
    size++;
    modCount++;
}

B) add(int index, E element)

将元素添加到指定位置

public void add(int index, E element) {
    //检查插入位置是否合理
    checkPositionIndex(index);
    
    if (index == size)
        //如果插入位置等于链表的大小,则在末尾插入数据
        linkLast(element);
    else
        //否则在指定之前插入数据
        linkBefore(element, node(index));
}


Node<E> node(int index) {
    // assert isElementIndex(index);
    // 若index小于size >> 1,通过从首节点向后遍历来寻找节点
    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) {
        //获取原来被插入位置节点的前一个节点
        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++;
}

C)E remove()

移除头部元素

public E remove() {
    return removeFirst();
 }

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
 }

private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        //获取头部元素的数据
        final E element = f.item;
        //获取头部元素的下一个节点
        final Node<E> next = f.next;
        //将头部元素的数据置空
        f.item = null;
        //将头部元素指向下一个节点的指针置空
        f.next = null; // help GC
        //将原先头部元素的下一个节点赋值给头部元素
        first = next;
        if (next == null)
            //如果下一个节点为空,说明链表已经空了
            last = null;
        else
            //将原先头部节点的下一个节点的上一个节点置空
            next.prev = null;
        size--;
        modCount++;
        return element;
}

D)E remove(int index)

删除指定位置的节点

public E remove(int index) {
    // 检查index下标的合法性
    checkElementIndex(index);
    // 获取元素值,并通过unlink方法删除节点
    return unlink(node(index));
}



F)boolean remove(Object o)

删除指定元素

public boolean remove(Object o) {
    // 要删除的元素为null
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } 
    // 要删除的元素不为null
    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) {
    
    // 获取要删除的元素值
    final E element = x.item;
    // 获取元素后继节点
    final Node<E> next = x.next;
    // 获取元素前驱节点
    final Node<E> prev = x.prev;
 
    // 若前驱节点为null,说明当前删除的节点为首节点,则更新首节点
    if (prev == null) {
        first = next;
    } 
    // 否则,更新前驱节点的后继节点
    else {
        //将前驱节点的下一个节点指向当前删除元素的后继节点
        prev.next = next;
        //将当前节点的前驱节点置空
        x.prev = null;
    }
 
    // 若后继节点为null,说明当前删除的节点为尾节点,则更新尾节点
    if (next == null) {
        last = prev;
    } 
    // 否则,更新后继节点的前驱节点
    else {
        //将前驱节点设置为后继节点的前驱节点
        next.prev = prev;
        //将当前删除节点的后继节点置空    
        x.next = null;
    }
 
    // 将当前节点的数据置空
    x.item = null;
    // 减少元素个数值
    size--;
    modCount++;
    return element;
}

LinkedList删除元素时,是分为元素为null和不为null两种方式来判断的,这也说明LinkedList允许添加null元素;同时,如果这个元素在LinkedList中存在多个,则只会删除最先出现的那个。

G)E get(int index) 

获取指定下表元素


public E get(int index) {
    // 检查index下标的合法性
    checkElementIndex(index);
    // 获取元素值
    return node(index).item;
}

通过上面的代码学习我们可以得到Q2的答案

Q2:为什么说相比于ArrayList来说linkedlist插入,删除快,而查找慢

相比于插入快原因是linkedlist通过add(int index, E element)向LinkedList插入元素时。先是在双向链表中找到要插入节点的位置index;找到之后,再插入一个新节点,而双向链表查找index位置的节点时,有一个加速动作若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找

而ArrayList中的add(int index, E element)方法,是通过调用系统的数组复制方法(System.arraycopy(elementData, index, elementData, index + 1, size - index))来实现了元素的移动。所以,插入的位置越靠前,需要移动的元素就会越多

删除也是同快也是同样的原因

而查找慢的原因则是;对于ArrayList,无论什么位置,都是直接通过索引定位到元素,时间复杂度O(1),而对于LinkedList查找,其核心方法就是上面所说的node()方法,所以头尾查找速度极快,越往中间靠拢效率越低

当然也不是绝对的说ArrAyList的删除和插入就比LinkedList的慢,这一切都要看集合的大小和元素所处位置来具体分析。

五、ArrayList和LinkedList有什么区别

1.ArrayList是基于动态数组实现的数据结构,LinkedList是基于双向链表实现的数据结构

2.对于随机访问操作get和set,ArrayList要优于LinkedList,因为LinkedList要移动指针找index对应的元素

3.对于添加和删除操作add和remove,LinedList比较占优势,因为除了要找index对应的元素,ArrayList还要移动数据

通常来说ArrayList更适合用于读操作多的场景,linkedList更适合用于添加或删除数据频繁的场景

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值