Java常见集合源码分析1:List

集合的分类

首先来看一下集合的整体分类。
Collection和Set中文都可以翻译成集合。但是从Java编程角度,Collection应该被翻译成容器,Set翻译成集合。
Collection和Set中文都可以翻译成集合。但是从Java编程角度,Collection应该被翻译成容器,Set翻译成集合。

ListSet
顺序性有序无序
重复性可重复不可重复
索引可通过索引操作元素,即可使用普通for循环遍历没有索引,即不可使用普通for循环遍历。

在这里插入图片描述

1.ArrayList

基本原理及优缺点

数组的长度是固定的,比如为100,当元素数量超过了100以后会扩容,把以前的数组拷贝到新的数组里。

缺点是:

  1. 数组扩容+元素拷贝的过程,会慢一些,所以不要频繁插入数据。
  2. 要是往数组的中间加一个元素,新增元素后面的全部元素需要往后面挪动一位,性能较差。

优点是:

  1. 因为基于数组来实现,可以通过内存地址来定位某个元素,所以在随机获取数组里的某个元素的时候,性能很高。

适用场景:

  1. ArrayList的元素按照插入的顺序来排列。
  2. 适用于查多插入频率低的场景。

源码分析

构造函数

默认的构造函数,会将内部的数组做成一个默认的空数组。

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

默认的初始化数组的大小是10,但是不是在初始化的时候进行赋值的,详看 List 数组扩容,是在第一次add中进行赋值的。

默认的值太小了,所以构造ArrayList一般会给一个大小,比如100个数据,避免数组太小。

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

add()方法的源码

会导致数组元素的移动。

每次往ArrayList中添加数据,都会判断当前数组的元素是满了。

如果满了,就会扩容数组,然后将老数组中的元素拷贝到新数组中。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

set()方法的源码

public E set(int index, E element) {
    // 检查是否越界
    rangeCheck(index);

    // 获取原本的值
    E oldValue = elementData(index);
    // 设置新值
    elementData[index] = element;
    // 返回原本的值
    return oldValue;
}


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


transient Object[] elementData;

@SuppressWarnings("unchecked")
E elementData(int index) {
    // 获取到i位置原本的值,李四
    return (E) elementData[index];
}

add(index, element)方法的源码

public void add(int index, E element) {
    // 判断越界
    rangeCheckForAdd(index);

    // 确保数组能添加这个元素,直接+1
    ensureCapacityInternal(size + 1);
    // 进行数据拷贝,看注释
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 上面相当于是数据向后移动一格,当前index进行赋值 
    elementData[index] = element;
    size++;
}

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

// 将elementData从第1位开始,拷贝到第二位,总共拷贝两个元素
// System.arraycopy(elementData, 1, elementData, 2, 2);

get()方法的源码

从数组中直接取出元素,是优点所在。

public E get(int index) {
    // 检查数组越界
    rangeCheck(index);

    // 直接返回
    return elementData(index);
}

remove()方法的源码

会导致数组元素的移动。

public class ArrayList<E> {
    public E remove(int index) {
        // 检查越界
        rangeCheck(index);
    
        // list长度变化的次数:增、删
        modCount++;
        E oldValue = elementData(index);
        
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 相当于把index+1位置的元素,全部往前移动一位
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 末尾那位设置为null,让垃圾回收去回收对象 
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
}

public abstract class AbstractList<E> {
    protected transient int modCount = 0;
}

数组扩容以及元素拷贝

默认情况,第一次add(),会给数组默认赋值10。

add()方法中进入。

假设一个数组的大小是10,且已经添加了10个元素,此时数组的size = 10,capacity = 10。

此时调用add()方法插入一个元素,无法插入第11个元素。

默认是扩大1.5倍。

transient Object[] elementData;

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}


