学习笔记(三):Java中的List集合——ArrayList、LinkedList、Vector、Stack、CopyOnWriteArrayList

2023-11-19

目录

引言

一、List简介

二、常用List实现类

(一)ArrayList

(二)LinkedList

(三)LinkedList和ArrayList的比较

三、其他List实现类

(一)Vector

(二)Stack

(三)CopyOnWriteArrayList 

 四、List的一些补充

(一)遍历List

(二)线程同步问题

(三)List排序问题

结语


引言

无论学习什么编程语言,语法和数据结构总是绕不开的知识点;在面试过程中,集合也是面试官常常提问的知识点之一。List是一个常用的集合,这里就对一些常用的、面试可能会碰到的一些List进行一个知识点的总结。

一、List简介

1. 功能描述 

List是Java中最常用的集合之一,我们常常使用List存储一些数据量大小未知且未来可能需要添加或者删除操作的数据。

List在Java中是一个泛型接口接口,其接口中的参数化类型E表示的是List中存储的数据类型。关于List接口的详细描述,我们可以查看下面的Java 8官方文档中对List接口的描述:

An ordered collection (also known as a sequence). The user of this interface has precise control over where in the list each element is inserted. The user can access elements by their integer index (position in the list), and search for elements in the list.

—— Java 8官方文档

 笔者渣翻:一个有序的集合(也可以说是序列)。接口的使用者可以准确的控制元素插入到列表中的任意位置。使用者可以通过元素的整数下标在列表中访问、查找列表中的元素。

2. 继承介绍

接口的继承关系如下图所示:

List接口的继承关系
List接口的集成关系

 在图中我们可以看到,List接口继承自Collection接口,Collection接口又继承自Iterable接口。这些接口都有什么作用,我们从最上面的说起。

Iterable是一个泛型接口,我们直译这个接口的名字就能大概明白它的作用,“可迭代的”。在这个接口中只定义了三个方法,但只有一个是抽象的,它们分别是forEach()、iterator()和spliterator(),其中iterator()是抽象方法,forEach()和spliterator()为默认实现,作用反别是遍历以及得到一个分割器(也有叫拆分器的,有许多叫法,都是指Spliterator)。iterator()返回值是一个迭代器,用于遍历元素。因此,只要实现该接口,就表明实现类是可迭代的,即可以使用迭代器对其进行遍历。

Collection继承自Iterable接口,因此,Collection接口同样也是一个泛型接口。我们对接口名称进行直译,翻译为“集合”。由此可见,Collection接口是对集合工具类的规范进行书写的一个规范接口,也是集合层次中的根接口。在其中定义了一些集合工具类统一规范:size()、isEmpty()、contains()、toArray()等。根据Java 8的官方文档,在Jdk中并没有直接提供该接口的实现类,只有该接口具体子接口。因此该接口一般用于集合的传递和需要最大泛用性的地方(使用Java的多态特性接收使参数可以同时接收List、Set等继承了Collection接口的集合),如下面列举的Collection接口中的两种使用。

boolean containsAll(Collection<?> c);

boolean addAll(Collection<? extends E> c);

3. 常用实现类

在List接口下,有许多的实现类与实现抽象类,但在我们平常的开发中并不是所有都能用到。

常用的如下(重点):

ArrayList、LinkedList

面试时可能会碰到的如下:

Vector、Stack、copyOnWriteArrayList

二、常用List实现类

(一)ArrayList

1. ArrayList简介

Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)

—— Java 8官方文档

 笔者渣翻:(ArrayList)是List接口下可变长度数组的实现。实现了所有可选的List操作,并且允许所有类型的元素,包括null。除了实现List接口外,该类还提供了一些用于操作在内部存储列表的数组的长度。(该类与Vector相比,除了不同步外都大致相同)

该类的声明如下:

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

总结:基于数组实现、允许有重复元素、允许有空元素、线程不安全、支持随机访问、可克隆、可序列化

2. ArrayList原理

通过查看官方文档我们知道,ArrayList底层是基于数组来实现的,我们查看Jdk(这里使用的是 Jdk 8)中的源码深入了解一下ArrayList。

进入ArrayList源码,首先映入眼帘的就是一下几个参数:

private static final int DEFAULT_CAPACITY = 10;

private static final Object[] EMPTY_ELEMENTDATA = {};

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

transient Object[] elementData;

private int size;

这几个参数中一些是比较容易理解的——DEFAULT_CAPACITY,ArrayList的默认容量;elementData,ArrayList用于存储元素的数组;size,ArrayList的当前长度。

而EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA两个Object数组的用处后面再做解释。

(1)构造方法

参数后面便是ArrayList的构造方法,先看我们最常用(maybe)的无参构造:

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

使用无参构造方法时,直接将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给了elementData,得到一个初始容量为10的ArrayList。

至于为什么初始容量是10,后面讲解add()方法时再做解释。

