史上最全的集合框架讲解 ----- Java 集合框架(2)---- List 相关类最全解析

6 篇文章 0 订阅
4 篇文章 0 订阅

引言

在上篇文章 Java 集合框架(1)— 概述 中我们从大体上看了一下 Java 中的集合框架,包括ListSetMap 接口的一些介绍并且解释了迭代器的用法。从这篇开始,我们将一起来看一下 Java 集合框架中一些具体的类的解析,了解它们的运行原理。先从 List 接口下的相关类开始。

在这里插入图片描述

导航

首先来看下集合框架的架构图:
在这里插入图片描述
还是先看一下上篇文章中的那张图,我们可以看到:在 Collection接口下有一个名为 AbstractCollection的抽象类,AbstractListAbstractSet都继承了这个抽象类,而List 接口下的具体实现类继承了AbstractListSet接口下的具体类继承了AbstractSet

关于这几个抽象类的讲解,请参考这篇博客:https://blog.csdn.net/Hacker_ZhiDian/article/details/80723493

看完的小伙伴们,接着我们讲解下List集合类的具体实现类:

ArrayList

这个类算的上是我们平常开发中最常用的类之一了。翻译过来意思是 数组列表 ,不过比起这个名称,我更喜欢叫它 动态数组(受 C++ STL 模板的 vector 模板类的影响)。不过不管怎么叫它,它的功能不会遍,我们经常会用它作为动态管理数组元素的集合类
我们先来看一下它的类继承图:
在这里插入图片描述
我们可以看到,ArrayList 类继承于 AbstractList 抽象类,这个抽象类我们在上篇文章中已经仔细介绍过了,它继承于 AbstractCollection 抽象类,实现了 List 接口,并且实现了一些 AbstractCollection接口没有实现的抽象方法(size()iterator()等方法)。官方文档对它的描述是:该类提供了 List 接口的骨架实现,以最大限度地减少实现由 “随机访问” 数据存储(如数组)所支持的接口所需的工作量。对于顺序访问数据(如链接列表),应该优先使用 AbstractSequentialList类 。ArrayList类本身就是表示一种线性结构的类,那么继承于 AbstractList类也是理所当然。此外,ArrayList类还实现了 SerializableRandomAccessCloneable 接口。其中 Serializable接口是用于将对象序列化以储存在文件中或者通过流的形式在网络中传输的接口,RandomAccess接口是一个没有声明任何方法的空接口,cloneable 接口是一个对象复写 Object 类中 clone() 方法必须实现的接口,它也是一个没有声明任何方法的空接口,但是它却是一个很重要的接口。我们知道 Object 类对象的 clone()方法用于生成一个和这个对象的完全相同的拷贝对象,但是调用一个对象的 clone()方法的前提是这个对象的类必须实现 Cloneable 接口,否则的话调用者就会得到一个 CloneNotSupportedException 异常,有兴趣小伙伴们去做个小实验就明白了。

关于 ArrayList提供的一些方法相信你已经不陌生了,其提供的大多数方法都是 AbstractList类中声明的,下面我们从源码的角度上来看其中的一些方法细节:

一、ArrayList构造方法及字段详解:

先从 ArrayList的构造方法开始:

/**
 * 指定数组容量的构造方法
 */
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // 将保存元素的数组指向一个默认为空的数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            // 指定的数组容量小于 0 ,抛出一个异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * Constructs an empty list with an initial capacity of ten.
     * 构造一个初始容量为 10 的空数组列表
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 构造一个包含了参数指定的集合中包含的所有元素的数组列表
     */
    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类默认的构造方法会构造出一个容量为 10 的数组吗,为什么在 ArrayList 类默认的构造函数中只看到了一句 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; 的代码,并且在后面的代码中也显示出这个 DEFAULTCAPACITY_EMPTY_ELEMENTDATA; 是一个容量为 0的空数组啊。难道官方说明出错了?

在解答这个问题之前,有个扩展知识需要讲解下:

  • 多线程场景下如何使用 ArrayList?
    ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");

for (int i = 0; i < synchronizedList.size(); i++) {
    System.out.println(synchronizedList.get(i));
}

  • 为什么 ArrayList 的 elementData 加上 transient 修饰

ArrayList 中的数组定义如下:

private transient Object[] elementData;

再看一下 ArrayList 的定义:

public class ArrayList<E> extends AbstractList<E>
     implements List<E>, RandomAccess, Cloneable, java.io.Serializable

可以看到 ArrayList 实现了Serializable接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject ()实现:
在这里插入图片描述

再接着看下defaultWriteObject()方法:
在这里插入图片描述

其每次序列化时,先调用 defaultWriteObject()方法序列化 ArrayList 中的非静态非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。

二、ArrayList添加元素及扩容机制详解:

好了,接着我们来解答下上面留个的疑问?当第一个元素通过 add 方法添加到当前 ArrayList 对象中时,如果 elementData 字段和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA相等时,elementData会被扩展至具有默认容量的数组。好了,这么说的还是有点虚,我们不妨来看看ArrayList类的add () 方法:

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

