java高级——Collection集合之List探索(包含ArrayList、LinkedList、Vector底层实现及区别,非常详细哦)

java高级——Collection集合之List探索

  • 前情提要
  • 文章介绍
  • 提前了解的知识点
    • 1. 数组
    • 2. 单向链表
    • 3. 双向链表
    • 4. 为什么单向链表使用的较多
    • 5. 线程安全和线程不安全的概念
  • ArrayList介绍
    • 1. 继承结构解析
      • 1.1 三个标志性接口
      • 1.2 AbstractList和AbstractCollection
    • 2. ArrayList底层代码实现
      • 2.1 五个常量的作用
      • 2.2 三个构造方法
      • 2.3 解析add方法,初步了解扩容机制
      • 2.4 解析扩容核心方法:grow()——1.5倍扩容
      • 2.5 ensureCapacity方法动态扩容
      • 2.6. subList()方法底层解析(重点)
      • 2.7. indexOf与contains方法
      • 2.8. fail-safe机制、modCount作用、checkForComodification方法
      • 2.9 两种迭代器iterator和listIterator
  • LinkedList介绍
    • 1. 继承结构介绍
      • 1.1 Queue接口
      • 1.2 Deque接口
      • 1.3 LinkedList和ArrayList的区别,如何选择使用哪个
  • Vector介绍
    • 1. 继承结构解析
    • 2. Vector和ArrayList的区别
      • 2.1. 扩容量不同(Vector为1倍,ArrayList为1.5倍)
      • 2. 2 Vector是线程安全的
    • 3 Collections工具类:synchronizedList()实现线程安全
    • 4 Vector存在的问题
    • 5 CopyOnWriteArrayList 写时复制策略
  • 总结
  • 下期预告

前情提要

   上一篇文章我们探索了一下String字符串在jvm中的实现,这篇文章来了解一下java中最重要的知识点之一,Collection。

java高级——String字符串探索(在jvm底层中如何实现,常量池中怎么查看)

文章介绍

在这里插入图片描述

   上图展现了java中容器的简化知识点,真实的继承图要更加复杂,上面只是列出了最重要和最常用的结构。本文主要的方向是Collection单列集合的List,而提起List,其中最常用的就是ArrayList,几乎占据了开发中80%的地位,本文将对ArrayList重点讲解,底层如何实现以及常用方法介绍。

提前了解的知识点

1. 数组

   数组是有序的元素序列,有以下优点:

  1. 数组是相同数据类型的元素的集合。
  2. 数组中各元素的存储是有先后顺序的,它们在内存中按照这个顺序连续存放到一起。内存地址(连续存储)
  3. 因为是随机访问,查找效率高

   缺点:

  1. 需要在申明时指定长度,无法动态扩容
  2. 增删数据效率较低,因为内存连续的原因,只要进行某个元素操作势必会带动其它元素的移动。
  3. 无法存储float或double等基本类型的数据。

在这里插入图片描述

   数组有着非常优秀的访问速度,因为内存连续的特性,访问速度出奇的快,但内存连续这也是一个致命的缺点,动态改变数组效率比较低。所以之后大多数人喜欢使用链表,也就是我们的List。

2. 单向链表

   链表是一种物理存储单元上非连续非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点组成,节点可以在运行时动态生成,节点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。总结为以下特点:

  1. 物理地址无序且不连续;
  2. 逻辑层面是一个有序的链路结构;
  3. 可以动态生成节点;

在这里插入图片描述

   上面的三个特点解决了数组需要预先知道数据长度的短板,且可以灵活的运用内存空间,实现内存的动态管理

   而单向链表属于链表的一种,其特点是链表的链接方向是单向的,链表的遍历要从头部开始顺序读取;结点构成,head指针指向第一个成为表头结点,终止于最后一个指向NULL的指针。特点如下:

  1. 因为是顺序读取,每次查找都需要从头开始遍历,速度稍慢;
  2. 操作数据不方便,如果是在中间添加元素,则要移动很多个元素;

3. 双向链表

   双向链表也叫双链表,是链表的一种,链表的每个数据结点中都有两个指针,分别指向直接后继直接前驱,从双向链表中的任意一个结点开始,都可以很快速地访问它的前驱结点和后继结点,链表结构的使用多数都是构造双向循环链表。特点如下:

  1. 因为有直接后继和直接前驱,也就是任何一个节点都知道它的上一个和下一个节点的地址,所以读取操作很快
  2. 操作数据比较快,每当改变一个节点时,只需要改变相邻的前驱节点和后继节点

在这里插入图片描述

参考:单向链表和双向链表

4. 为什么单向链表使用的较多

   上面说到双向链表怎么看都有优势,从操作上还是读取速度上,但是双向链表相对单向链表有两个指针,这就代表了存储空间的增大,而且维护成本较高,这就变相的印证了一句话,宁可慢点,也不能太麻烦哈哈哈。