除了无参构造外,ArrayList还有一个接收一个int类型参数的构造方法。

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

由方法中的形参名称便可猜出接收到的这个变量是干什么的。没错,这个构造方法就是接收一个ArrayList的初始化容量,然后使用这个容量构造ArrayList内部的Object数组。如果这个容量为0,则使用ArrayList中已经进行了初始化的EMPTY_ELEMENTDATA作为elementData的值。如果容量小于0,则抛出非法参数异常。

最后,ArrayList还可以通过其他Collection接口的实现类来构造一个实例:

public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            elementData = EMPTY_ELEMENTDATA;
        }
    }

在方法中可以看到,先通过Collection中声明了的toArray()方法将Collection转换为一个Object[] a,将a的length赋值给ArrayList的成员变量size。然后判断size是否等于0,如果是,则领elementData等于EMTPTY_ELEMENTDATA;否则,判断Collection的类型,如果是ArrayList,将a赋值给elementData,如果不是,则使用Arrays工具类对a进行拷贝,结果赋值给elementData。

(2)add()方法

下面详细讲解一下添加元素的过程,不想看过程可以直接看下面的红字总结,面试喜欢问。

add()的源码如下:

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

 ensureCapacityInternal()的源码如下:

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

calculateCapacity()的源码如下:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

 ensureExplicitCapacity()的源码如下:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

grow()的源码如下:

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

列了这么多的方法,我们一个一个来看。

首先我们从add()方法开始,在方法的第一行就调用了ensureCapacityInternal()方法,我们通过直译得到这个方法的作用,确保内部容量,参数是minCapacity,值为size + 1,意思是最小容量应该是size + 1。然后在ensureCapacityInternal()方法内又调用了一个计算容量的方法,也即calculateCapacity()方法。在calculateCapacity()方法的内部我们看到有一个判断,判断elementData是否是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,如果是,返回size + 1和DEFAULT_CAPACITY(具体值为10)的最大值,否则返回size + 1。这里就解释了上面使用无参构造方法时初始容量为10。

在此之后,将计算得到的容量作为参数,调用了一个ensureExplicitCapacity(),确认显式容量。在该方法中首先对modCount进行了自增,modCount时ArrayList中定义的用来记录数组修改次数的一个成员变量。然后判断size + 1是否大于element.length来决定是否需要调用grow()方法来增加ArrayList的容量。

在grow()方法中,定义了两个临时变量oldCapacity和newCapacity。oldCapacity为elementData的长度,newCapacity为oldCapacity加上oldCapacity右移一位的结果,

相当于newCapacity = oldCapacity + oldCapacity / 2。然后在下面有一个newCapacity和MAX_ARRAY_SIZE的比较,MAX_ARRAY_SIZE是ArrayList内定义的一个常量,值为int最大值减8。如果newCapacity大,则调用hugeCapacity()方法返回int最大值或者MAX_ARRAY_SIZE作为新容量,如果size + 1 < 0,则抛出OOM(out of memory)异常。确定了新容量后,使用Arrays工具类对elementData进行拷贝,结果赋值给elementData。

确认是否需要扩容后,将需要添加的元素放到elementData中索引为size的位置,然后size自增,返回true。

以上就是add()方法的执行全过程。总结一下就是先确定添加这个元素是否需要对容器进行扩容操作,如果容器有空位,则不进行扩容;否则,采取一般扩容策略进行扩容操作,即新容量为老容量加上老容量的一半,如果新容量超过了MAX_ARRAY_SIZE,则看具体情况使用int最大值或者MAX_ARRAY_SIZE作为新容量。扩容完成后,将元素存到数组中,size自增。如果当前容量为int最大值且容器已经装满,无法扩容,抛出OOM异常。

针对一般扩容策略,这里举个栗子,初始容量为10,扩容后为10 + 10 / 2 = 15;15容量扩容后为15 + 15 / 2 = 22。以此类推……

-----------------------------------------------------------------------------------------------

到这里我觉得在我在面试中经常被问到的ArrayList相关的知识点也整理的差不多了,虽然还有很多操作和方法没有将,但是每一个都讲实在写不了了(悲),因此在这里只详细介绍了较为典型的add()操作。在理解了add()操作后再看其他操作相信也是如吃饭喝水一般了。对其他操作感兴趣的同学也可以自行阅读源码。如果觉得笔者上面讲的还是不怎么清楚,“纸上得来终觉浅”,自己debug一下或许也就念头通达了呢~

(二)LinkedList

1. LinkedList简介

Doubly-linked list implementation of the List and Deque interfaces. Implements all optional list operations, and permits all elements (including null).

All of the operations perform as could be expected for a doubly-linked list. Operations that index into the list will traverse the list from the beginning or the end, whichever is closer to the specified index.

Note that this implementation is not synchronized.

—— Java 8官方文档

 笔者渣翻:(LinkedList)是Deque接口和List接口双向链表的实现。实现了所有List接口中的可选操作,并且允许所有类型的元素(包括空元素)。