我们看到先调用了 ensureCapacityInternal(int )方法,我们继续跟进:

private void ensureCapacityInternal(int minCapacity) {
    // 当 elementData 指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的时候,
    // 将数组容量设置为 DEFAULT_CAPACITY 和参数 minCapacity 中较大的一个
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

在出现了 elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA的相等性比较之后,方法又调用了 ensureExplicitCapacity(int ) 方法,我们还是继续跟进:

private void ensureExplicitCapacity(int minCapacity) {
    // 该字段定义在 AbstractList 中,定义代码为:
    // protected transient int modCount = 0;
    // 代表了列表元素的更改次数,此时明显这个值要加 1
    modCount++;
    // overflow-conscious code
    // 如果要求的最小容量大于当前元素数组的长度,那么进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

在这里调用了 grow(int ) 方法来进行扩容,还是继续看一下 grow(int )方法吧:

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 可以看到:新的数组容量为前一次数组容量的 1.5 倍,
    // 即每次储存元素的数组容量扩大的倍数为 1.5
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果计算出扩容后的容量小于参数指定的容量,那么将容量调整为参数指定的容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果计算出扩容后的容量大于允许分配的最大容量值,那么进行溢出判断处理,
    // MAX_ARRAY_SIZE 为 AbstractList 中定义的一个字段,代码:
    // private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    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);
}

这里通过 hugeCapacity(int ) 方法来进行判断溢出,我们来看看这个方法:

private static int hugeCapacity(int minCapacity) {
    // int 类型为 32 位有符号整数,并且计算机内部通过补码来保存数字,
    // 最高位为符号位,如果为 0,代表为正数,如果为 1,代表为负数。
    // 如果 minCapacity 发生溢出,那么其最高位必定为 1 ,
    // 整个数字就是一个负数,此时抛出 OutOfMemoryError 异常。
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
        
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

关于计算机中数字的表示方法这里再解释一下:我们知道,计算机通过二进制补码来表示整数,对于有符号的整数,用其最高位的那一位来表示当前数字的正负,如果最高位为 0,那么这个数为正数,如果最高位为 1,那么这个数为负数,这里举个例子:

int a = 0b11000000000000000000000000000000; // 11 后面跟 30 个 0
System.out.println(a);
System.out.print(-(1 << 30));

此时 a 的值是多少呢?按照我们之前的理论:此时 a 的最高位为 1 ,那么就是一个负数,第二个 1 后面跟了 30 个 0,那么 a 的值应该是 -2^30 ,后面的那个输出我将1 向左移 30 位在取相反数,那么此时两个结果应该相同。事实真的如此吗,我们来看看结果:
在这里插入图片描述
我们看到确实是这样的。其实关于补码还有一点特殊的规则,比如 0 和对应数据类型的最大负值是怎么表示的,关于位运算的知识,有兴趣的小伙伴可以自己查阅一些资料或参考下这篇博客:Java基础-一文搞懂位运算

我们回到上面的 grow(int ) 方法中来,在调用了 hugeCapacity(int )方法之后,会调用 Array.copyOf 方法来进行扩容处理,我们继续跟进:

@SuppressWarnings("unchecked")
public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

直接返回了 copyOf重载方法的返回值,继续看这个方法吧:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    // 如果数组储存的元素类型为 Object 类型,那么创建一个新的扩容后的 Object 数组,
    // 否则创建一个和数组储存的元素类型相同的扩容后的数组
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    // 将原数组中的元素值拷贝到新数组中
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

在这里我们终于看到了创建新的数组的操作代码。好了,这样的话我们就把 ArrayList 的添加元素的整个流程过了一遍,主要流程也不复杂:

先判断元素数组是否需要扩容 ⇒ 确定扩容后的容量(第一次将容量调整为默认容量(10),之后 以1.5 倍数进行扩容)⇒ 判断扩容后容量是否溢出 ⇒ 进行数组扩容并复制原数组元素到新数组中

友情提示:同时我们也知道:进行扩容操作的代价是很大的,尤其是当你的 ArrayList 的元素数量很大的时候,向虚拟机申请内存空间和进行元素拷贝的开销都很大,所以我们在使用的时候如果能够预知需要使用的最大容量,我们应该调用传入固定数值参数作为数组元素最大容量的构造方法,以最大化减小系统开销。

三、ArrayList获取元素详解:

ArrayList 的 add() 方法和扩容机制我们已经看完了,下面来看看获取元素值的 get() 方法:

/**
 * Returns the element at the specified position in this list.
 *
 * @param  index index of the element to return
 * @return the element at the specified position in this list
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E get(int index) {
    // 检查参数范围是否在数组允许的下标范围之内
    rangeCheck(index);

    return elementData(index);
}

获取元素值得方法相对简单,先调用了rangeCheck(int )方法来检查参数范围,返回了 elementData(int )方法的返回值,我们来看看这两个方法:

/**
 * Checks if the given index is in range.  If not, throws an appropriate
 * runtime exception.  This method does *not* check if the index is
 * negative: It is always used immediately prior to an array access,
 * which throws an ArrayIndexOutOfBoundsException if index is negative.
 */
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

接下来是 elementData(int )方法:

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

四、ArrayList其它方法:

好了。接下来我们再来看看 ArrayList类中的一些其他的方法:

/**
 * Inserts the specified element at the specified position in this
 * list. Shifts the element currently at that position (if any) and
 * any subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    // 检测下标是否越界
    rangeCheckForAdd(index);
    
	// 判断是否需要扩容,如果需要,那么进行扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    
    // 将 elementData 数组中从下标 index 开始的 size - index 个元素
    // 复制到 elementData 数组中从 index + 1 开始的 size - index 个元素中,
    // 即为将 elementData 数组中从 index  下标开始的所有元素向后移动一个位置
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 插入新元素
    elementData[index] = element;
    size++;
}

这个方法用于在指定下标(index) 的位置插入一个新元素(element),可以看到这个方法的时间复杂度为 O(N)

/**
 * Replaces the element at the specified position in this list with
 * the specified element.
 *
 * @param index index of the element to replace
 * @param element element to be stored at the specified position
 * @return the element previously at the specified position
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E set(int index, E element) {
    // 检查下标是否越界
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

这个方法用于将指定下标(index) 的元素值设置为参数指定的新元素值(element),并返回旧元素值

/**
 * Removes the element at the specified position in this list.
 * Shifts any subsequent elements to the left (subtracts one from their
 * indices).
 *
 * @param index the index of the element to be removed
 * @return the element that was removed from the list
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E remove(int index) {
    rangeCheck(index);

    modCount++;

    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 将数组中从 index + 1 下标开始的元素向前移动一个位置
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

这个方法用于移除元素数组中指定下标(index)的元素,并且返回旧元素。方法的时间复杂度为 O(N)

五、ArrayList 的优缺点:

优点如下:

  • ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快,时间复杂度为O(1)
  • ArrayList 在顺序添加一个元素的时候非常方便。

缺点如下:

  • 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能,时间复杂度为O(N)
  • 插入元素的时候,也需要做一次元素复制操作,缺点同上,时间复杂度也为O(N)

如何实现数组和 List 之间的转换?

  • 数组转 List:使用 Arrays. asList(array) 进行转换。
  • List 转数组:使用 List 自带的 toArray() 方法。

代码示例:

// list to array
List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
list.toArray();

// array to list
String[] array = new String[]{"123","456"};
Arrays.asList(array);

六、总结:

  • 有序,不唯一,线程不安全。
  • 内部采用数组保存元素,初始默认容量为 10,之后添加元素时,如果数组容量不足,则以 1.5 倍的倍数扩容数组(变为原来的 1.5 倍),溢出时抛出 OutOfMemeryError异常。扩容操作即为新建一个更大的数组并将原数组中的元素拷贝到新数组中。在元素较多时扩容操作开销较大,如果一开始可以确定最大需要的容量,那么建议使用另一个构造方法来创建指定初始容量的 ArrayList 以提高效率。
  • 因为采用的数组储存元素,所以插入删除元素操作较慢(时间复杂度为 O(N)),但随机访问元素的时候可以通过数组的索引进行访问,所以时间复杂为O(1)

扩容机制:
在这里插入图片描述

好了,关于 ArrayList 类中的一些常用方法就介绍到这里了。到这里我们知道 ArrayList采用数组来储存元素值,虽然它的查找元素的效率挺高,(O(1) 的时间复杂度),但是它的插入元素和删除元素操作的效率并不高(O(N) 的时间复杂度),所以它不适用于需要进行频繁插入和删除元素操作的场合中,那么如果我就需要频繁进行插入和删除元素等操作怎么办呢?此时就该 LinedList类上场了,来看看这个线性结构类:

LinkedList

这个类想必大家也很熟悉了,其实现就是一个双向链表,我们来看看这个类的继承图:
在这里插入图片描述
可以看到:LinkedList 类继承了 AbstractSequentialList 抽象类,同时实现了 ListQueueDequeCloneableSerializable接口。其中,Cloneable接口和 Serializable接口我们在上面已经讲过了,前者是一个空接口,为 clone()方法服务的,后者也是一个空接口,而其是为对象序列化而服务的。我们来看一下 Queue接口,根据接口名我们大概能猜到这个接口声明了 队列 的相关方法:

public interface Queue<E> extends Collection<E> {
    /**
     * 插入一个元素到队列尾部,成功返回 true, 失败返回 false,
     * 如果队列有容量限制并且已经达到最大容量,
     * 那么抛出一个 IllegalStateException 异常
     */
    boolean add(E e);

    /**
     * 插入一个元素到队列尾部,成功返回 true, 失败(队列元素已满)返回 false,
     * 不抛出 IllegalStateException 异常
     */
    boolean offer(E e);

    /**
     * 取出队列头部元素,并且将这个元素从队列中移除,
     * 如果队列为空,那么抛出 NoSuchElementException  异常
     */
    E remove();

    /**
     * 取出队列头部元素,并且将这个元素从队列中移除,
     * 和 remove() 方法的区别是如果队列为空,那么返回 null ,而不是抛出异常
     */
    E poll();

    /**
     * 取出队列头部元素,但是不从队列中移出这个元素,返回取出的元素,
     * 如果队列为空,那么抛出一个 NoSuchElementException 异常
     */
    E element();

    /**
     * 取出队列头部元素,但是不从队列中移出这个元素,返回取出的元素,
     * 和 element() 方法的区别在于当队列为空时这个方法返回 null 而不抛出异常
     */
    E peek();
}

可以看到,这个接口确实声明了一个 队列(元素从队尾进入队列、从队头出队列,即先进先出) 中应有的相关操作方法。我们再来看看 Deque接口,这个接口声明了 双端队列 的相关操作方法:

对双端队列不熟悉的小伙伴,可以去参考我这篇博客:数据结构详解

public interface Deque extends Queue {
	/**
	 * 添加元素到双端队列头部,如果队列元素已满,那么抛出一个 IllegalStateException 异常
	 */
	void addFirst(E e);
	
	/**
	 * 添加元素到双端队列尾部,如果队列元素已满,那么抛出一个 IllegalStateException 异常
	 */
	void addLast(E e);
	
	/**
	 * 插入一个元素到双端队列头部,插入成功返回 true,否则(队列元素已满)返回 false
	 */
	boolean offerFirst(E e);
	
	/**
	 * 插入一个元素到双端队列尾部,插入成功放回 true,否则(队列元素已满)返回 false
	 */
	boolean offerLast(E e);
	
	/**
	 * 移除并返回双端队列的头部元素,如果队列已空,那么抛出一个 NoSuchElementException 异常
	 */
	E removeFirst();
	
	/**
	 * 移除并返回双端队列的尾部元素,如果队列已空,那么抛出一个 NoSuchElementException 异常
	 */
	E removeLast();
	
	/**
	 * 移除并返回双端队列头部的元素,如果队列已空,那么放回 null 而不抛出异常
	 */
	E pollFirst();
	
	/**
	 * 移除并返回双端队列尾部的元素,如果队列已空,那么返回 null 而不抛出异常
	 */
	E pollLast();
	
	/**
	 * 返回但不移除双端队列头部元素,如果队列已空,那么抛出 NoSuchElementException 异常
	 */
	E getFirst();
	
	/**
	 * 返回但不移除双端队列尾部元素,如果队列已空,那么抛出 NoSuchElementException 异常
	 */
	E getLast();
	
	/**
	 * 返回但不移除双端队列首部元素,如果队列已空,那么返回 null 而不抛出异常
	 */
	E peekFirst();
	
	/**
	 * 返回但不移除双端队列尾部元素,如果队列已空,那么返回 null 而不抛出异常
	 */
	E peekLast();
	
	// ...
	
}

Queue 中的方法在逻辑上有点类似,只不过这里是双端队列,可以对队列头部尾部进行操作。
由此我们也知道了,LinkedList 还可以充当队列 / 双端队列使用,因为其实现了 Deque接口。而 Deque接口又继承了 Queue 接口。

下面再看看 AbstractSequentialList 抽象类,LinkedList类继承了这个类,这个类继承了 AbstractList抽象类,我们看看这个类的方法:

/**
 * 获取下标 index 所指向的元素,如果下标越界,抛出 IndexOutOfBoundsException 异常
 */
public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

/**
 * 将下标为 index 的元素设置为参数 element 指定的元素,
 * 如果下标越界,抛出 IndexOutOfBoundsException 异常
 */
public E set(int index, E element) {
    try {
        ListIterator<E> e = listIterator(index);
        E oldVal = e.next();
        e.set(element);
        return oldVal;
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

/**
 * 插入一个元素到下标 index 的位置上,如果下标越界,抛出 IndexOutOfBoundsException 异常
 */
public void add(int index, E element) {
    try {
        listIterator(index).add(element);
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

/**
 * 移除下标为 index 的元素,如果下标越界,抛出 IndexOutOfBoundsException 异常
 */
public E remove(int index) {
    try {
        ListIterator<E> e = listIterator(index);
        E outCast = e.next();
        e.remove();
        return outCast;
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}


// Bulk Operations

/**
 * 将集合 c 中的所有元素按照集合 c 迭代器遍历顺序插入到当前 List 从 index 下标开始的位置,
 * 如果下标 index 越界,抛出 IndexOutOfBoundsException 异常
 */
public boolean addAll(int index, Collection<? extends E> c) {
    try {
        boolean modified = false;
        ListIterator<E> e1 = listIterator(index);
        Iterator<? extends E> e2 = c.iterator();
        while (e2.hasNext()) {
            e1.add(e2.next());
            modified = true;
        }
        return modified;
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

我们在上篇文章已经说过:AbstractList抽象类实现了List接口声明的一些接口,包括 iterator()方法用于返回一个当前List 对象的迭代器, 但是其并没有实现诸如元素访问和修改的方法(get(int index)set(int index, E element)等)。那么这个类即通过 AbstractList中实现的迭代器方法来实现这个对 List 对象中元素进行访问和修改的方法。这样的话在某个方面来说也是减轻了子类的负担子类可以有选择性的复写父类的方法)。

好了,说了这么多,我们来看看LinkedList 类,先从其储存的元素类型开始,因为我们知道 LinkedList 内部其实是通过双向链表的形式来储存元素节点,那么我们来看看这个用于表示元素节点的类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;
    }
}

可以看到,这个 Node类实际上是 LinkedList类的一个私有内部类。包含元素值直接前驱直接后继。即为一个双向链表节点

一、LinkedList构造方法及字段详解:

下面来看看 LinkedList类的构造方法:

/**
 * Constructs an empty list.
 */
public LinkedList() {
}

/**
 * 构造一个包含了 c 集合中所有元素的 LinkedList 对象(按照 c 的迭代器遍历顺序添加元素),
 * 如果 c 为 null,那么抛出 NullPointException 异常
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

LinkedList提供了两个构造方法,并且也没有什么默认容量的概念,也没有扩容的概念,仔细想想很容易理解:双向链表的特性便是来一个元素就储存一个元素,即每当添加一个新元素的时候,就将其插入到当前链表的结尾,它不像数组一样需要一开始就要确定数组的容量,并且当有新的元素需要储存的时候还需要考虑当前数组剩余空间是否能够储存新的元素进而考虑数组扩容。但是相对于数组其缺点也很明显:每个节点除了保存当前节点的元素值以外,还需要保存其对应的直接前驱结点对象和直接后继结点对象的引用。在某个方面来说,这是消耗了额外的储存空间

下面来看看 LinkedList类中定义的相关字段:

// 保存 LinkedList 的元素数量,用 transient 关键修饰使其不参与序列过程
transient int size = 0;

/**
 * Pointer to first node.
 * 指向 LinkedList 第一个节点的引用
 * Invariant: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;

/**
 * Pointer to last node.
 * 指向 LinkedList 最后一个节点的引用
 * Invariant: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;

相比 ArrayList来说,LinkedList 定义的字段相对简单。

二、LinkedList添加元素详解:

好了,接下来看看LinkedList添加新元素的方法:

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

可以看到,通过这个方法将新的元素添加到 LinkedList末尾。这里调用了linkLast(E )方法来进行添加元素,我们跟进这个方法:

/**
 * Links e as last element.
 */
void linkLast(E e) {
    final Node<E> l = last;
    // 新建一个节点来保存要储存的元素值,并且将这个节点的直接前驱引用设置为 l(last)
    final Node<E> newNode = new Node<>(l, e, null);
    // 更新 last 引用(指针)
    last = newNode;
    // 如果当前 LinkedList 没有元素,那么将当前节点作为双向链表的头结点
    if (l == null)
        first = newNode;
    // 否则的话将 l.next 赋值为 newNode,即将 l(last)的直接后继节点设置为 newNode
    else
        l.next = newNode;
    // 元素个数 + 1
    size++;
    // 集合元素更改次数加 1,该变量在 AbstractList 中定义
    modCount++;
}

这里涉及到数据结构中在双向链表末尾添加新元素的过程,即将新的末尾节点和旧的末尾结点通过直接前驱和直接后继的关系链接起来,然后更新末尾节点为新添加的这个节点(相当于末尾节点引用(指针)后移,这里的源码是先后移再建立链接)。再来看一个重载的方法:

/**
 * Inserts the specified element at the specified position in this list.
 * Shifts the element currently at that position (if any) and any
 * subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
	// 检测下标是否越界,越界则抛出 IndexOutOfBoundsException 异常
    checkPositionIndex(index);
	
	// 如果 index 和 size 相等,那么即直接在链表尾部插入新元素
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

我们看到,方法通过 node(int index) 方法来得到指定下标的元素,并且通过 linkBefore() 来完成插入操作,我们先来看看node(int index)方法:

/**
 * Returns the (non-null) Node at the specified element index.
 */
Node<E> node(int index) {
    // assert isElementIndex(index);
	
	// 如果 index 不大于链表长度的 1/2 ,那么正向遍历, 找出对应下标的元素
    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;
    }
}

在这个方法里面通过链表长度的 1/2index 的值进行比较,进而判断是采用正向遍历链表还是反向遍历链表来找出指定下标的元素,最大化减少循环的执行次数,方法的设计者真大牛!接下来看看linkBefore(E , E )方法:

/**
 * Inserts element e before non-null Node succ.
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    // 记录 succ 节点的直接前驱节点
    final Node<E> pred = succ.prev;
    // 新建 Node 对象保存要插入的元素,并且指定其直接前驱结点和直接后继节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // succ 的直接前驱结点赋值为 newNode(在 succ 之前插入 newNode 节点)
    succ.prev = newNode;
    // 如果当前 LinkedList 没有任何元素,那么将这个节点作为 first 结点
    if (pred == null)
        first = newNode;
    // 否则将 succ 的直接前驱节点的直接后继结点设置为 newNode 完成插入
    else
        pred.next = newNode;
    // 元素个数加 1 
    size++;
    // LinkedList 更改次数加 1
    modCount++;
}

这个操作其实就是双向链表中在某个元素(这里为 succ )前插入一个新元素的操作,和插入元素到链表尾部差不多,不再陈述了。看完了主要的添加元素的方法,接下来看看获取元素的方法:

/**
 * Returns the element at the specified position in this list.
 *
 * @param index index of the element to return
 * @return the element at the specified position in this list
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E get(int index) {
	// 判断下标是否越界,如果越界抛出一个 IndexOutOfBoundsException 异常
    checkElementIndex(index);
    return node(index).item;
}

我们看到也是通过 node(int index)来得到对应下标的节点并返回储存的元素值。

三、LinkedList其它方法详解:

下面来看看修改元素的相关方法:

先是 set(int index, E element)方法:

/**
 * Replaces the element at the specified position in this list with the
 * specified element.
 *
 * @param index index of the element to replace
 * @param element element to be stored at the specified position
 * @return the element previously at the specified position
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E set(int index, E element) {
	// 检测下标是否越界,如果越界,抛出一个 IndexOutOfBoundsException 异常
    checkElementIndex(index);
    // 获取 index 下标所指的元素节点
    Node<E> x = node(index);
    // 更新元素节点的 item 引用为要设置的新值
    E oldVal = x.item;
    x.item = element;
    // 返回被替换的旧值
    return oldVal;
}

添加修改元素看完了,接下来是移除元素的方法了:

/**
 * Removes the element at the specified position in this list.  Shifts any
 * subsequent elements to the left (subtracts one from their indices).
 * Returns the element that was removed from the list.
 *
 * @param index the index of the element to be removed
 * @return the element previously at the specified position
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E remove(int index) {
	// 检测下标是否越界,越界抛出 IndexOutOfBoundsException 异常
    checkElementIndex(index);
    // 通过 node(int index) 方法找到要移除的元素,
    // 并且调用 unlink 方法来移除这个元素
    return unlink(node(index));
}

node(int index)方法我们已经讲过了,那么来看看 unlink(E )方法:

/**
 * Unlinks non-null node x.
 */
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;
	
	// 如果 x 的直接前驱节点为 null ,那么证明 x 为头结点,此时把 first 后移
    if (prev == null) {
        first = next;
    } else {
	    // 否则将 x 的直接前驱结点的直接后继结点指向 x 的直接后继结点(有点绕,仔细理解一下)
        prev.next = next;
        // 断开 x 和其直接前驱结点的联系
        x.prev = null;
    }
	
	// 如果 x 的直接后继结点为 null,那么证明 x 为尾节点,此时把 last 前移
    if (next == null) {
        last = prev;
    } else {
	    // 否则将 x 的直接后继结点的直接前驱结点指向 x 的直接前驱结点(仿造上面)
        next.prev = prev;
        // 断开 x 和其直接后继结点的联系
        x.next = null;
    }
	
	// x.item 置为空,方便 GC 回收对象
    x.item = null;
    // LinkedList 元素个数减 1
    size--;
    // LinkedList 修改次数加 1
    modCount++;
    // 返回被移除的节点的 item 元素值
    return element;
}

remove ()方法还有一个重载方法:

/**
 * Removes the first occurrence of the specified element from this list,
 * if it is present.  If this list does not contain the element, it is
 * unchanged.  More formally, removes the element with the lowest index
 * {@code i} such that
 * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
 * (if such an element exists).  Returns {@code true} if this list
 * contained the specified element (or equivalently, if this list
 * changed as a result of the call).
 *
 * @param o element to be removed from this list, if present
 * @return {@code true} if this list contained the specified element
 */
public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

操作其实差不多,如果要移除的元素值 o 为 null,那么遍历链表找到 item 等于 null 的节点,之后就是调用 unlink 方法来移除这个节点了。当 o 不为 null 的时候就遍历链表并且通过 equals 方法来找到 item 等于 o 的节点,再调用 unlink 方法来移除这个节点。

四、ArrayList 和 LinkedList 的区别是什么?

  • 数据结构实现:ArrayList 是动态数组数据结构实现,而LinkedList 是双向链表的数据结构实现。
  • 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,时间复杂度为O(1);因为 LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找,时间复杂度为O(N)
  • 增加和删除效率:在非首尾的增加和删除操作,LinkedList要比 ArrayList 效率要高,因为只需要改变指针的指向即可,时间复杂度为O(1);而ArrayList 增删操作要影响数组内的其他数据的下标,需要扩容,所以时间复杂度为O(N)
  • 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素一个指向后一个元素
  • 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全。

综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList

补充:数据结构基础之双向链表。

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点后继结点

五、链表结构和顺序存储结构的对比:

在这里插入图片描述

六、总结:

  • 有序,不唯一,线程不安全。
  • 内部采用双向链表来储存元素,每添加一个元素就新建一个 Node 并添加到对应的位置,就没有所谓的扩容机制
  • 同时实现了Deque 接口,可以作为队列 / 双端队列使用。插入元素、移除元素效率较高(时间复杂度为 O(1)),但是随机访问元素效率较低(时间复杂度为 O(N))。
  • 查找元素的时候使用将链表长度折半的方式进行查找,从而提高查找效率:
    在这里插入图片描述
    OK,到这里我们就把 LinkedList 的相关方法介绍完了,LinkedList 内部通过双向链表实现,相对 ArrayList 来说,其插入元素、删除元素的效率更高

Vector

这个类其实和 ArrayList 类相当像,也是利用数组储存元素,同时也可以动态的管理元素,我们可以看看它的类继承结构图:

在这里插入图片描述

一、Vector相关方法及线程安全详解:

可以看到 Vecctor 类和 ArrayList 继承的类和实现的接口都一样,那么它们有什么地方不同吗?答案是肯定的,要不然 Java 没必要设计两个功能相同的类来添加开发者的负担,我们先看看 Vector 类的构造方法:

/**
 * 创建一个初始容量为 initialCapacity ,每次扩容量为 capacityIncrement 的 Vector 对象
 */
public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}

public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

/**
 * Constructs an empty vector so that its internal data array
 * has size {@code 10} and its standard capacity increment is
 * zero.
 * 创建具有默认个数(10)个容量的 Vector
 */
public Vector() {
    this(10);
}

/**
 * 创建一个 Vector,并把集合 c 中的元素按照迭代器的遍历顺序将元素添加到 Vector 中
 * @since   1.2
 */
public Vector(Collection<? extends E> c) {
    elementData = c.toArray();
    elementCount = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}

我们可以看到 Vector类的构造方法中多了一个带有 capacityIncrement参数的方法,并且在代码中有一句:this.capacityIncrement = capacityIncrement; ,那么我们来看看这个 capacityIncrement字段的定义:

/**
 * The amount by which the capacity of the vector is automatically
 * incremented when its size becomes greater than its capacity.  If
 * the capacity increment is less than or equal to zero, the capacity
 * of the vector is doubled each time it needs to grow.
 */
protected int capacityIncrement;

从注释中我们可以得到如果这个值小于或等于 0,那么Vector 每次扩容的倍数为 2,即每次扩容时容量增加一倍。关于这个,我们可以参考源码:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
	
	// 如果 capacityIncrement  大于 0,那么扩大 capacityIncrement 大小,
	// 否则扩大 oldCapacity 大小(即扩大一倍)
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

Ok,如果没有给 Vector 指定每次扩大的容量,那么其每次默认扩大的倍数为 2

我们再来看其中的一些方法:
在这里插入图片描述

我们可以看到,类中的一些关键方法用 synchronized 关键字修饰,关于synchronized关键字,小伙伴们可以查阅资料或参考这篇博客Java 多线程(4) — 线程的同步(中)。回到这里,也就是说这些方法都是受同步控制的,即为多线程安全的方法。反观 ArrayList ,其并没有对方法加以同步控制,也就是说 ArrayList 是非线程安全的,我们取 ArrayList 中的一个 add 方法来看:

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

现在假设有两个线程同时进入这个方法内执行,首先线程 1 执行的很顺利,一直执行,直到 size++; 这一句,我们知道sie++是没有原子性的,线程 1先从主内存取到size的值,不巧的时候当线程 1 取到size 的值之后被阻塞了,我们把此时线程 1私有工作内存中的size的值记为oldSize,此时线程 2得到 CPU 资源开始执行,线程 2执行的很顺利,一次性就把add方法中的所有代码执行完了,并且把 size 的值更新到主内存中,那么此时主内存size的值为oldSize + 1,之后线程 2 让出 CPU 资源线程 1 得到 CPU 资源从上次停止的位置继续执行,因为此时在线程 1 中的size 的值还是为 oldSize,那么执行完之后线程 1 中的 size 的值会变成oldSize + 1,之后线程 1主内存size 的值更新为 oldSize + 1(其实线程 2 之前已经将主内存中 size 的值加一了),此时出现了明明添加了两个元素到 ArrayList中,而 size 的值确只增加了 1

对线程的私有工作内存和主内存不是很了解的小伙伴们,可以参考本博主的这篇博客的java内存模型模块:(2020史上最全总结,跳槽必看),一篇带你立马搞定jvm内存,类加载机制全过程,java内存模型,分代垃圾回收机制,垃圾回收算法和垃圾收集器

对于 Vector 来说,这种情况就不存在了,因为方法用 synchronized关键字修饰了,那么同一时刻只有一个线程能够进入方法中执行,即使这个线程被阻塞让出了 CPU,它所占用的锁资源并不会被释放,所以其他线程任然不能进入这个方法执行代码,这样就保证了该操作的多线程安全

二、ArrayList 和 Vector 的区别是什么?

这两个类都实现了 List接口(List 接口继承了 Collection 接口),他们都是有序集合

  • 线程安全:Vector使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList非线程安全的。
  • 性能:ArrayList 在性能方面要优于 Vecto,因为减少了同步的操作。
  • 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector扩容每次会增加 1 倍,而 ArrayList 只会增加 50%

Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。

Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。

三、插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性?

  • ArrayListVector底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快(时间复杂度为O(1)),而插入,删除数据慢(时间复杂度为O(N))
  • LinkedList 底层是基于双向链表进行存储数据,因为不能通过所以来进行查找元素,只能从头结点开始查找,所以时间复杂度为O(N),而插入删除元素的时,只需要改变元素的指针指向,不需要扩容,移动元素这些操作,所以时间复杂度为O(1)。
  • Vector 中的方法由于加了 synchronized修饰,因此 Vector 是线程安全容器,但性能上较ArrayList和LinkedList差。

四、总结:

  • 有序,不唯一,线程安全。
  • 和 ArrayList 相似,内部采用数组保存元素,默认容量为 10。
  • 创建时如果指定了 capacityIncrement参数,那么每次扩容时数组容量增加 capacityIncrement,否则扩容时数组容量变为原来的 2 倍

扩容机制:
在这里插入图片描述

Stack

最后来看看 Stack类,这个类继承了Vector类,提供了数据结构中 栈 的实现。我们来看看它的类继承图:

在这里插入图片描述
这里没有出现新的类和接口,但是个人觉得这里的继承设计并不合理,为什么这么说?我们知道的操作无非就几种:入栈出栈查看栈顶元素判断栈是否为空得到栈中元素的个数。而Vector不仅支持这几种操作,同时支持随机访问随机修改随机添加。没有必要直接继承 Vector类。我们知道类的继承层次越深,创建这个类所需要的内存空间就越大(创建子类对象之前得先创建其父类对象),而栈本身应该是一种轻量级的数据结构。个人觉得像 QueueDeque接口那样新建一个 Stack接口并提供栈的相关操作方法,然后让LinkedList类实现这个Stack接口并且重写其中对应的方法就可以了。当然这里也只是我的个人看法,可能设计者有其他的目的吧。我们还是看一下 Stack类中的一些方法:

/**
 * Creates an empty Stack.
 */
public Stack() {
}

/**
 * 添加元素到栈顶
 */
public E push(E item) {
    addElement(item);

    return item;
}

/**
 * 返回并弹出栈顶元素,如果栈为空,
 * 那么抛出一个 EmptyStackException 异常
 */
public synchronized E pop() {
    E       obj;
    int     len = size();

    obj = peek();
    removeElementAt(len - 1);

    return obj;
}

/**
 * 返回但是不弹出栈顶元素,如果栈为空,
 * 那么抛出一个 EmptyStackException 异常
 */
public synchronized E peek() {
    int     len = size();

    if (len == 0)
        throw new EmptyStackException();
    return elementAt(len - 1);
}

/**
 * 判断栈是否为空
 */
public boolean empty() {
    return size() == 0;
}

可以看到这里面的一些方法也是使用了synchronized修饰,也就是说Stack类的方法也是线程安全的,可能设计想把 Stack 设计成线程安全的类,所以让其继承 Vector 类吧。

大总结:

  • ArrayList:内部采用数组保存元素,初始默认容量为 10,之后添加元素时,如果数组容量不足,则以1.5 倍的倍数扩容数组,溢出时抛出 OutOfMemeryError异常。扩容操作即为新建一个更大的数组并将原数组中的元素拷贝到新数组中。在元素较多时扩容操作开销较大,如果一开始可以确定最大需要的容量,那么建议使用另一个构造方法来创建指定初始容量的 ArrayList 以提高效率。因为采用的数组储存元素,所以查找的时间复杂度为O(1);插入和删除元素操作较慢(时间复杂度为 O(N))。 ArrayList 为非线程安全的类。
  • LinkedList :内部采用双向链表来储存元素,每添加一个元素就新建一个Node并添加到对应的位置,就没有所谓的扩容机制,同时实现了 Deque 接口,可以作为队列 / 双端队列使用。插入元素、移除元素效率较高(时间复杂度为 O(1)),但是随机访问元素效率较低(时间复杂度为 O(N))。LinkedList 非线程安全。
  • Vector :和 ArrayList相似,内部采用数组保存元素,默认容量为 10。创建时如果指定了 capacityIncrement 参数,那么每次扩容时数组容量增加 capacityIncrement,否则扩容时数组容量变为原来的 2 倍Vector 线程安全。
  • Stack :继承于 Vector 类,提供了数据结构中 栈 的相关操作方法,线程安全

好了,这篇文章我们一起看了一下 ArrayListLinkedListVectorStack等 List 接口下的类,并且从源码的角度上分析了一些常用的方法和这些类各自的特性。下篇文章我们将继续探讨 Map集合接口中的一些类和接口。

如果博客中有什么不正确的地方,还请多多指点。如果这篇文章对您有帮助,请不要吝啬您的赞,欢迎继续关注本专栏。

谢谢观看。。。

感谢博主大佬,昵称为:Hiro的支持和昵称为:ThinkWon的支持。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值