5. 线程安全和线程不安全的概念

   所谓线程安全就是加锁机制,这个和数据库打过交道的同学比较熟悉,当多个线程同时操作一条数据时,如果不进行加锁,就会导致数据错误(A修改为了1,B修改为了2,最后A可能看到的是2而不是1),加锁之后,当不同线程操作同一条数据时,一定保证那一刻只有一个线程操作那条数据,操作结束后交给下一个线程。举一个现实生活中的例子,比如你去租房子,那个房子有很多人在看,你相中了这个房间,准备明天去看看,结果有人提前你一步交了定金,这就相当于加了一把锁,虽然那个人还没有签合同,但定金保证了这个房子别人暂时不能看了,这就是线程安全的概念。而线程不安全则是相反,用专业属于来讲就是违背了下面三个条件之一:

  1. 原子性:一个或多个线程操作 CPU 执行的过程中被中断,互斥性称为操作的原子性。
  2. 可见性:一个线程对共享变量的修改,其他线程不能立刻看到。
  3. 有序性:程序执行的顺序没有按照代码的先后顺序执行。

ArrayList介绍

1. 继承结构解析

在这里插入图片描述

1.1 三个标志性接口

  • RandomAccess:表示可以随机访问
  • Cloneable:可以克隆
  • Serializable:可序列化

   RandomAccess表示的是随机访问,这个接口点进去啥都没有,标志性接口作用就是赋予你一个标志,之后在操作的时候如果你有这个标志,可以进行特殊的操作,而RandomAccess就是随机访问的标志,因为ArrayList底层是用数组实现的,是动态数组的概念,所以有随机访问的特性

在这里插入图片描述

   关于RandomAssess接口简单说几个结论,Collections类中有相关的实现,目前我们只需要知道,有这个标识的走indexedBinarySearch,也就是通过下标寻找对应的元素,相反则是通过iteratorBinarySearch迭代器双向链表LinkedList使用迭代器相对快一些,详细了解看下面这篇。

RandomAccess详解

   Cloneable同样,是一个可以克隆的标志。

   Serializable表示可序列化,序列化说明一下,是将数据结构对象转换成一种可存储或可传输格式的过程。在序列化后,数据可以被写入文件发送到网络存储在数据库中,以便在需要时可以再次还原成原始的数据结构或对象。序列化的过程通常涉及将数据转换成字节流或类似的格式,使其能够在不同平台和编程语言之间进行传输和交换。

   说白了就是这玩意儿能在网络上传输和存储,我们应该用到过在接口调用时,直接将一个List作为传输的参数吧,或者包装在对象中,实际就是因为实现了这个接口。

1.2 AbstractList和AbstractCollection

在这里插入图片描述

   这两个类是ArrayList的父类,其实研究意义并没有那么大,里面的方法基本和ArrayList相差不大,可以理解为ArrayList的前身,祖宗,后面儿子更牛逼一些

2. ArrayList底层代码实现

在这里插入图片描述

2.1 五个常量的作用

   首先了解一下上面五个常量是什么意思。

  1. DEFAULT_CAPACITY:默认初始化容量,也就是new ArrayList的初始容量;
  2. EMPTY_ELEMENTDATA:空数组的实例,用于有参构造;
  3. DEFAULTCAPACITY_EMPTY_ELEMENTDATA:空数组的实例,用于无参构造;
  4. elementData:实际存储元素的对象,后续扩容也操作的这个对象;
  5. size:数组的长度;

   上面的五个常量唯一不好理解的就是第二和第三个,都是一个空数组,为什么要存在两个,除了一个是无参和有参的区别外,DEFAULTCAPACITY_EMPTY_ELEMENTDATA可以知道第一次add元素时需要的扩容数量。不着急,我们根据下面的几个构造方法就可以很清楚的了解了。

