集合的学习

集合

集合概述

Java集合分为Collections和Map接口

其中,Collections用来存储单一元素,常见的有list(有序,可重复),set(无序,不可重复),queue(有序,可重复)

list分为arraylist,linked list,vector

Map(通过key-value来进行快速的搜索,key是无序的,不可重复的,value是无序的,可重复的)则用来存放键值对,常见的有hashmap,hashset ,treemap,linkedhashmap

为什么要使用集合?

当我们需要存储一组数据类型相同的数据时,数组时最常用的数据类型之一,但是,数组也有着许多的不方便,在实际的使用中,数据的类型多种多样且数量并不确定。这个时候,与数组相比,集合提供了更加灵活的,更加高效的办法来存储多个数据对象,相较于数组而言,集合具有可以动态扩容,支持泛型,具有内建算法的优点,所以我们集合来进行更加高效的开发

List

ArrayList 和 Array(数组)的区别?

arraylist内部基于动态数组实现,array数组是静态的

arraylist不需要指定数组的长度,会根据内部存储的元素进行动态的扩容和缩容,而array需要指定长度,之后无法改变 数组的大小

arraylist允许你使用泛型来确保类型安全,而数组却不可以

arraylist中只允许存储对象,不允许存储基本的数据类型,如果要存储基本类型,那么就需要使用其对应的包装类,而array既可以存储基本类型,也可以存储对象类型

arraylist中存在一些内建的算法,比如判断集合是否为空,增加,删除,改变等,array只是一个固定长度的数组,智能按照下标访问其中的元素,不具备动态的添加,删除元素这些操作

ArrayList可以添加null值吗

可以添加,但是不推荐,因为这样的值没有意义

ArrayList 与 LinkedList 区别?

线程安全:都是不保证线程安全的

底层的数据结构:arraylist是数组,而linkedlist是双向链表

插入和删除是否受到元素位置的影响:

是否支持快速随机访问:

arraylist支持,因为他的底层是数组,可以通过下标来进行快速的访问,但是linkedlist是链表,他的数据存储在内存中不连续,只能通过指针来进行访问,所以不能快速随机访问。

内存占用:

arraylist是他在每次动态扩容之后都会预留一部分空间,而linkedlist则是因为他的每个数组所需要占用更大的数据,因为要存在数据和前后节点的指针

而在实际的使用中 ,我们基本上不使用linkedlist,因为需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且,性能通常会更好

CopyOnWriteArrayList

在很多时候,因为读操作不会对原有的数据进行改变,因此,对于每一次的读操作进行加锁都是一种浪费,比如Vector,相当于给整个对象加了一把大锁,很多操作都需要获取锁资源,这是一种资源的浪费,而CopyOnWriteArrayList这个类,他既是一个线程安全的类,性能也比vector要好,他的设计思路优点类似于读写锁,读读不互斥,读写互斥,写写互斥,但是更加优秀的一点是,对于读写也不完全互斥。

copy---on---write :写时复制 简称COW

顾名思义:他的意思时写的时候复制,当我们需要对数据进行修改的时候,我们不会直接修改原数组,而是先创建原数组的副本,对副本数组进行修改,修改之后在将副本数组赋值回去,他对于读方法没有加限制,对于写操作加入了可重入锁,来确定修改数据时的线程安全,可以看出,这样特别适合读多写少的情况

这是更加准确的描述:

写入时复制(英语:Copy-on-write,简称 COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。


但是,他也有几个缺点:

1.因为每次进行修改,都需要创建副本数组,会占用额外的内存空间

2.当一个请求在修改数组的时候,还没有将修改后的数组写回去的时候,其他请求无法看到修改的数据,这可能会导致一定的数据不一致性

在Java关于集合的面试中,针对list的知识主要集中在以下问题中

讲一下Java里list的几种实现方式,几种实现有什么不同?

在Java中,list的实现方式常见的有arraylist,linkedlist,vector

arraylist的底层是由数组实现的,它可以进行动态扩容,每次基于当前结合的容量扩容1.5倍,他是线程不安全的

linkedlist的底层是由双向链表实现的,他是线程不安全的

vector也是动态扩容的数组,他是线程安全的,但是效率不高,现在很少使用

list可以一边遍历元素一边修改元素吗?

对于list的遍历,一般有以下三种方式,

通过for循环遍历,这样的遍历方式可以一边遍历一边修改元素

通过迭代器遍历,这样的遍历方式可以通过remove方法来删除元素,但是如果想要修改元素的值,对于不可变的对象,需要使用ListIterator中的set方法,不可以使用list的set方法,否则会报异常

通过foreach遍历,他的底层还是使用了迭代器遍历,它不可以修改,因为可能会破坏迭代器的结构

list如何快速删除某个指定下标的元素?

对于arraylist,通过索引的方式删除,删除尾部元素最快,删除其他位置的元素,复杂度为0n,因为其他元素还要移动

对于linkedlist,他需要先通过遍历,找到需要删除的元素,然后修改前后节点对应的指针,不过,删除头节点和尾节点的元素最快

arraylist和linkedlist的区别,那个集合是线程安全的?

首先,底层数据结构不同,arraylist是数组,linkedlist是双向链表

查询元素和删除元素效率不同,arraylist通过索引查找,速度更快,对于删除元素,arraylist删除尾部元素是01,其他元素是0n,链表删除头尾是01,其他的是0n

空间占用上,数组需要一串连续的内存空间,对空间的要求相对较高

使用场景不同,arratlist适合多读少写的场景,linkedlist适合中间插入和删除的场景

他们都是线程不安全的

arraylist和vector有什么区别?

他们最大的区别在于线程是否安全

arraaylist他是线程不安全的,而vector他是线程安全的,他的大部分方法都加了synchrnized关键字。

而因为在线程安全的不同,二者在性能上也有所不同,vecto的效率要低于arraylist

在对于大小的扩容上,arraylist默认的扩容是1.5倍,而vector默认的扩容是2倍,除此之外,vector可以指扩容因子,这是arraylist所不具有的。

arraylist线程安全吗?有什么办法可以把arraylist变得线程安全?

arraylist他是线程不安全的,把他变安全的方法有几种:

1.使用collections类中的synchronizedList方法可以把arraylist变得安全

2.使用copyonwritearraylist类来替换arraylist类,他是一个线程安全的list类的实现

3.使用vector类来替换arraylist类

为什么arraylist是线程不安全的?具体来说是哪里线程不安全?

arraylist的底层是一个动态扩容的数组,他之所以线程不安全是因为他的所有方法都没有加任何线程同步锁,也没有并发控制机制,所以他是线程不安全的。

具体来说,会出现以下三个问题:

java 复制代码
// 简化的 add 核心逻辑
public boolean add(E e) {
    // 步骤1:检查数组容量,不够则扩容
    ensureCapacityInternal(size + 1);
    // 步骤2:把元素放入数组的 size 位置,然后 size+1
    elementData[size++] = e;
    return true;
}

1.数据丢失:

当线程a和线程b同时添加时,两个线程都走到步骤二的时候,可能会读取到同一个size值,之后添加元素的时候,线程a添加之后,线程b添加就会覆盖线程a的值,这就是数据丢失

2.索引越界

假设初始 数组大小是10,size=9,线程a获取到size=9,准备添加元素,此时线程b也来获取到size=9,然后线程a添加成功,size++,线程b读取到的还是之前的size=9,然后b也开始添加,那么此时size就会加到11,进行访问时就会 出现数组越界

ArrayList的扩容机制说一下

1.计算新的容量,一般默认扩大至之前的额1.5倍,判断有没有超过最大容量限制。

2.创建新的数组,根据刚才计算的新容量来创建新数组

3.将原数组里的元素赋值到新的数组

4.将引用指向新的数组

CopyonWriteArraylist是如何实现线程安全的

CopyOnWriteArrayList底层使用一个被volatile关键字修饰的数组,保证线程对当前数组改变后,其他的数组可以及时的感知到

在写入操作时,加了一把互斥锁ReentrantLock以保证线程安全。

在写入新的元素的时候,将旧数组拷贝一份然后长度加1 ,将旧数组的元素放到新数组里,新元素放到最后,然后引用指向新的数组

List<>里面填基本数据类型为什么会报错?

首先是因为泛型的类擦除机制,Java泛型在被编译后会被擦除为Object类型,而Object类只能接受应用类型

其次是历史原因 ,Java 最初设计时基本数据类型和引用类型是严格区分的,泛型是后期(JDK 1.5)才引入的特性,为了兼容已有的类型系统,选择只支持引用类型

Set

Set的排序

Java里set本身默认不保证有序,如果想实现set的有序,核心是选择带有排序特性的set或者把set转换为有序的结构

第一点,对于set中的treeset,底层数据结构是红黑树,支持自然排序(元素实现comparable)和自定义comparator排序

第二点,

Comparable 接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序

Comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序

一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法

无序性和不可重复性意味着什么?

无序性意味着数据在存储的时候,并不是按照底层数组的索引顺序进行添加,而是通过计算hash值来进行存储的

不可重复性则意味着添加的元素按照equals()判断时,需要重写equals和hashcode方法

hashset linkedhashlist treeset三者的区别

三者最主要的区别时他们的底层数据结构不同

hashset的底层结构是哈希表(基于hashmap实现的),用于不需要保证元素插入和取出的顺序的场景

linkedhashlist的底层结构是hash表和链表,用于需要保证元素插入和取出符合FIFO的场景

FIFO:先进先出

treeset的底层结构是红黑树,元素是有序的,用于支持对元素进行自定义排序的场景

Map

ConcurrentHashmap和Hashmap

HashMap

基本特点:双列集合,键不可以重复,值可以重复。只有一个键可以为null

数据结构:

HashMap数据结构:

1.默认长度为16的Hash表(数组)

2.链表

3.红黑树

源码:

复制代码

重点参数:

哈希表长度:默认是16

每次扩容是当前长度的2倍(16---32---64)

哈希表负载因子为0.75 最多使用当前长度的四分之三

哈希表是不会希装满的,装满会影响查询效率,所以会牺牲掉一定的空间来换取查询效率

链表长度上限是8,当尝试将链表转化为红黑树时,不一定会转化成功,还要判断哈希数组长度,如果小于64,则会先扩容哈希表,扩容后所有元素的位置需要重新计算,链表长度会变短。

只有当链表长度大于等于8且哈希数组长度大64,链表才会转换为红黑树

当红黑树节点小于等于6时,,会退化成链表

ConcurrentHashmap

他是线程安全的,但是他的加锁方式和HashTable不同

HashTable时直接在方法上加锁,一次只能有一个线程进入访问

concurrenthashmap是直接在哈希表每个位置上加锁。

加锁时把每个位置第一个node当作锁对象,当这个位置没有元素时,采用cas+synchronized的方式

此外,他不支持存储键为null或值为null,主要是为了消除歧义。

因为当concurrenhashmap找不到数据的时候,他会返回null,所以无法分辨是因为key的值为null还是找不到key为null。

hashmap和hashtable的区别

底层的数据结构来看:在jdk1.8之后,hashmap首先存储数据的结构是数组,初始长度为16,每次扩容为之前的2倍,当存储的链表结构的大小大于阈值的时侯(默认大小为8),会将链表转换为红黑树,在转换为红黑树时,会进行判断,如果当前的数组长度不够64时,会优先将数组扩容至64,而不是转换为红黑树,这是为了减少搜索的时间。

从线程是否安全来看,hashmap是线程不安全的,而hashtable是线程安全的,是因为hashtable中的方法都是经过sychorized修饰的

对于null KEY 和null VALUE的处理,hashmap中支持空键和空值的出现,但是只能由一个空键,但是可以有若干个value的值,而hashtable则不支持空键和空值的出现,否则会抛出异常

对于扩容机制而言,hashmap的初始值为16,每次扩容时为之前的两倍,hashtable的初始值为11,每次扩容为之前的2倍加1,创建时如果给定了初始的容量,hashtable会直接使用初始值,而hashmap会将其扩充为2的幂次方大小

那么在这里有一个新的问题,为什么要扩充为2的幂次方?

1.位运算效率更高:位运算(&)比取余运算(%)更高效。

2.可以更好地保证哈希值的均匀分布:扩容之后,在旧数组元素 hash 值比较均匀的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。

hashmap和hashset的区别

HashSet 底层就是基于 实现的。( 的源码非常非常少,因为除了少部分是 自己不得不实现之外,其他方法都是直接调用 中的方法。

hashmap的treemap的区别

首先,hashmap和treemap都继承了abstractmap接口,但是treemap还继承了NavigableMap接口,这让他拥有了对集合内的元素的搜集能力,Navigable提供的很多方法都是基于红黑树的数据结构实现的,红黑树保持平衡状态,从而保证了搜索操作的时间复杂度为 O(log n),这让 TreeMap 成为了处理有序集合搜索问题的强大工具。

其次,treemap还继承了sortmap接口,这让他拥有了对集合内的数据根据键排序的能力,默认是按照key的升序排列的,我们也可以使用指定规则的排序器

在java关于集合的面试中,针对map的知识主要集中在以下问题中

hashmap的原理介绍一下

在Java1.7之前,hashmap使用数组和链表来进行实现,hashamap通过哈希算法将数据的键映射 到数组的一个位置上,多个元素都在一个数组的位置上时, 将他们以链表的形式存储在同一个位置上,但是链表的查询效率较低,所以在jdk1.8之后,引入了红黑树,当链表的长度大于8且数组长度大于64时,将链表转换为红黑树,红黑树节点小于6时,转换为链表。

了解的哈希冲突解决方法有哪些?

链接法:用链表或者其他的数据结构来存储冲突的键值对,让多个数据可以存储在同一个哈希桶中

在哈希法:当发生冲突时,使用其他的哈希函数来计算得到新的哈希值,直到找到一个空键来存储

哈希桶扩容:动态的扩容哈希桶的大小,重新分配键值对,可以 减少冲突的概率

hashmap时线程安全的吗?

hashmap不是线程安全的,在并发环境下,可能出现put值被覆盖的情况

可以使用Collections.synchronizedMap同步加锁的方式,也可以使用hashtable,但他们的效率都不算高,跟推荐使用concurrenthashmap

在 Java 的 hashmap 中 get一个元素的过程是怎样的?

首先判断hashmap中存储数据的table数组不为空且数组长度不为0,table已经创建且通过hash值计算出来的地方不为空,接下来进行具体的寻找,首先判断哈希值计算出来的位置的第一个元素是不是我们要查找的元素,如果不是,判断是否有下一个节点,如果有下一个节点,那么就继续查找,如果没有,那就说明我们查找的元素不存在,如果第一个节点是树形节点,那就使用红黑树的查找算法查找,如果不是,那就是链表结构,使用dowhile进行循环查找,找到了值我们就返回,没有找到,那就返回null。

源码如下:

java 复制代码
/**
 * Implements Map.get and related methods
 *
 * @param hash key到hash值
 * @param key key值
 * @return the node, or null if none
 */
final HashMap.Node<K,V> getNode(int hash, Object key) {
    HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
​
    // 以下if语句中判断三个条件:
    // 1、HashMap中存储数据的数组table不为null;
    // 2、数组table不为null,且长度大于0;
    // 3、table已经创建,且通过hash值计算出的节点存放位置有节点存在;
    // 若上面三个条件都满足,才表示HashMap中可能有我们需要获取的元素
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
​
        // 定位到元素在数组中的位置后,我们开始沿着这个位置的链表或者树开始遍历寻找
        // 注:JDK1.8之前,HashMap的实现是数组+链表,到1.8开始变成数组+链表+红黑树
​
        // 首先判断这个位置的第一个节点的key值是否与参数的key值相等,
        // 若相等,则这个节点就是我们要找的节点,将其返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 若上面的不满足,则判断第一个节点是否有下一个节点
        // 若有,继续判断;若没有,那表示我们要找的节点不存在
        if ((e = first.next) != null) {
            // 若第一个节点是应该树节点,则通过红黑树的查找算法进行查找
            if (first instanceof HashMap.TreeNode)
                return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
            // 若不是一个树节点,表示当前位置是一个链表,则使用do...while循环遍历查找
            do {
                // 若查找到某个节点的key值与参数的key值相等,则表示它就是我们要找的节点,将其返回
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 若没有找到对应的节点,返回null
    return null;
}
hashmap的put过程介绍一下

第一步:根据添加的键的哈希值计算在数组中的位置

第二步:找到对应的位置后,判断当前位置有没有数据,如果没有数据,将这个键值对包装为一个node对象,键值成为node对象的键值,存放到计算出来的数组位置

如果当前位置有数据,判断当前位置的第一个元素的键和我们要添加的元素的键是否相同,判断两个键是否相同,首先判断他们的哈希值是否相同,如果哈希值相同,使用equals方法来判断j具体的键内容是否相同,如果相同,那么就覆盖原本的值,如果不相同,那么就判断数据类型,如果是树的话,进行查找,找到相同的键,那就替换旧的值,如果没有找到,那就将新的键值对添加到树中,如果是链表结构,也是进行查找,找到了就替换旧值,找不到就把新的键值对添加到链表的头部

第三步:检查链表的长度是否到了阈值,默认为8,如果到了并且哈希数组的长度大于等于64,将链表转换为红黑树

第四步:检查当前的哈希数组的size和哈希的长度比值是否到达负载因子,如果已经到达,那么就要扩容,创建当前数组二倍的新数组,将旧数组中的哈希值进行计算分配到新的数组,更新hashmap的引用

最后,完成添加

源码如下:

复制代码
  
java 复制代码
  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; //hash数组
        Node<K,V> p; 
        int n, i;//n是数组长度,i是索引
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//创建hash表
        if ((p = tab[i = (n - 1) & hash]) == null)//这个计算效果==97%16 因为这个为位运算,速度更快
            //如果这个位置为null,则将(key,value)包装到一个node对象里,放到i这个位置的第一个
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //两部分判断key是否相同,先判断hash值是否相等,为了快速定位,第二部分用equals,为了判断内容是否相同,是为了准确,因为会出现hash值相同,但内容不同,例如(a和97 "通话"和"重地")
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//如果键相同的话,直接跳转到下方
            else if (p instanceof TreeNode)//判断类型,看是树还是链表
                //如果是树,直接加进去
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//链表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //添加完成后,判断链表长度
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//转为红黑树
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //跳转处
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//如果键相同,则后面的值会覆盖前面的值
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
HashMap一般用什么做Key?为啥String适合做Key呢?

一般使用string作为key,因为string对象是不可变的,为什么string对象不可变呢?首先,string对象最核心的存储字符的数组,使用private和final修饰,同时string对象并没有提供修改数组内容的方法,所有看起来修改的方法,都是创建新的数组加上指向新的引用,原来的数组完全没有发生改变。基于这个原因,这确保了key的稳定性,因为如果key是可变的,会导致hascode和equals方法的不一致,进而影响hashmap的准确性。

简单说:存的时候用的是 Key 当时的 hashCode,取的时候用的是 Key 修改后的 hashCode,两个 hashCode 不一样,自然找不到;即使 hashCode 碰巧一样,equals 也可能判定不相等。

重写HashMap的equal和hashcode方法需要注意什么?

最重要的是要注意如果两个key的hascode相同,那么他们equals方法也是相同的。但是他们equals相同 ,他们hascode不一定相同。

hashmap的扩容机制的讲一下

hashmap的负载因子是0.75,也就是说当前存储的元素超过总数的百分之75的时候,hashmap就会扩容,扩容分为两个步骤:

首先,对哈希表长度的扩展。扩充至之前长度的二倍

其次,将旧哈希表的数据填入到新的哈希表之中

因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。

说说hashmap的负载因子

HashMap 负载因子 loadFactor 的默认值是 0.75, HashMap 中的元素个数超过了容量的 75% 时,就会进行扩容。

默认负载因子为 0.75,是因为它提供了空间和时间复杂度之间的良好平衡。

负载因子太低会导致大量的空桶浪费空间,负载因子太高会导致大量的碰撞,降低性能。0.75 的负载因子在这两个因素之间取得了良好的平衡。

ConcurrentHashMap怎么实现的?

jdk1.7

在这个时候,他使用数组加链表的形式,分为一个大数组segment和一个小数组hashentry。segment是一个可重入锁,hashentry用来存储元素,每个hashentry是一个链表结构的元素。

这个时候,他将数据按照一段一段的方式进行存储,在多线程的情况下,一个线程访问一段数据,其他的线程可以访问其他段的数据,实现并发访问。

jdk1.8

在jdk1.7中,解决了线程安全的问题,但是他的底层是数组加链表,在数据量比较大的时候,访问速度会比较慢,所以在jdk1.8中,采取了数组加链表加红黑树

而在锁方面,也进行了新的进步。JDK 1.8 放弃了 JDK 1.7 中分段锁(Segment)的设计,转而采用 volatile + CAS + synchronized 组合来保证线程安全,核心是锁粒度细化到哈希桶的头节点

添加元素时首先会判断容器是否为空

1.如果为空, 则使用volatile和CAS进行初始化,通过 volatile 保证数组可见性,ConcurrentHashMap 的底层数组 table懒加载 的 ------ 不是创建对象时就初始化,而是第一次调用 put() 方法时才初始化。如果没有CAS初始化,那么就会出现线程一判断数组为空,准备创建时被线程二抢走,线程二也判断为空,此时两个线程都创建了数组,就会出现数据被覆盖的情况。

那么CAS如何解决重复初始化的这个问题呢?

CAS 的全称是 Compare And Swap(比较并交换),是 CPU 级别的原子操作,核心逻辑:

1.如果内存中的值和我们的预期值相等,那么我们就初始化

2.如果不相同,那么说明其他线程已经修改了,那么我们就不做任何改变,直接返回即可

**2.如果不为空,**则根据存储的元素计算存储的位置是否为空,

如果为空,则利用CAS设置该节点的值

如果不为空,则使用synchronized,那么这里为什么要使用synchronized,这是因为如果当前位置有元素,我们要对元素进行遍历和修改操作,遍历是找是否有相同key的节点,修改就是如果有就替换,没有就添加,所以需要加锁。然后在判断是否需要转变为红黑树 。

相关推荐
6+h1 小时前
【java IO】转换流 + 对象流 + 序列化详解
java·开发语言
嫂子的姐夫2 小时前
036-spiderbuf第C9题
爬虫·python·js逆向·逆向
沈阳信息学奥赛培训2 小时前
#define 和 typedef 的区别
开发语言·c++
Laurence2 小时前
CMake 查找、打印 Qt 所有 Components / 模块列表
开发语言·qt·cmake·打印·查找·所有组件·所有模块
j_xxx404_2 小时前
LeetCode模拟算法精解I:替换问号,提莫攻击与Z字形变换
开发语言·数据结构·c++·算法·leetcode
青槿吖2 小时前
第二篇:Spring MVC进阶:注解、返回值与参数接收的花式玩法
java·开发语言·后端·mysql·spring·mvc·mybatis
共享家95272 小时前
Java入门(抽象类 与 接口)
java·开发语言
hanbr2 小时前
C++ string类模拟实现(完整版,含全运算符重载)
java·开发语言
老树临风_2 小时前
ROS2机器人智能小车学习(1)- ROS2 最简安装与配置
学习·机器人·ros2