List集合的使用和源码

目录

1、List框架图

2、List的基本方法

3、ArrayList(最常用的)

4、Vector(线程安全,性能差)

5、LinkedList(链表)

6、CopyOnWriteArrayList(线程安全,性能好)

总结比较


1、List框架图

2、List的基本方法

以下为List接口的全部方法。在后面的具体实现类中会讲解具体的逻辑

java 复制代码
public interface List<E> extends Collection<E> {
    int size():获取集合大小
    boolean isEmpty():判断是否为空
    boolean contains(Object o):是否包含某个元素
    Iterator<E> iterator():返回迭代器
    Object[] toArray():集合转数组
    boolean add(E e):添加元素
    boolean remove(Object o):删除元素
    boolean addAll(Collection c):批量添加
    void clear():清空集合
    E get(int index):根据下标获取元素
    E set(int index, E element):根据下标修改元素
    void add(int index, E element):指定位置插入
    E remove(int index):根据下标删除
    int indexOf(Object o):查找元素下标
    List<E> subList(int from, int to):截取子集合
}

3、ArrayList(最常用的)

数据结构:数组

是否线程安全:否

成员变量:

java 复制代码
    // 默认初始容量,当我们在创建ArrayList时,未指定容量大小则初始容量为10
    private static final int DEFAULT_CAPACITY = 10;

    // 用于指定容量为 0 时的空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 用于默认大小的空数组实例(无参构造时使用)
    // 将其与 EMPTY_ELEMENTDATA 区分开,是为了知道添加第一个元素时扩容到多少。
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // ArrayList 的底层存储数组,这里可以看到ArrayList的底层数据结构是基于数组实现的
    transient Object[] elementData; // non-private to simplify nested class access

    // 当前数组的实际元素个数
    private int size;

EMPTY_ELEMENTDATA 和DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的区别可以看后续的add方法,这里先简单说下区别:

  • EMPTY_ELEMENTDATA:new ArrayList(0) 使用,第一次扩容到 1

  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:new ArrayList() 使用,第一次扩容到 10

目的:让 JDK 区分"用户手动设为0"和"默认构造",执行不同扩容策略。

常见问题点:

1、根据add方法可以看到如果不指定初始容量,大量数据会频繁扩容,性能较差,如果我们知道长度预先指定长度可以减少扩容次数。

2、线程不安全,多线程下会出现数据丢失、越界

3、适用场景:查询快,但是增删慢。删除元素如果元素在数组中间,需要将后面所有元素前移。如果增加元素可能出发扩容。

常用方法源码解析:

public ArrayList()

ArrayList的无参构造

在我们使用ArrayList的无参构造方法时,可以看到将当前元素指定为DEFAULTCAPACITY_EMPTY_ELEMENTDATA对象,后续扩容时扩容为默认大小10

public ArrayList(int initialCapacity)

ArrayList有参构造,入参为该数组大小

java 复制代码
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            // 如果入参值大于0,设置当前集合大小为入参值
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // 如果入参值等于0,标记为EMPTY_ELEMENTDATA,后续扩容为1
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            // 小于0报错
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

public ArrayList(Collection<? extends E> c)

ArrayList有参构造,入参为顶级Collection接口

java 复制代码
public ArrayList(Collection<? extends E> c) {
        // 将入参赋值给elementData 
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // 如果入参集合长度不等于0,就使用copyOf方法复制为一个新数组赋值到elementData 
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 入参集合长度等于0,标记为空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

public boolean add(E e)

常用的add方法,向集合中添加一个元素

java 复制代码
public boolean add(E e) {
        // 这个方法是集合进行动态扩容的关键
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

调用该方法进行判断
private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

获取此时调用add方法时需要的容量大小
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 如果是等于无参构造中的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这里也就是体现到了DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA的区别。
        // 如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,说明是第一次向集合中添加元素
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 取默认值和当前数组所需容量大小的最大值返回。这里就可以看到使用了默认长度10
            // 可以看到集合不是在new的时候创建容量的,而是在第一次add元素时创建容量的
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

private void ensureExplicitCapacity(int minCapacity) {
        //快速失败机制(fail-fast) 和迭代器一致性检测
        modCount++;

        // 这里判断是否需要扩容,如果返回的所需数组大小,大于当前数组的长度,说明容量不够用了,            就需要出发扩容方法来增加容量。
        // 因此从这里可以看出来,如果我们可以指定集合存放数据的大小,就可以使用初始化时设置集合长度避免扩容。
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

实际扩容方法

oldCapacity + (oldCapacity >> 1); 从这句可以看出每次扩容是之前的1.5倍

java 复制代码
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);
    }

由此我们可以看到数组的扩容是由数组的拷贝实现的

public boolean remove(Object o)

通过这个源码可以发现当我们移除元素时,需要进行for循环一个一个进行比较,调用equals比较,移除掉后需要将整个数组前移,所以他的时间复杂度是O(n)

java 复制代码
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;
    }

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
    }