2.2 三个构造方法

    /*** 构造一个初始容量为 10 的空列表。*/public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}

   第一个无参构造,很清晰,只是给elementData赋值了一个空数组,长度为10,代码中看不到哪里的长度为10是吧,不用在意,最底层其实就是一个长度为10的数组。

    /*** 构造具有指定初始容量的空列表。Params: initialCapacity – * 列表的初始容量 抛出: IllegalArgumentException – 如果指定的初始容量为负数*/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);}}

   第二个是有参构造,参数为指定容量的大小,上面的代码也很好理解,如果initialCapacity大于0则new一个指定大小的数组赋值,为0则赋值空数组,小于0报错。

   一般在开发中,如果提前知道我们要操作的数组大小或者可以估计出大概的长度,建议使用这个有参构造,提前开辟出指定大小的ArrayList,后面就不用进行多次扩容了,可以提高代码的执行效率。

    /*** 构造一个列表,其中包含指定集合的元素,按集合的迭代器返回这些元素的顺序排列** @param c the collection whose elements are to be placed into this list* @throws NullPointerException if the specified collection is null*/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 {// replace with empty array.elementData = EMPTY_ELEMENTDATA;}}

   这是一个参数为Collection的有参构造,一般我们是不会这么使用的,只有在某些特殊情况需要利用反射机制操作数组才这么使用。上面的代码看起来也很简单,就是将Collection通过toArray方法进行转换,之后赋值,只不过加了一个c.getClass() == ArrayList.class的判断,不成立自己会拷贝一份数组,其实通过追踪,toArray方法其实就在ArrayList中,到最后执行的还是Arrays类中的copyOf方法,复制数组。

2.3 解析add方法,初步了解扩容机制

    public boolean add(E e) {
1       modCount++;
2       add(e, elementData, size);return true;}private void add(E e, Object[] elementData, int s) {
3        if (s == elementData.length)
4           elementData = grow();
5        elementData[s] = e;
6        size = s + 1;}

   上面是直接往数组中添加元素的方法,第一个方法是暴露给开发者调用的,第二个方法是一个重载方法,用于真正执行添加的方法,通过六个点进行分析。

  1. modCount++:这一行作用是记录我们对ArrayList操作了多少次,这里我们只需要知道干什么就行,具体什么作用会在本文最后进行详细讲解。

  2. add(e, elementData, size):调用真正的添加方法,e为添加的元素,elementData为ArrayList数组对象,size为数组的长度(注意,如果是无参构造初始化,则size为0)。

  3. if (s == elementData.length):这个判断就是看执行扩容还是不执行,一旦size的大小和集合中元素长度一致,就代表数组的容量不够了,需要进行扩容了。

  4. grow():扩容的核心方法。

  5. elementData[s] = e:扩容后给最后给数组的最后放入我们要添加的元素。

  6. size = s + 1:集合中元素长度加1。

   看上面的六步得出,其实只有第四步的扩容是最核心的,添加的元素就是在扩容后放到了数组的最后一个位置,接下来我们对grow方法进行跟踪。

2.4 解析扩容核心方法:grow()——1.5倍扩容

    private Object[] grow() {return grow(size + 1);}/*** 增加容量以确保它至少可以容纳最小容量参数指定的元素数** @param minCapacity the desired minimum capacity* @throws OutOfMemoryError if minCapacity is less than zero*/private Object[] grow(int minCapacity) {int oldCapacity = elementData.length;if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
1           int newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity - oldCapacity, /* minimum growth */oldCapacity >> 1           /* preferred growth */);
2           return elementData = Arrays.copyOf(elementData, newCapacity);} else {
3           return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];}}
  1. 获取扩容的大小;
    public static int newLength(int oldLength, int minGrowth, int prefGrowth) {int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflowif (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {return prefLength;} else {// put code cold in a separate methodreturn hugeLength(oldLength, minGrowth);}}

   上面是ArraysSupport.newLength方法,这个方法就是用来获取扩容长度,核心就是将最小增长量minGrowth准备扩容的长度prefGrowth相加获取扩容后长度prefLength,下面的判断实际是为了防止数组长度超过最大值2147483639

   了解这个方法后我们就要知道要干什么了,就是计算准备扩容的大小prefGrowth,回到最开始的grow方法中,它传入的参数是oldCapacity >> 1 ,而oldCapacity是我们原始数组elementData的长度,假设我们是无参构造的集合,那这个值就是10,那10 >> 1是多少呢?

注意:>> 是位运算符,可以理解为除法,是编译原理中常见的考点,这里指的是向右移动一位,10的源码是00001010,右移一位就是00000101,也就是5,相当于除2。

   那么扩容的大小计算出来了,加上原始的大小10,最后我们要扩容到15的大小,那也就是多少倍,1.5倍,这是ArrayList中扩容的结论,1.5倍扩容,这下知道怎么来的了吧!

  1. 根据扩容后大小获取新数组
    @IntrinsicCandidatepublic static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {@SuppressWarnings("unchecked")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;}

   上面是copyOf最终执行的方法,实际就是创建一个创建一个新长度的数组,然后将旧的数组内容复制进去,不要纠结System.arraycopy方法,这是一个native方法,用C实现的,你也看不到哈哈哈。

  1. 第三步可以理解为创建一个初始化长度为10的数组,也就是初始化扩容。

2.5 ensureCapacity方法动态扩容

    public void ensureCapacity(int minCapacity) {if (minCapacity > elementData.length&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA&& minCapacity <= DEFAULT_CAPACITY)) {modCount++;grow(minCapacity);}}

   看代码也没啥,就是给ArrayList进行执行大小的扩容,那有人就疑惑了,底层有自动扩容的机制,要这个方法有啥用呢?

   存在即合理,这个方法是在我们初始化完ArrayList后进行扩容操作,最开始不是说构造方法可以指定大小来优化效率嘛,如果我们最开始不知道到底要存多少个,那就不优化效率了吗,没办法了吗?那肯定不行,这个方法不就有作用了吗。

   还有,自动扩容机制是1.5倍,如果你有个几千几万条数据,要是一直这么扩容下去相对肯定慢吧,我直接一次扩容个几千不香吗。

2.6. subList()方法底层解析(重点)

看这儿看这儿,这个方法慎用慎用,不然会导致很多bug的!!!

   直接说几点结论:

  1. subList方法并没有创建一个新的List,而是使用了原List的视图,这个视图使用内部类SubList表示。
    在这里插入图片描述
  2. 如果对父List或子List中一个进行非线性操作(只改变元素的值,不改变集合的大小),两者都会改变
  3. 如果对子List进行非线性操作(改变集合大小如添加删除元素),父List会收到影响
  4. 如果对父List进行操作后,在操作子List(此时子List已经存在了),会报错ConcurrentModificationException
    在这里插入图片描述

   在使用subList的时候要注意,一旦操作不当就会导致报错,报错是小事,就怕不报错,你在subList中添加了一个元素,最后你返回了原数组,导致多了一条记录,误导了用户,这就严重了。所以一般在截取后都是新创建一个集合的。

参考:为什么阿里巴巴要求谨慎使用ArrayList中的subList方法

2.7. indexOf与contains方法

public boolean contains(Object o) {return indexOf(o) >= 0;}public int indexOf(Object o) {return indexOfRange(o, 0, size);}int indexOfRange(Object o, int start, int end) {Object[] es = elementData;if (o == null) {for (int i = start; i < end; i++) {if (es[i] == null) {return i;}}} else {for (int i = start; i < end; i++) {if (o.equals(es[i])) {return i;}}}return -1;}

   contains方法实际调用的也是indexOf,所以我们看indexOf方法就行。

   根据上面的代码可以得出,就是循环elementData中所有元素,如果o.equals某个元素则返回对应的下标,只不过我们要注意的是,对象o是可以为null的,一般参数给null会报错,这里可不会,所以在使用前一定要注意不能为null。

   还有一点是equals方法,注意哈,这里的equals用的是Object类中的原始方法,只比较对象的内存地址,这个需求不能满足我们,实际开发中并不能保证两个对象的内存地址一样,但数据是一样的,这时候我们常用的手段是转为map,在map中找,或者使用stream流比较也很常见,不过还有一种更快的方法,既然我们知道这是通过equals方法比较的,那么我们能不能重写对象的equals方法呢?答案是可以,看下面的例子。

在这里插入图片描述

   只要重写了equals方法,就可以满足我们在开发中的一些需求了,虽然stream流很方便,单从效率来讲,肯定最原始的是最快的。

   顺带说一下,还有一个方法是lastIndexOf,和上面同理,实现方式稍有区别,也是for循环,只不过是倒序,也就是倒着循环,我最开始是以为定义一个变量,不断地赋值下标,最后循环结束返回呢,结果还是年轻了,要不说这是源码呢,怎么效率高怎么来。

在这里插入图片描述

2.8. fail-safe机制、modCount作用、checkForComodification方法

   fail-safe机制很常见,我们在开发中也经常使用,但就是不知道专业的名词。

public int divide(int divisor,int dividend){if(dividend == 0){throw new RuntimeException("dividend can't be null");}return divisor/dividend;
}

   很简单,就是在某些执行逻辑前进行检查,如果有问题则进行报错,避免执行一些复杂逻辑或者说不应该执行的代码,导致一些不必要的结果。

   modCount我们在之前讲过,当集合进行add或者remove操作时,modCount值会加一,到底有什么作用呢?其实就是给fail-safe机制使用的,而具体的实现方法就是checkForComodification,来我们简单看下。

    private void checkForComodification(final int expectedModCount) {if (modCount != expectedModCount) {throw new ConcurrentModificationException();}}

   这个方法在ArrayList源码中很多方法都有,比如迭代器、equals方法等等,具体作用就是为了防止多线程情况下操作同一个集合而导致的一些错误,我们常见的触发场景是在增强for循环中对集合进行add和remove操作时,就会触发这个错误,但是在普通for循环是不会有这个错误的哈,因为普通for循环只是在一个循环中多次操作同一个集合,每一次操作它的modCount值都是会同步的,而增强for循环(forEach)是基于迭代器的,具体看下面这篇文章。

一不小心就踩坑的fail-fast是个什么鬼

   上面这篇文章对ArrayList的迭代器和上面所说的问题有详细的介绍,很清晰,想要了解的伙伴可以看看,或者记住一点。

   要再循环中操作ArrayList的增删,就用迭代器,当然普通for循环也可以(经过测试),不过规范还是在迭代器中实现。

        List<User> userList = new ArrayList<>();User user1 = new User("张三", "1");User user2 = new User("李四", "0");User user3 = new User("王五", "-1");userList.add(user1); userList.add(user2); userList.add(user3);// 会报错// for (User user : userList) {//     if (user.getName().equals("李四")) {//         userList.add(new User());//     }// }int size = 0;for (int i = 0; i < userList.size(); i++) {User user = userList.get(i);if (user.getName().equals("李四")) {userList.add(new User("tese", "0"));}size += 1;}System.out.println("size = " + size);   // 实际循环为4次System.out.println("userList = " + userList);

2.9 两种迭代器iterator和listIterator

   在ArrayList中存在两种迭代器,首先说一下迭代器的概念,这是为了集合而专门设计的一个类,就是说所有的集合都会实现这个接口,无论是List还是Set还是Map,都可以使用迭代器遍历操作,我们可以理解为专为集合设计的一个for循环工具,为什么要使用迭代器,迭代器有什么好处呢?

   迭代器除了是一个规范外,还有我们上面所说的2.8,有很不错的fail-safe机制,里面的checkForComodification方法提供的保护作用。具体我们在上面已经说过,不懂的伙伴可以再看一看源码。接下来我们介绍一下这两种迭代器的区别和相同点:

在这里插入图片描述

   上面这张图说明了一个问题,两个方法最终返回的都是一个Itr对象,只是ListItr是继承自Itr,而在java中,继承一定能说明一个问题,子类肯定有比父类更多的功能,这里也是一样的,来瞅一下两个类中的方法:

Iterator迭代器:

  1. hasNext():如果迭代器指向位置后面还有元素,则返回 true,否则返回false

  2. next():返回集合中Iterator指向位置后面的元素

  3. remove():删除集合中Iterator指向位置后面的元素

ListIterator迭代器:

  1. hasNext():如果迭代器指向位置后面还有元素,则返回 true,否则返回false

  2. next():返回集合中Iterator指向位置后面的元素

  3. remove():删除集合中Iterator指向位置后面的元素

  4. add(E e): 将指定的元素插入列表,插入位置为迭代器当前位置之前

  5. hasPrevious():如果以逆向遍历列表,列表迭代器前面还有元素,则返回 true,否则返回false

  6. nextIndex():返回列表中ListIterator所需位置后面元素的索引

  7. previous():返回列表中ListIterator指向位置前面的元素

  8. previousIndex():返回列表中ListIterator所需位置前面元素的索引

  9. set(E e):从列表中将next()或previous()返回的最后一个元素返回的最后一个元素更改为指定元素e

// 首先有个ArrayList
ArrayList<Object> list = new ArrayList<>();
// 然后获取到迭代器
Iterator<Object> iterator = list.iterator();
// 再开始遍历ArrayList,使用hasNext()判断ArrayList中是否有下一个元素
while (iterator.hasNext()){// 如果ArrayList中有下一个元素,就使用next()方法获取下一个元素Object e = iterator.next();if ("条件".equals(e)){// 如果元素e满足某种条件就使用remove()删除该元素iterator.remove();}
}

   上面就是我们常用的迭代器,不过listIterator明显更加强大,有6个新增的方法,分别能实现倒序遍历获取前一个或者后一个元素的下标新增元素或修改元素,这在有些场景下是非常有用的,毕竟list集合下标是我们经常使用的。

参考:ArrayList迭代器       JAVA中ListIterator和Iterator详解与辨析

LinkedList介绍

   因为LinkedList和ArrayList在源码上差异并不大,所以这里不再分析源码,有兴趣的可以看看,主要是对继承结构中的两个接口进行分析,得出LinkedList的优势和缺点。

1. 继承结构介绍

在这里插入图片描述

   看图发现有两个没有在ArrayList中出现过的接口,这两个接口有什么作用呢?

   首先Deque接口是继承Queue接口,所以Deque接口更有研究价值,不过我们先来了解一下Queue祖宗。

1.1 Queue接口

   这是一个队列接口,队列有先进先出的特点,所以这个接口中就是为了这个特点而定义了几个方法(这儿借用一下别的博客图片),这个其实是为了Deque而准备的,有的人就疑惑了,为啥ArrayList没有实现,LinkedList是双向链表哦,而ArrayList是基于数组实现的。

在这里插入图片描述

1.2 Deque接口

   这是一个双端队列接口,所谓双端队列就是队列头和队列尾都可以进出,而上面所说的是普通队列,有了这个概念我们能得出什么结论呢?为什么说LinkedListList的添加和删除操作速度快呢?这个接口给了我们完美的答案。

在这里插入图片描述

参考:Java双端队列Deque使用详解

1.3 LinkedList和ArrayList的区别,如何选择使用哪个

区别:

  1. LinkedList无法随机读取,速度相对于ArrayList较慢;
  2. LinkedList基于双向链表,增加和删除元素更快

   有了上面两个区别后,在实际开发中我们应该如何选择呢?一般在频繁读取操作时选用ArrayList频繁操作中选择LinkedList,可是实际我们大多数还是使用ArrayList,也不会为了那点时间去用LinedList,不过在几个场景还是建议用LinkedList。

场景1:导出Excel时,这时候势必要频繁的操作集合(对于使用java原生API的开发者)。
场景2:对于数据量很大时,我们需要对集合进行删除操作,使用LinkedList。
场景3:当需要不断地new对象,后续进行赋值操作后放入集合时,使用LinkedList。

   虽然开发中我们不太在意这个,可当数据量上来时这个时间差距就会被放大,千万不要小瞧,在一个大的系统中,效率一直是被人诟病的问题,而往往就是这些小的点会慢慢拉低整个系统的性能。

Vector介绍

1. 继承结构解析

在这里插入图片描述

   上图看着很熟悉对吧,Vector和ArrayList的继承结构基本相同的,只是在实现上Vector是线程安全的,而ArrayList则不是,所以重点还是对线程安全这个点进行讲解。

2. Vector和ArrayList的区别

2.1. 扩容量不同(Vector为1倍,ArrayList为1.5倍)

在这里插入图片描述

   注意看上面这段代码,和ArrayList也很相似,不同点就在于ArraysSuport.newLength这个方法最后一个参数扩容的量是elementData的长度,也就是一倍的增长量,而capacityIncrement这个值是在构造方法复制的,当你new的时候不赋值,那就默认是1倍扩容

2. 2 Vector是线程安全的

   线程安全的概念我们在最开始就讲到了,直接通过代码验证一下ArrayList为什么不是线程安全的。

    public static void main(String[] args) {List<String> list = new ArrayList<>();for (int i = 0; i < 30; i++) {String threadName = "threadName" + i;new Thread(() -> {renderList(threadName, list);}, threadName).start();}}public static void renderList(String threadName, List<String> list) {for (int i = 0; i < 50; i++) {list.add(threadName + String.valueOf(i));if (i == 20) {System.out.println("list = " + list);}}}

在这里插入图片描述

   上面的代码和运行结果说明ArrayList确实不是线程安全的,这种例子呢网上也有很多,不过我要说的是这种写法在实际开发中很少出现,上面的例子无非就是模拟了一下多个线程调用同一个方法,而不同线程操作的都是1个List集合,所以肯定会出现报错,但是在实际开发中我们会这么用吗?

   答案是肯定不会这么用的,每次肯定都会new一个List,所以线程不安全的问题也很少出现,开发中我们大多数都是对数据库进行锁的机制。这里为了搞懂这个概念和Vector为啥是线程安全的,所以这么举例子,大家也不要太纠结为什么这么写,毕竟我也确实纠结了好几天。

在这里插入图片描述

   我们将ArrayList换为了Vector,程序就可以正常运行了,为什么呢?因为Vector在底层是用了synchronized关键字,也就是加锁机制,所以也不会出现报错。

3 Collections工具类:synchronizedList()实现线程安全

在这里插入图片描述

   其实除了Vector外Collections中也提供了一个方法能实现线程安全,那么使用的话也很简单。

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

   同时也对Map、Set等都有类似的方法。

4 Vector存在的问题

   有一点要注意,Vector的锁只是会限制同一种操作不会出现问题,也就是如果你的多个线程都只是新增操作那没啥事儿,如果不同线程中会出现一会儿添加一个,一会儿删除一个,那保不齐也会出问题,可能不报错,但数据最终不是你期望的结果。而且速度很慢。

  • 速度慢
  • 在进行复杂操作时不能保证数据的一致性

   为了解决Vector都不能解决的问题,就出现了一个新的类,既能加快速度,又能完全保证数据的一致性。

5 CopyOnWriteArrayList 写时复制策略

参考:集合中线程安全和不安全

List<String> list = new CopyOnWriteArrayList<>();

   写时复制:我们要向一个文件中添加新数据时,先将原来文件拷贝一份,然后在这个拷贝文件上进行添加;而此时如果有别人读取数据,还是从原文件读取;添加数据完成后,再用这个拷贝文件替换掉原来的文件。这样做的好处是,读写分离,写的是拷贝文件,读的是原文件,可以支持多线程并发读取,而不需要加锁

在这里插入图片描述
   volatile关键字可以保证数据的可见性。

   写时复制是一个解决并发很好的策略,有的人可能会疑惑,复制一份数据操作不是也比较慢吗?也不无道理,肯定有一定的影响,但是相对于加锁是很快了,当线程过多时,排队势必是最慢的,虽然我们这里复制了很多份数据,但是并发呀,同时操作呀,速度上肯定有优势。

   但是,又有一个但是,这种方案也要慎用,如果你的线程并发太大,而且操作的数据比较大的时候(比如集合中动不动就几千几万条),这个操作有可能影响系统的整体性能,毕竟复制的数据不在少数,弄不好系统崩了也不是不可能,但加锁倒是不会,就是这一个功能会受到影响,具体怎么选择还是要根据实际情况来看。当然,如果你的系统内存足够大,而且有分流和集群的策略,那问题不大,并发也不会有太大影响。

总结

   本文是对集合中List家族进行了整体的讲解,重点还是基于源码的分析和一些重要优化策略,在使用方面没有过多提及,后续会专门说一下常用的使用方法,本文参考了众多博客,同时也有自己的看法,经过了大量的佐证,如果有哪里不对的地方,大家可以在评论区提及。

下期预告

   下一篇文章将围绕Arrays工具类说明,期待下次见面。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/19109.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

民国漫画杂志《时代漫画》第32期.PDF

时代漫画32.PDF: https://url03.ctfile.com/f/1779803-1248635561-0ae98a?p9586 (访问密码: 9586) 《时代漫画》的杂志在1934年诞生了&#xff0c;截止1937年6月战争来临被迫停刊共发行了39期。 ps: 资源来源网络!

去除字符串中的空格和特殊字符

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 用户在输入数据时&#xff0c;可能会无意中输入多余的空格&#xff0c;或在一些情况下&#xff0c;字符串前后不允许出现空格和特殊字符&#xff0c;…

Beego 使用教程 7:Web 文件上传下载和错误处理

beego 是一个用于Go编程语言的开源、高性能的 web 框架 beego 被用于在Go语言中企业应用程序的快速开发&#xff0c;包括RESTful API、web应用程序和后端服务。它的灵感来源于Tornado&#xff0c; Sinatra 和 Flask beego 官网&#xff1a;http://beego.gocn.vip/ 上面的 bee…

「清新题精讲」Skiers

更好的阅读体验 Skiers Description 给定 n n n 个点的有向无环平面图&#xff0c;求最少多少条从 1 1 1 到 n n n 的路径能覆盖原图的所有边&#xff1f; 1 ≤ n ≤ 5 1 0 3 1\le n\le 5\times10^3 1≤n≤5103 Solution 考虑从 1 1 1 到 n n n 的路径其实是边的链覆…

如何让你的网站能通过域名访问

背景 当我们租一台云服务器&#xff0c;并在上面运行了一个Web服务&#xff0c;我们可以使用云服务器的公网IP地址进行访问&#xff0c;如下&#xff1a; 本文主要记录如何 实现让自己的网站可以通过域名访问。 买域名 可以登录腾讯云等主流公有云平台的&#xff0c;购买域名…

设计模式21——命令模式

写文章的初心主要是用来帮助自己快速的回忆这个模式该怎么用&#xff0c;主要是下面的UML图可以起到大作用&#xff0c;在你学习过一遍以后可能会遗忘&#xff0c;忘记了不要紧&#xff0c;只要看一眼UML图就能想起来了。同时也请大家多多指教。 命令模式&#xff08;Command&…

失落的方舟 命运方舟台服怎么下载游戏客户端 游戏账号怎么注册

《失落的方舟》&#xff08;Lost Ark&#xff09;是韩国Smilegate公司精心打造的一款大型多人在线角色扮演游戏&#xff08;MMORPG&#xff09;&#xff0c;以其精美的画面、沉浸式的剧情、类似动作游戏的战斗体验和广阔的开放世界设定&#xff0c;自面世以来便深受全球玩家喜爱…

计算机毕业设计 | SpringBoot+vue仓库管理系统(附源码)

1&#xff0c;绪论 1.1 项目背景 随着电子计算机技术和信息网络技术的发明和应用&#xff0c;使着人类社会从工业经济时代向知识经济时代发展。在这个知识经济时代里&#xff0c;仓库管理系统将会成为企业生产以及运作不可缺少的管理工具。这个仓库管理系统是由&#xff1a;一…

一款高级管理控制面板主题!【送源码】

AdminLTE是一个完全响应的管理模板。基于Bootstrap5框架和JavaScript插件。高度可定制&#xff0c;易于使用。适用于从小型移动设备到大型桌面的多种屏幕分辨率。AdminLTE 是一个基于Bootstrap 3.x的免费高级管理控制面板主题。 https://github.com/almasaeed2010/AdminLTE —…

操作系统真象还原:完善MBR

第3章-完善MBR 这是一个网站有所有小节的代码实现&#xff0c;同时也包含了Bochs等文件 编译器给程序中各符号&#xff08;变量名或函数名等&#xff09;分配的地址&#xff0c;就是各符号相对于文件开头的偏移量 。 section 称为节&#xff0c;在有的编译器中&#xff0c;同…

STM32的时钟介绍

目录 前言1. 简介1.1 时钟是用来做什么的1.2 时钟产生的方式 2. 时钟树的组成2.1 时钟源2.1.1 内部时钟2.1.2 外部时钟 2.2 PLL锁相环2.3 SYSCLK2.4 AHB和HCLK2.5 APB和PCLK2.6 总结 3. STM32时钟的如何进行工作4.我的疑问4.1 使用MSI和HSI有什么区别吗&#xff1f;4.2 MSI的频…

Linux系统编程(五)多线程

目录 一、基本知识点二、线程的编译三、 线程相关函数1. 线程的创建2. 线程的退出3. 线程的等待补充 四、综合举例 一、基本知识点 线程&#xff08;Thread&#xff09;是操作系统能够进行运算调度的最小单位。它被包含在进程之中&#xff0c;是进程中的实际运作单位。一个标准…

【4.vi编辑器使用(下)】

一、vi编辑器的光标移动 二、vi编辑器查找命令 1、命令&#xff1a;:/string 查找字符串 n&#xff1a;继续查找 N&#xff1a;反向继续查找 /^the 查找以the开头的行 /end 查找以 查找以 查找以结尾的行 三、vi编辑器替换命令 1、语法: : s[范围,范围]str1/str2[g] g表示全…

可视化大屏:随意堆数据,错!要主次分明、重点突出,动静结合。

可视化大屏是一种展示数据的方式&#xff0c;它的设计应该遵循一些原则&#xff0c;以确保信息的传递和理解效果最佳。以下是一些关键点&#xff0c;可以帮助设计出主次分明、重点突出、动静结合的可视化大屏&#xff1a; 定义目标和重点&#xff1a; 在开始设计可视化大屏之前…

C语言数据结构堆排序、向上调整和向下调整的时间复杂度的计算、TopK问题等的介绍

文章目录 前言一、堆排序1. 排升序&#xff08;1&#xff09;. 建堆&#xff08;2&#xff09;. 排序 2. 拍降序&#xff08;1&#xff09;. 建堆&#xff08;2&#xff09;. 排序 二、建堆时间复杂度的计算1. 向上调整时间复杂度2. 向下调整时间复杂度 三、TopK问题总结 前言 …

Java事务入门:从基础概念到初步实践

Java事务入门&#xff1a;从基础概念到初步实践 引言1. Java事务基础概念1.1 什么是事务&#xff1f;1.2 为什么需要事务&#xff1f; 2. Java事务管理2.1 JDBC 的事务管理2.2 Spring 事务管理2.2.1 Spring JDBC2.2.1.1 添加 Spring 配置2.2.1.2 添加业务代码并测试验证 2.2.2…

43-3 应急响应 - WebShell查杀工具

一、WebShell 简介 WebShell是一种以asp、php、jsp等网页文件形式存在的代码执行环境,通常用于网站管理、服务器管理和权限管理等操作。然而,如果被入侵者利用,它也可以用于控制网站服务器。具有完整功能的WebShell通常被称为"大马",而功能简单的则称为"小马…

双指针+前缀和,蓝桥云课 近似gcd

一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 0近似gcd - 蓝桥云课 (lanqiao.cn) 二、解题报告 1、思路分析 考虑近似gcd的子数组的特点&#xff1a;不为g的倍数的数字个数小于等于1 我们用前缀和pre[]来存储不为g的倍数的数字个数 那么枚举左端点l&a…

数据结构(八)二叉树、哈希查找

文章目录 一、树&#xff08;一&#xff09;概念1. 前序遍历&#xff1a;根左右2. 中序遍历&#xff1a;左根右3. 后序遍历&#xff1a;左右根4. 层序遍历&#xff1a;需要借助队列实现 &#xff08;二&#xff09;代码实现&#xff1a;二叉树1. 结构体定义2. 创建二叉树1. 注意…

LED显示屏模组七大参数

LED模组是LED显示屏的核心组件&#xff0c;它包含LED线路板和外壳&#xff0c;将LED灯珠按照特定规则排列并封装&#xff0c;通常还会进行防水处理。随着LED显示屏行业的发展及其广泛应用&#xff0c;LED模组的功能和作用变得愈加重要。那么&#xff0c;LED模组的七大参数是什么…