所有操作地执行都和双向链表的期望一样。有用到List中索引的所有操作都将从(链表的)开始或者结尾遍历,从哪一段开始由索引距离哪一端近决定。

需要注意的是,此实现不是同步的。

 该类的声明如下:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

总结:基于双向链表实现、允许有重复元素、允许有空元素、线程不安全、可克隆、可序列化

2. 链表基础

因为怕一些小伙伴没学过C语言或是数据结构的知识,理解这一块的知识有一定的障碍,因此这里对链表的知识进行一个补充。

觉得对链表有一定了解的小伙伴可以直接跳过这一部分。

(1)链表介绍

链表是一种物理存储单元上非连续、非顺序的存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间。

 —— 百度百科

 上述是百度百科中对链表的描述,读起来比较抽象,可以结合下面的栗子自己理解一下。

链表使用图形更加形象的表示出来如图:

 其中每一个大方框表示链表中的一个节点。而在节点中又分为两部分(实际上一个节点解释一个完整的类,不作区分,这里为了形象强行分了出来),data部分用于存储数据,next是一个指针,指向下一个节点,也即存储了下一个节点的地址。最后一个节点的next指针则只想null。head是指向第一个节点的指针,称为“头指针”。一般来说,头指针不储存数据(也有储存数据的,看个人习惯)。

(2)链表的遍历、查找

由于链表是不同于数组的数据结构,数组是在内存中一片连续的区域,因此可以使用索引进行随机访问和快速遍历;而链表在内存中的存储可以是不连续的,因此在查找时不能通过索引直接获取到对应的元素。

因此需要使用一个指针p,令这个指针p等于头指针head,然后可以获取到第一个元素。获取到第一个元素后,令p等于第一个节点的next,因此指针就由最开始的指向第一个节点变为指向第二个节点。以此循环往复,直到p等于null就遍历完了整个链表。如果是查找的话,直到满足查找条件或者p为null即可。

(3)链表的插入和删除

因为插入和删除本质上都差不多,删除实际上就是插入的逆过程,因此,考略到篇幅原因,这里只讲插入。

数组中,在指定位置插入一个元素是非常困难的。因为你必须将该位置以及位置后面的所有元素往后挪一位,为插入元素腾出位置。

而链表中因为节点之间在内存中并不连续,因此只需要对节点中的next作文章即可。代码如下:

// 这里虚构一个类Node,表示节点
// 找到需要插入的位置p
// 需要插入的节点为target
Node q = p.next;
p.next = taget;
target.next = q;

 形象表示如下:

 (4)双向链表

以上所描述的都是单向链表,双向链表是对单向链表的升级。每个节点不只有后指针,也有一个前指针指向前一个结点。这样做解决了单向链表只能单向遍历的弊端。

形象表示如下:

 双向链表再插入和删除时就必须对前指针和后指针都进行更改和设置,相较于单向链表操作更加复杂。

 LinkedList的底层实现使用的就是双向链表。

3. LinkedList的使用

(1)链表节点

在LinkedList中,双向链表节点的实现使用了内部类的形式,代码如下:

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)LinkedList的使用

LinkedList一般作为栈、队列的替代使用,在算法中使用的较多。

-----------------------------------------------------------------------------------------------

LinkedList中也是有许多操作的,这里为什么不详细介绍呢?因为LinkedList内部操作还是比较简单的,具体一些查询、插入、删除等操作和基础链表中的操作相差无几,因此不多赘述。对链表操作不熟悉的小伙伴可以参考上面的链表基础或是参照其他数据结构教学资源自行了解。

(三)LinkedList和ArrayList的比较

这里使用Junit进行测试,测试类如下:

public class AppTest {

    private List<Integer> linked;
    private List<Integer> array;

    @Before
    public void before() {
        linked = new LinkedList<>();
        array = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            linked.add(i);
            array.add(i);
        }
    }

}

 这里再每个测试方法执行前,使用@Before注解对两个List进行初始化,存入了0-9999,即10000个数字,然后运行测试方法进行时间比较。

注:测试的运行时间根据不同的电脑配置、Jdk版本、测试方法、Junit版本而不同。

1. 插入、删除操作

测试代码:

@Test
public void testList() {
    // 插入测试
    long sta = System.nanoTime();
    array.add(0, 0);
    long end = System.nanoTime();
    System.out.println("ArrayList在0处插入所需时间" + (end - sta) + " ns");
    sta = System.nanoTime();
    linked.add(0, 0);
    end = System.nanoTime();
    System.out.println("LinkedList在0处插入所需时间" + (end - sta) + " ns");
    // 删除测试
    sta = System.nanoTime();
    array.remove(0);
    end = System.nanoTime();
    System.out.println("ArrayList在0处删除所需时间" + (end - sta) + " ns");
    sta = System.nanoTime();
    linked.remove(0);
    end = System.nanoTime();
    System.out.println("LinkedList在0处删除所需时间" + (end - sta) + " ns");
}