private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// elementData已经填充了10个元素了,minCapacity = 11
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果是空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 跟默认的10去比
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // 表示是要扩大数组
    // elementData.length默认就是10
    if (minCapacity - elementData.length > 0)
        // 进行扩容
        grow(minCapacity);
}

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // oldCapacity >> 1 = oldCapacity / 2 = 5
    // newCapacity = 15
    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);
}

2.LinkedList

基本原理以及优缺点

LinkedList,底层是基于双向链表数据结构来实现的。
在这里插入图片描述

优点:

  1. 大量的插入时不需要像ArrayList那样去扩容,不断的把新的节点挂到链表上就可以了。所以适合频繁的插入和删除操作。
  2. LinkedList可以当做队列来用,先进先出,在list尾部怼进去一个元素,从头部拿出来一个元素。

缺点:

  1. 不适合获取元素,因为需要遍历整个链表,直到找到index = 10的这个元素。

插入元素的原理

add()

在双向链表的尾部插入一个元素。

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

/**
 * Links e as last element.
 */
void linkLast(E e) {
    // 原本的尾节点
    final Node<E> l = last;
    // 封装一个新的node 具体代表的含义如下类所示
    final Node<E> newNode = new Node<>(l, e, null);
    // 进行覆盖
    last = newNode;
    // 表示原本就是空List
    if (l == null)
        // 重新给头结点赋值
        first = newNode;
    else
        // 改变原本l的下一个指向
        l.next = newNode;
    size++;
    // list长度是有变化的
    modCount++;
}

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

add(index, element)

是在队列的中间插入一个元素。

public void add(int index, E element) {
    // 保证>=0 <=size,直接点进去看,很简单
    checkPositionIndex(index);

    // 插在队尾
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

// 获取index位置的node
Node<E> node(int index) {
    // 插入位置在list的前半部分
    if (index < (size >> 1)) {
        // 从头部开始遍历,得到index位置的节点
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } 
    // 插入的位置在队列的后半部分
    else {
        // 从尾部开始逆着遍历,得到index位置的节点
        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;
    // index 位置的 Node,前一位 Node 需要指向 值为e的node
    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++;
}

addFirst()

在队列的头部插入一个元素。

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

private void linkFirst(E e) {
    // 得到头结点
    final Node<E> f = first;
    // 新建一个头结点
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        // 原本的头结点,指向新的头结点
        f.prev = newNode;
    size++;
    modCount++;
}

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

addLast()

跟add()方法是一样的,也是在尾部插入一个元素。

public void addLast(E e) {
    linkLast(e);
}

获取元素的原理

poll(),从队列头部出队

peek(),获取队列头部的元素,但是头部的元素不出队

getFirst()

getFirst() == peek()

获取头部的元素,直接返回first指针,指向的Node里的数据。

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

transient Node<E> first;

getLast()

获取尾部的元素。

public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

get(int index)

这个方法性能差,使用node(index)这个方法(),要进行链表的遍历。

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

// 获取index位置的 node
Node<E> node(int index) {
    // 插入位置在list的前半部分
    if (index < (size >> 1)) {
        // 从头部开始遍历,得到index位置的节点
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } 
    // 插入的位置在队列的后半部分
    else {
        // 从尾部开始逆着遍历,得到index位置的节点
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

删除元素的原理

removeLast()

public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    
    // 让jvm的垃圾回收自动去收
    l.item = null;
    l.prev = null; // help GC
    // 链表的重新指向
    last = prev;
    
    // 原本只有一个元素
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}

removeFirst()

removeFirst() == poll()
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;
}

remove(int index)

public E remove(int index) {
    // 不越界
    checkElementIndex(index);
    return unlink(node(index));
}


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;
        // 要删除节点本身的前后指向,也要指向null,让垃圾回收回收掉
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        // 后节点的指向
        next.prev = prev;
        x.next = null;
    }

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

可能一篇笔记,找了很多篇文章才解决一个问题,如有引用未贴的来源,请告知补上,水平有限,如有错误欢迎评论。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值