public boolean contains(Object o)

这里看到contains方法也是通过for循环遍历对比的,性能也较差,时间复杂度O(n)

java 复制代码
public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

4、Vector(线程安全,性能差)

数据结构:数组

是否线程安全:是

常用方法源码解析:

我们可以看到他的线程安全是通过synchronized实现的,所以性能较差,现在已经很少用了

java 复制代码
 public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

public synchronized E remove(int index) {
        modCount++;
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
        E oldValue = elementData(index);

        int numMoved = elementCount - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--elementCount] = null; // Let gc do its work

        return oldValue;
    }

5、LinkedList(链表)

数据结构:链表

是否线程安全:否

成员变量:

java 复制代码
    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

常见问题点

1、占用内存比ArrayList多,因为每个元素都是一个Node节点,需要存储前后节点。

2、线程不安全。

3、增删快,查询慢。增删只需要修改前后节点指针即可,查询需要遍历。

构造方法

java 复制代码
public LinkedList() {
    }

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param  c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

Node对象

java 复制代码
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;
        }
    }

常用方法源码解析:

public boolean add(E e)

将当前要添加的元素挂在链表最后一个节点

java 复制代码
public boolean add(E e) {
        linkLast(e);
        return true;
    }

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

public E get(int index)

可以看到是通过遍历获取的,性能较差

java 复制代码
public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

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

6、CopyOnWriteArrayList(线程安全,性能好)

数据结构:数组

是否线程安全:是

核心原理:读不加锁、修改时加锁复制

常见问题点

1、读:无锁,直接读当前数组

2、写:加锁 → 复制新数组 → 写完替换引用。所以性能比不加锁差。

3、数据有最终一致性,不能实时一致。因为读不加锁,有可能写正在修改时候被读导致数据不一致。

4、内存占用高(同一时间存在新旧两个数组)

代码实现

java 复制代码
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();
        }
    }

总结比较

1、日常开发优先用 ArrayList

2、频繁头尾增删用 LinkedList

3、高并发读多写少用 CopyOnWriteArrayList

4、Vector 基本废弃

|------|-----------|------------|-----------------|----------------------|
| | ArrayList | LinkedList | Vector | CopyOnWriteArrayList |
| 数据结构 | 动态数组 | 双向链表 | 动态数组 | 写时复制数组 |
| 查询速度 | O1 | On | O1 | O1 |
| 增删速度 | On | O1 | On | On(加锁、慢) |
| 内存占用 | 小 | 大 | 小 | 高 |
| 线程安全 | 否 | 否 | 是(synchronized) | 是(ReentrantLock) |
| 扩容机制 | 1.5倍 | 无 | 2倍 | 每次复制新数组 |
| 适用场景 | 大量查询、少增删 | 频繁增删、少查询 | 基本废弃 | 读多写少 |

相关推荐
蓝天居士2 小时前
认识libcurl(2)
linux·libcurl
武藤一雄2 小时前
WPF深度解析Behavior
windows·c#·.net·wpf·.netcore
Jonathan Star2 小时前
在 Claude Code 中重新加载插件,最常用的是 **`/reload-plugins` 热重载**,也
linux·运维·服务器
SEO-狼术2 小时前
Secure PDF Delphi Edition
服务器·windows·pdf
Dazer0072 小时前
Windows 11 关闭微软输入法 Ctrl+Shift+F 简繁切换快捷键
windows·microsoft
日更嵌入式的打工仔2 小时前
Windows 下 GitLab 完整使用指南
windows·gitlab
A.A呐2 小时前
【Linux第二十一章】http
linux·运维·http
王琦03183 小时前
第七章 命令解释器-shell
linux·运维·服务器
啥咕啦呛3 小时前
java打卡学习6:集合框架 Collection
java·windows·学习