在0处进行插入和删除:

 在5000处插入和删除:

 

在9999处插入和删除: 

2. 根据索引取值

测试代码:

@Test
public void testList() {
    long sta = System.nanoTime();
    array.get(5000);
    long end = System.nanoTime();
    System.out.println("ArrayList在5000处取值所需D时间" + (end - sta) + " ns");
    sta = System.nanoTime();
    linked.get(5000);
    end = System.nanoTime();
    System.out.println("LinkedList在5000处取值所需时间" + (end - sta) + " ns");
}

3. 遍历

测试代码:

@Test
public void testList() {
    long sta = System.nanoTime();
    for (Integer i : array) {
        int integer = i;
    }
    long end = System.nanoTime();
    System.out.println("ArrayList遍历时间" + (end - sta) + " ns");
    sta = System.nanoTime();
    for (Integer i : linked) {
        int integer = i;
    }
    end = System.nanoTime();
    System.out.println("LinkedList遍历时间" + (end - sta) + " ns");
}

-----------------------------------------------------------------------------------------------

结论:在插入和删除上,如果数据量较小时,理论上LinkedList是要比ArrayList要快的;如果数据量比较的庞大,在对List两端或是离两端较近的位置上进行操作时,依然是LinkedList较快,但是在靠近中间的位置时,ArrayList要快一点(因为LinkedList要从开头或是结尾处一个一个遍历,数据量大时比较耗时)。

在根据索引取值上更是不必多说,数据量越大,两者之间的取值时间相差就越大,在这方面ArrayList完胜。在遍历方面,ArrayList以零点几毫秒的时间差小胜LinkedList。

因此,在平常开发中,使用哪种List储存数据得根据实际需求、数据量大小、使用场景来自行定夺。

三、其他List实现类

(一)Vector

The Vector class implements a growable array of objects. Like an array, it contains components that can be accessed using an integer index. However, the size of a Vector can grow or shrink as needed to accommodate adding and removing items after the Vector has been created.

......

Unlike the new collection implementations, Vector is synchronized. If a thread-safe implementation is not needed, it is recommended to use ArrayList in place of Vector.

—— Java 8官方文档

笔者渣翻:Vector类实现了一个长度可变的对象数组。跟数组一样,它包含了可以通过整数索引访问的组件。但是,Vector的长度可以根据需要增长或缩短,以此来适应Vector在创建实例后的添加和删除元素。

……

不像新集合的实现,Vector是同步的。如果不需要线程安全的实现,推荐使用ArrayList来代替Vector。

总结:基于数组实现、允许有重复元素、允许有空元素、线程安全、支持随机访问、可克隆、可序列化

-----------------------------------------------------------------------------------------------

 Vector的使用、一般扩容策略基本与ArrayList相差无几,它们俩最大的区别就是线程安全还是不安全。Vector在几乎所有的方法上都加了synchronized关键字。

可能会奇怪,多线程现如今使用较广,为什么Vector会被人们忽略?

(1)Vector效率低下。Vector使用的是synchronized关键字实现的线程同步,而synchronized关键字施加的是一种重量级的锁,加锁和释放锁都会非常的耗费系统资源,因此耗时会很大,效率也就非常低下。

(2)Vector是伪线程安全的。虽然Vector对绝大多数方法都加上了synchronized关键字,表示同一时间只能用一个线程使用这个方法。但是,该关键字不能防止两个线程同一时间使用不同的方法。例如同时添加和删除。例如下面这段代码,一个线程进行添加操作,令一个线程进行删除操作,当数据量足够大时,就会抛出越界异常。因此,在对Vector进行复杂的操作时,要想实现真正的线程安全,还必须在Vector外面再加锁,非常的麻烦且耗费资源。

public class App {

    private static final Vector<Integer> VECTOR = new Vector<>();

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000000; i++) {
                    VECTOR.add(i);
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000000; i++) {
                    VECTOR.remove(0);
                }
            }
        }).start();
    }

}

(二)Stack

 The Stack class represents a last-in-first-out (LIFO) stack of objects. It extends class Vector with five operations that allow a vector to be treated as a stack.

—— Java 8官方文档

 笔者渣翻:Stack类表示一个后进先出(LIFO)的对象栈。它通过五种操作扩展了Vector类来让一个Vector可以被看做一个栈。

 总结:Vector的子类、线程安全、后进先出

-----------------------------------------------------------------------------------------------

Stack作为Vector的一个子类,在平时开放中同样不怎么能被人们想起。它逐渐过气的原因与它的父类基本相同,除此之还,它的作用也逐渐被Deque(双端队列接口)所取代。 

(三)CopyOnWriteArrayList 

A thread-safe variant of ArrayList in which all mutative operations (addset, and so on) are implemented by making a fresh copy of the underlying array.

......

All elements are permitted, including null.

