JDK 普通操作 之 List 合集

本文深入探讨了JDK中三种常见的List实现:ArrayList、LinkedList和CopyOnWriteArrayList。详细解析了它们的源码,重点讨论了添加、删除和获取元素的操作,以及扩容和线程安全等问题。ArrayList的扩容机制和LinkedList的链表结构得到了详细阐述,同时介绍了CopyOnWriteArrayList如何通过使用锁和 volatile 实现线程安全。

hello,大家好啊,不知道大家在平时的工作、学习中List用的多不多,这个我猜比Thread用的人多多了,人均会用

今天就和大家一起看一下一些List的源码

用的最最最最最多的List之ArrayList:

先看看ArrayList里面有哪些属性

/** 默认容量,当不传入初始化容量时默认为10,但是不是在构造时初始化的,是在第一次添加时扩容的 */
private static final int DEFAULT_CAPACITY = 10;

/** 空实例的空实现(用于构造函数传入初始化容量为0或传入的集合长度为0) */
private static final Object[] EMPTY_ELEMENTDATA = {};

/** 如果不传入初始化容量默认elementData为这个 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/** 用上面这两个空数组确定是有参构造还是无参构造 */

/** 数组 */
transient Object[] elementData; // non-private to simplify nested class access

/** 省略 */
private int size;

接下来看其添加操作:

整个add()方法非常简单,ensureCapacityInternal()确定内部容量(是否需要扩容),在数组的下一个位置放入该元素即可

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

这个直接跳过,我们看其内部的两个具体方法

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

计算容量:也就是看当前数组是否初始化了,没初始化就返回DEFAULT_CAPACITY

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 这里的minCapacity = 当前size + 1
    // 如果是无参构造创建的,即当前内部数组并没有初始化
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 返回DEFAULT_CAPACITY
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

确定是否要扩容:

判断添加元素(size + 1)过后 是否大于 当前 数组长度 ,大于则扩容

如果上一步是未初始化返回DEFAULT_CAPACITY,这里的当前数组长度就为0,也是需要扩容的

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

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

这是真正的扩容方法,前面都只是前戏😀

首先按照当前容量扩大1.5倍,看看够不够,不够就取当前需要的容量

当然不能太大,最多就Integer.MAX_VALUE,不能再多了

复制一个新的数组,容量为新容量

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 把当前容量扩大1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 扩大1.5倍都还不够
    if (newCapacity - minCapacity < 0)
        // 就取minCapacity
        newCapacity = minCapacity;
    // 大于上限了
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        // 要不的要不得,最高就Integer.MAX_VALUE,不能再高了
        newCapacity = hugeCapacity(minCapacity);
    // 复制一个新的数组,容量为新容量
    elementData = Arrays.copyOf(elementData, newCapacity);
}

接下来看一下另一个add()方法:在指定index添加一个元素

首先检查指定的下标是否合理(是否越界或者小于0)

然后检查是否需要扩容,和上面同理

把当前数组的元素从index开始,都给👴往后挪一位,把index这个位置腾出来给新元素

public void add(int index, E element) {
    // check数组下标是否合理,也就是是否越界或者小于0
    rangeCheckForAdd(index);

    // 是否需要扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 把这个数组的元素都往后挪一位,让出地方
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 腾出的这个位置给新元素
    elementData[index] = element;
    size++;
}

在看一看获取操作:

无聊,没意思,乱懂

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}
E elementData(int index) {
    return (E) elementData[index];
}

最后看一眼删除操作:

整个流程也比较简单

public E remove(int index) {
    // 这里和上面的check略有不同,不检查是否小于0,我也不知道为什么,反正小于0就会抛异常
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 经典help GC
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

换个姿势的remove:

简单来说就是遍历数组,待删除元素为null就删除元素为null的,不为null就删除equals的元素,注意,这里只删除第一次出现的元素

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

fastRemove():和上面remove异曲同工之妙

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
}

下面再看经常被拿出来和ArrayList比较特点的LinkedList

先看LinkedList的Node

相当清晰,经典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 */
transient int size = 0;

/** 头节点指针 */
transient Node<E> first;

/** 尾节点指针 */
transient Node<E> last;

添加操作:

摆设,默认会添加到当前链表的尾部

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

整个过程非常之简单

就是新增一个Node,将新Node prev指针指向尾节点,再把尾节点指向new Node

如果之前尾节点为null,则把头节点也指向当前节点,说明此时整个链表就只有一个节点

如果之前尾节点不为null,则将之前尾节点 的 next 指针指向new Node

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

另一种添加操作呢:

会判断index是否为当前链表长度,是的话就添加到最后

否则会添加到指定index处

public void add(int index, E element) {
    // check index是否越界
    checkPositionIndex(index);
	
    // 如果待插入元素的index为当前链表长度,就把其添加到最后
    if (index == size)
        linkLast(element);
    else
        // 否则
        linkBefore(element, node(index));
}

node()方法:返回指定index下标处的Node

首先会判断index在链表的前半部分还是后半部分,相当于二分一下

在前半部分就从前往后找,在后半部分就从后往前找

Node<E> node(int index) {
    // assert isElementIndex(index);

    // 这里做了一个优化,判断index是否为size的一半,相当于二分一下
    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;
    }
}

将一个Node添加到另一个Node前面,基本操作

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;
    // 如果之前的前驱节点为null,说明他是head节点
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

获取方法:

和上面一样,是有一个二分的优化

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

删除操作:

public E remove(int index) {
    // 判断index是否大于0小于size
    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;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

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

最后讲一下一个很高大上的List之CopyOnWriteArrayList:

先看一下其属性:

/** lock锁实现 */
final transient ReentrantLock lock = new ReentrantLock();

/** 内部元素数组 */
private transient volatile Object[] array;

这个属性array只能通过这两个方法访问和赋值,好家伙,这是连类内部都要封装

final Object[] getArray() {
    return array;
}

final void setArray(Object[] a) {
    array = a;
}

添加操作:

先加锁,然后将当前数组扩容1,赋值新元素给最后一个下标处,再把当前内部数组替换,最后解锁

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

其他添加操作和删除操作就不过多bb了,大同小异,主要依靠ReentrantLock控制当前只能有一个线程进行写操作

获取操作:

没有加锁,整个获取操作是可以多线程并行访问的
因为内部数组并无修改,只有替换,且用了volatile修饰,保证了线程可见性
即如果另一个线程替换了内部数组,其他线程会立马知晓这个线程的修改,也就是获取到的都是最新的值

public E get(int index) {
    return get(getArray(), index);
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

OK,这就是JDK比较经典的List实现类,这不在评论区总结一下?

评论 9
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值