—— Java 8官方文档

 笔者渣翻:(CopyOnWriteArrayList)是一个ArrayList线程安全的变体,所有元素变动操作(添加、设置等)都是通过创建一个基本数组的副本来实现的。

……

允许所有元素,包括空元素。

总结:基于数组实现、允许有重复元素、允许有空元素、线程安全、支持随机访问、可克隆、可序列化

 -----------------------------------------------------------------------------------------------

 CopyOnWriteArrayList同样也是相当于线程安全版的ArrayList,为什么Vector就遭到了冷落?

(1)CopyOnWriteArrayList的锁机制相较于Vector更加轻量级。CopyOnWriteArrayList的锁没有Vector的锁那么耗费资源,因此在效率上要比Vector高。

(2)CopyOnWriteArrayList是真·线程安全。参见Vector条目。

 四、List的一些补充

(一)遍历List

1. for循环遍历

这是比较笨重,但是也是使用最广泛、最基础的一种遍历方式,参考代码如下:

List<Integer> list = new ArrayList<>();
... // 添加元素、删除元素等
for (Integer i : list) {
    System.out.println(i);
}
List<Integer> list = new ArrayList<>();
... // 添加元素、删除元素等
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

2. 迭代器遍历

因为集合类都实现了Iterable接口,因此可以通过该接口中定义的iterator()方法获取到一个该列表的迭代器,通过这个迭代器来遍历列表,参考代码如下:

List<Integer> list = new ArrayList<>();
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

3. forEach lambda遍历

通过Iterable接口中定义的forEach()方法,该方法时Iterable接口中的默认实现。参考代码如下:

List<Integer> list = new ArrayList<>();
list.forEach(i -> {
    System.out.println(i);
});

4. stream forEach遍历

该方法是通过调用Collection接口中定义的默认实现方法stream(),将List转换为Stream,通过遍历Stream,简介实现对List的遍历:

List<Integer> list = new ArrayList<>();
list.stream().forEach(i -> {
    System.out.println(i);
});

-----------------------------------------------------------------------------------------------

 这里列举了4种,准确来说是5种遍历List的方式,使用下面的测试代码在Junit种对每种遍历方式测得了遍历时间:

@Before
public void before() {
    for (int i = 0; i < 100000000; i++) {
        list.add(i);
    }
}

@Test
public void testList() {
    long sta = System.nanoTime();
    for (int i = 0; i < list.size(); i++) {
        list.get(i);
    }
    long end = System.nanoTime();
    System.out.printf("%-21s%d ns\n", "普通for遍历耗时", (end - sta));
    sta = System.nanoTime();
    for (Integer i : list) {
        Integer a = i;
    }
    end = System.nanoTime();
    System.out.printf("%-21s%d ns\n", "增强for遍历耗时", (end - sta));
    sta = System.nanoTime();
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()) {
        iterator.next();
    }
    end = System.nanoTime();
    System.out.printf("%-21s%d ns\n", "迭代器遍历耗时", (end - sta));
    sta = System.nanoTime();
    list.forEach((i) -> {
        Integer a = i;
    });
    end = System.nanoTime();
    System.out.printf("%-21s%d ns\n", "forEach方法遍历耗时", (end - sta));
    sta = System.nanoTime();
    list.stream().forEach((i) -> {
        Integer a = i;
    });
    end = System.nanoTime();
    System.out.printf("%-21s%d ns\n", "stream forEach方法遍历耗时", (end - sta));
}

在List种初始化了1亿个整数,测试结果如下:

 结论:普通for、迭代器 < stream forEach < 增强for、forEach方法

 这里如果追求效率的话推荐使用迭代器,追求功能性强大的话推荐使用stream。

如果对stream不熟悉的话,可以学习我之前写的stream API详解。

学习笔记(一):Java中Stream的基本用法和相关API详解

(二)线程同步问题

之前稍微介绍了一点List与线程相关的一点问题,下面详细讲一下如何得到一个线程安全的List集合。

1. 使用CopyOnWriteArrayList代替ArrayList

使用前面讲解过的CopyOnWriteArrayList可以很好的解决线程安全问题,但是得考虑资源消耗问题。

2. 使用Collections工具类中的synchronizedList()方法进行包装

该方法封装在Collections工具类中,是一个静态方法,可以直接通过类名调用。synchronizedList()方法接收一个List对象list,该方法会判断list是否实现了RandomAccess接口,对应返回一个基于list创建的SynchronizedRandomAccessList或是SynchronizedList的实例。这两个类都是线程同步的List集合,一个支持随机访问,一个不支持。

代码示例:

List<String> list = Collections.synchronizedList(new ArrayList<>());

 通过同步方法包装后便可以跟普通List一样使用了。

3. 使用早期的线程安全的集合(不推荐)

如果你对前面两种方法都不熟悉,可以使用Vector等早期的线程安全的集合,但是这里还是推荐学习前两种方法。这里非常不推荐早期的线程安全的集合。

(三)List排序问题

1. Collections.sort()

在集合工具类Collections中有对List排序的sort()方法。该方法有两种重载,接收一个List对象以及接收一个List对象和比较器。当List中的元素的类实现了Comparable接口,可以使用一个参数的排序方法;如果没有实现可比较接口,则必须提供一个比较器,使用两个参数的sort()方法。

@Test
public void testList() {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(5);
    list.add(2);
    list.add(0);
    Collections.sort(list);
    System.out.println(list);
}

2.  通过Stream对List排序

我们可以先将List转化为Stream,然后使用Stream中的sort()方法进行排序。

 如果对stream不熟悉的话,可以学习我之前写的stream API详解。

学习笔记(一):Java中Stream的基本用法和相关API详解

@Test
public void testList() {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(5);
    list.add(2);
    list.add(0);
    list = list.stream()
            .sorted()
            .collect(Collectors.toList());
    System.out.println(list);
}

 3. TreeSet

TreeSet是Java中一个不包含重复元素的Set集合,它的底层基于红黑树来维护,因此将List集合中的元素一个一个加入TreeSet中时也就对List完成了排序。

TreeSet的详细讲解后面也会更新的Set集合笔记中讲解。

@Test
public void testList() {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(5);
    list.add(2);
    list.add(0);
    list = new ArrayList<>(new TreeSet<>(list));
    System.out.println(list);
}

 

 -----------------------------------------------------------------------------------------------

上面介绍了三种对List排序方法,这较为推荐的的Stream,因为Stream不但可以对List进行排序,而且可以进行映射、查找等各种操作,非常的方便,笔者平时也非常喜欢使用这种方式。如果对stream不熟悉的话,可以学习我之前写的stream API详解。

学习笔记(一):Java中Stream的基本用法和相关API详解

结语

非常感谢看完这接近两万字的文章,我们应该大概可能也许不一定还会更新其他类型的集合讲解吧,对你有帮助的话还请点一个免费的赞或者收藏,想看后面的详解也可以点一个关注哦,非常感谢。

虽说讲的也比较多了,但是还是有许多的地方没有讲的很清楚,感兴趣的小伙伴也可以查询相关资料或是阅读源码自行理解。这里推荐阅读Java官方文档结合源码食用理解更加透彻哦。

笔者也是前一两个月才开始写文章的,有待改进、书写错误、逻辑漏洞、知识讲解有误的地方还请指出,大家友善讨论。

有不理解的地方也可以私信,或是评论留言。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

学习笔记(三):Java中的List集合——ArrayList、LinkedList、Vector、Stack、CopyOnWriteArrayList 的相关文章

  • 如何像java中的make一样程序化生成塞尔达传说

    我将如何用java制作程序生成的地图 游戏本身就像塞尔达传说是程序生成的 有帮助吗 不久前的 塞尔达传说 地图使用等距平铺视图 您需要做的第一件事是将等距图块集加载到您的程序中 我确信您可以找到塞尔达图块集 然后 您需要决定如何按程序生成地
  • Jsch:命令输出不可用

    我正在尝试使用 jsch 连接到远程交换机并运行一些命令并提取输出 我可以使用 连接到交换机 但是命令输出在输入流中不可用 也许我没有以正确的方式做这件事 这是代码 session jsch getSession user 10 0 0 0
  • 同步不经常更新的哈希图的最佳方式

    我有一个在应用程序中使用的 HashMap 数据是在应用程序初始加载期间从数据库填充的 然后它始终只是读取并且从不更新 会有多个线程不断地读取数据 由于数据永远不会更新 因此我们目前不使用任何同步 仅使用 HashMap 我们现在定义的方式
  • ModelMapper:匹配多个源属性层次结构

    我无法解决 modelMapper 错误 您知道问题出在哪里吗 注意 鉴于 java sql Time 没有无参构造函数 我没有找到比编写转换器更好的方法 org modelmapper ConfigurationException Mod
  • 如何在类路径上遍历目录树查找资源文件?

    我在类路径上有一个资源集合 大致如下 com example foo r1 txt r2 txt r3 txt bar b1 txt b2 txt baz x txt y txt 我知道这个包位于 WEB INF lib 中的类路径上 我希
  • 从java程序调用SVN命令

    我想从 java 程序调用 SVN 命令 update commit 有什么帮助吗 SVN 乌龟SVN 环境 java程序将在jBoss服务器内运行 从应用程序服务器内使用 GUI SVN 客户端是一个非常非常糟糕的主意 而Tortoise
  • 使用 Java Config 围绕 Spring Security 匿名访问的混乱

    我将以下 Java 配置与 Spring Security 结合使用 protected void configure HttpSecurity http throws Exception http authorizeRequests an
  • IntelliJ IDEA 中的自动错误检测

    我是 Java 编程语言和 IntelliJ IDEA 2017 1 IDE 的新手 我刚刚安装了 IDE 并激活了所有各种检查 但每当我犯了错误 例如省略括号或分号 时 IDE 都无法检测到错误 此图像显示激活的检查 This is a
  • 为什么 Eclipse 要求我在 java 代码中设置(任意)括号?

    我目前正在尝试弄清楚如何使用 Eclipse 在 java 中对 Escape 模型进行编程 我对 Escape 和 Eclipse 很陌生 自从我用 java 编程以来已经有一段时间了 所以如果这是一个愚蠢的问题 请原谅 基本上 我一直被
  • 查看两个对象是否具有相同的类型

    假设我有一个类 A 并且 B C D 都是从 A 派生的 如果我想知道引用的对象的类型是什么 我可以声明 pseudo code if obj instanceof B lt is B gt else if obj instanceof C
  • 在使用 Ant/Jenkins 时,如何查看同一 Java 项目的不同 Subversion 标签/分支?

    这是我的开发配置 颠覆之下 我有我的project X trunk 带有我最新的开发人员 我有我的project X tags 具有不同的版本 我正在考虑添加一个分支文件夹 我正在使用 Jenkins 使用 Ant 脚本构建我的projec
  • 无法解析符号“servlet”

    我有一个新手大问题 当我尝试以下操作时 servlet 变成红色并指示 无法解析符号 servlet import javax servlet http import javax servlet ServletException 我的 ap
  • DateTimeFormatter 中的通配符

    我需要将一个字符串解析为LocalDate 该字符串看起来像31 03 2016用正则表达式术语 即 表示日期数字后可能有 0 个或多个未知字符 输入 输出示例 31xy 03 2016 gt 2016 03 31 我希望在 DateTim
  • Java 中的冒号是什么意思?

    Java 中的冒号是什么意思 我有这个 public static List
  • 在Fragment中第一次调用时SharedPreferences为空

    我有一个示例 Android 应用程序 根据位置 邮政编码 和设置 SharedPreference 中设置的温度单位 该应用程序显示 7 天的天气 当应用程序第一次获取温度并检查 SharedPreference 中设置的温度单位时 它似
  • 如何从 Ant 构建文件设置 Eclipse 构建路径和类路径?

    关于 Ant 和 Eclipse 有很多讨论 但之前的答案似乎对我没有帮助 事情是这样的 我正在尝试构建一个可以从命令行使用 Ant 成功编译的 Java 程序 更令人困惑的是 我尝试编译的程序是 Ant 本身 我真正想做的是将这个项目引入
  • JavaFX 3D 面孔着色...再次

    我研究了这个question https stackoverflow com questions 26831871 coloring individual triangles in a triangle mesh on javafx 但我还
  • 在Java中,如何在每次进入或退出给定对象的监视器时记录一条消息?

    我正在尝试调试一些使用一些自定义引用计数 锁定的 C Java 绑定 我想让 JVM 在每次给定对象进入或退出其监视器时打印一条消息 有什么办法可以做到这一点吗 基本上 我想要这个 synchronized lock System out
  • 如何解决消息有效负载类型为:Mule 中的 BufferInputStream 异常

    我已经转换为字节数组 但我不断收到此错误 ERROR 2015 02 25 11 12 30 517 ESR HTTP Request Listener worker 01 org mule exception DefaultMessagi
  • Java 全屏模式对话框

    如何创建一个可用作内部对话框的自定义模式 JDialog 用于全屏独占模式 我有一个 JScrollPane 带有一个巨大的滚动条 里面充满了巨大的按钮 如下所示 FOO BAR BIZ

随机推荐

  • 前端开发时常用的第三方工具库

    前端开发时常用的第三方工具库 JavaScript 实用工具库 一 lodash 1 官方文档 中文文档 https www lodashjs com 2 简介及使用场景 Lodash 是一个一致性 模块化 高性能的 JavaScript
  • windows sqlite可视化工具sqlitestudio下载、安装、使用

    1 下载地址 https sqlitestudio pl index rvt 2 使用 选择数据库 gt 添加数据库 gt 选择你的本地数据库 并点击 增加就可以查看数据库了
  • 浅谈Buffer

    什么是Buffer 在 Node js 中 Buffer 类是随 Node 内核一起发布的核心库 Buffer 库为 Node js 带来了一种存储原始数据的方法 可以让 Node js 处理二进制数据 global Buffer gt f
  • 爬虫小白也能玩转!Python爬虫中的异常处理与网络请求优化

    大家好 我是来自爬虫世界的小编 今天 我要和大家分享一些关于Python爬虫中的异常处理和网络请求优化的经验 不论你是初学者还是有一定经验的爬虫程序员 我相信这些实用的技巧和代码示例都能为你在爬取数据的过程中带来方便和效率 1 异常处理 保
  • MySQL查看、创建和删除索引的方法分享

    这篇文章主要介绍了MySQL查看 创建和删除索引的方法 结合实例形式较为详细的分析了MySQL中索引的作用 以及查看 创建及删除索引的相关实现技巧 具有一定参考借鉴价值 需要的朋友可以参考下 本文实例讲述了MySQL查看 创建和删除索引的方
  • STM32系统时钟超详解

    作者简介 嵌入式入坑者 与大家一起加油 希望文章能够帮助各位 个人主页 rivencode的个人主页 系列专栏 玩转STM32 保持学习 保持热爱 认真分享 一起进步 目录 一 什么是时钟 二 时钟树 1 HSE时钟 2 HSI时钟 3 L
  • Shell变量的设置规则

    1 变量设置规则 变量与变量内容以一个等号 myname LSX 等号两边不能直接接空格 myname LSX 或 myname L SX 都是错误 变量名称只能是英文字母与数字 但是开头字符不能是数字 2myname LSX 错误 2 双
  • 华为od机考真题-数据分类

    while 1 try c b nums list map int input split dp
  • C++11 新特性:模板别名

    C 11 新特性 模板别名 豆子 2012年5月22日 C 没有评论 参考文章 https blogs oracle com pcarlini entry template aliases 2002 年 ISO C 标准化组织就已经提出了模
  • Jmeter Springboot Redisson分布式锁并发订单操作(下单、取消单、完成单、加库存)

    Jmeter Springboot Redisson分布式锁并发订单操作 下单 取消单 完成单 加库存 涉及知识点 java springboot mybatis开发 redis分布式锁 Redisson客户端 Jmeter各种骚操作 用户
  • 多元线性回归&梯度下降法——多元线性回归

    多特征 当Y值的影响因素不是唯一时 采用多元线性回归模型 例子 梯度下降法 多元线性回归 import numpy as np from numpy import genfromtxt import matplotlib pyplot as
  • 时序预测

    时序预测 MATLAB实现DNN深度神经网络时间序列预测未来 多指标 多图输出 目录 时序预测 MATLAB实现DNN深度神经网络时间序列预测未来 多指标 多图输出 预测效果 基本介绍 模型结构 程序设计 学习总结 预测效果 lt
  • CentOS7.3下载,CentOS7.3 iso下载

    原网站 http man linuxde net download CentOS 7 3 当前位置 首页 CentOS CentOS7 3下载 CentOS7 3 iso下载 CentOS 7 3 是CentOS 7系列的第四个发行版本 官
  • linux设置pg库开机自启

    要在Linux系统上设置PostgreSQL数据库开机自启 可以按照以下步骤操作 打开终端并使用root权限登录系统 编辑 etc rc local 文件 sudo vi etc rc local 在文件的最后一行添加以下内容 su pos
  • Ubuntu 安装 Tensorflow-gpu 与 Keras

    为深度学习所用 博主预想在Ubuntu16 04上安装 显卡驱动 CUDA cuDNN Tensorflow gpu Keras PyCharm 参考了众多资料 最终成功将所有软件安装完毕 且能成功运行使用 该篇博客介绍了Tensorflo
  • matlab求二元函数极值算法_高等数学下册(部分)复习——知识点:多元函数微分方法及其应用...

    空间解析几何与向量代数的部分就不说了 比较简单 以几道例题练一练就差不多了 首先从第九章 多元函数微分方法及其应用说起 01 多元微分 理论 要学习多元 我们首先要从一元开始 一元的学会了 就能够类比得到多元的结论 在理论部分 首先要介绍一
  • WIN10系统MYSQL的下载与安装详细教程

    前两天ubuntu下安装mysql遇到了一些依赖问题 结果解决了半天 没解决好 还把我的系统搞坏了 小白破坏力好强 到现在我的ubuntu也没装好 电脑驱动的问题 联想小新310一装ubuntu 进去就卡 原来禁用原先的显卡驱动 可是 第二
  • win10 Enable developer Mode

    经过漫长的安装过程 win10终于装上了vs2015 rc 写个小程序试试 结果提示 根据提示打开 设置 更新 for developer 据说应该有这么个界面 但是这个界面根本出不来 直接闪退的说 翻 MSDN 终于翻出了解决方法 htt
  • ChatGPT启示录: 智能、推理的本质是什么?神经网络既是推理机,也是知识规则库?

    多种因素让人类对自身的智力产生了一种自信 毕竟这个世界上其他生物没有我们大脑发达 智力似乎是上天给人类的独有礼物 作为孩子的父母 老师说孩子不努力似乎是可以接受的 但是说自己娃娃笨是极其羞辱的 类似的 让很多人不能接受的是 机器人可以算得比
  • 学习笔记(三):Java中的List集合——ArrayList、LinkedList、Vector、Stack、CopyOnWriteArrayList

    目录 引言 一 List简介 二 常用List实现类 一 ArrayList 二 LinkedList 三 LinkedList和ArrayList的比较 三 其他List实现类 一 Vector 二 Stack 三 CopyOnWrite