java面试-java集合

如何选用集合?

  • 需要键值对选用 Map 接口下的集合,需要排序用 TreeMap,不需要排序用 HashMap。
  • 不需要键值对仅存放元素则选择 Collection 下实现的接口,保证元素唯一使用 Set,不需要则选用 List。

如何确保一个集合不能被修改?

  • 使用 final 修饰集合只能保证引用不被修改,但集合里面的内容还是可以修改的。
  • 使用 Collections 包下的 unmodifiableMap 方法可以返回一个不可修改的 Map,修改操作会报 java.lang.UnsupportedOperationException 异常。

Collection 和 Collections 有什么区别?

  • Collection 是集合类的上级接口,继承它的主要有 List 和 Set。
  • Collections 是针对集合类的一个工具类,提供一些静态方法实现,如 Collections.sort() 排序、Collections.reverse() 逆序等。

List 与 Set 的区别:

  • List 和 Set 都是继承自 Collection 接口。
  • List 特点:元素有放入顺序,元素可重复。
  • Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉。
  • Set 和 List 对比:
    • Set 检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
    • List 类似于数组,可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。

Map 集合

面试题:HashMap如何get一个元素?

  • 计算哈希值:首先,根据传入的键(key),使用键的哈希函数来计算出该键的哈希值。哈希值是一个整数,用于确定元素应该存储在HashMap的哪个桶(bucket)中。

  • 找到桶:根据计算得到的哈希值,确定元素应该存储在HashMap的哪个桶中。一个桶中可以存储多个键值对,通常使用链表或红黑树等数据结构来存储这些键值对。

  • 搜索桶内元素:在确定了元素所在的桶之后,HashMap会在该桶内搜索目标元素。如果桶内只有一个元素,直接比较该元素的键和传入的键是否相等;如果桶内有多个元素,HashMap会根据键的哈希值进行快速搜索,找到目标元素。

  • 返回结果:如果找到了目标元素,HashMap会返回该元素的值;如果未找到,返回null,表示元素不存在

常见的 Map 数据结构及其区别

HashMap

  • 初始容量大小和每次扩充容量大小: 创建时如果不指定容量初始值,HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。创建时如果给定了容量初始值,HashMap 会将其扩充为 2 的幂次方大小。
  • 对 Null key 和 Null value 的支持: 最多只允许一条记录的键为 null,允许多条记录的值为 null
  • 线程同步: 不支持线程的同步,可以用 Collections.synchronizedMap 方法使 HashMap 具有同步的能力,或者使用 ConcurrentHashMap

为什么是2的幂次方?
在 Java 中,HashMap 的扩容操作会将内部的哈希桶数组容量扩大为当前容量的两倍,并且新的容量必须是 2 的幂次方。这是因为使用 2 的幂次方作为容量有助于减少哈希碰撞(Hash Collision)的发生。

具体来说呢
1.当数组容量为 2 的幂次方时,假设容量为 capacity = 2^k,其中 k 是一个非负整数。这样,哈希函数计算得到的哈希值hash 对容量进行取模操作时,可以用位运算的形式实现 hash & (capacity - 1)。这相当于将 hash 的低 k 位作为索引,用于确定元素在数组中的存储位置。
2.由于容量为 2 的幂次方的二进制表示中只有一个位是 1,其余位都是 0,因此 capacity - 1 的二进制表示中低 k 位全为 1,高位全为 0。例如,当 capacity = 16 时,其二进制表示为 10000,而 capacity - 1 = 15 的二进制表示为 01111。
3.通过将哈希值与 capacity - 1 进行按位与运算,可以保留哈希值的低 k位,而将高位的影响消除掉。这样,元素的存储位置只与哈希值的低 k 位有关,而与高位无关,实现了更加均匀的分布。
4.相比于其他非幂次方的容量,使用容量为 2的幂次方可以最大程度地减少哈希碰撞的发生。因为非幂次方的容量在进行取模操作时,会存在一些哈希值的高位对结果的影响,可能导致哈希值的分布不够均匀。

Hashtable

  • 初始容量大小和每次扩充容量大小: 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小。
  • 对 Null key 和 Null value 的支持:不允许记录的键或值为 null
  • 线程同步: 支持线程的同步,任一时刻只有一个线程能写 Hashtable
  • 效率问题: 写入时会比较慢,因为内部的方法基本都经过 synchronized 修饰

LinkedHashMap

  • 是 HashMap 的子类,额外持有一个双向链表,维护了记录的插入顺序
  • 遍历时,得到的记录是先插入的,也可以按照应用次数排序
  • 在实际数据较少时,遍历起来可能会比 HashMap 慢

TreeMap

  • 实现 SortMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器
  • 遍历时,得到的记录是排过序的

HashMap 的链表转换红黑树的机制(HashMap是如何解决哈希冲突的)

  • 当链表长度大于阈值(默认为 8),会将链表转换成红黑树(创建一个红黑树节点,将链表中的元素逐个转移到新创建的红黑树节点中,保持元素在红黑树中的顺序),以减少搜索时间(因为红黑树的查找操作的平均时间复杂度为 O(log n),而链表的查找操作的平均时间复杂度为 O(n)
  • 如果当前数组小于 64,会先进行数组扩容而不是转换为红黑树
  • 需要注意的是,在哈希表中,红黑树并不是替代链表,而是链表的一种改进结构。当红黑树中的元素数量减少到一定程度时,会将红黑树重新转换为链表,以节省空间和提高性能。

为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。
因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

不用红黑树,用二叉查找树可以么?

可以。但是二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。

为什么链表改为红黑树的阈值是 8?

首先和hashcode碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡。在负载因子0.75(HashMap默认)的情况下,单个hash槽内元素个数为8的概率小于百万分之一,将7作为一个分水岭,等于7时不做转换,大于等于8才转红黑树,小于等于6才转链表。链表中元素个数为8时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了8,是根据概率统计而选择的。

默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8 ?

1、负载因子是1.0
数据一开始是保存在数组里,当发生了Hash碰撞的时候,就是在这个数据节点上,生出一个链表,当链表长度达到一定长度的时候,就会把链表转化为红黑树。
当负载因子是1.0时,也就意味着,只有当数组的8个值(这个图表示了8个)全部填充了,才会发生扩容。这就带来了很大的问题,因为Hash冲突时避免不了的。
后果:当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。
因此一句话总结就是负载因子过大,虽然空间利用率上去了,但是时间效率降低了。
2、负载因子是0.5
后果:负载因子是0.5的时候,这也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低。查询效率就会增加。
但是,此时空间利用率就会大大的降低,原本存储1M的数据,现在就意味着需要2M的空间。
总之,就是负载因子太小,虽然时间效率提升了,但是空间利用率降低了。
3、负载因子0.75
经过前面的分析,基本上为什么是0.75的答案也就出来了,这是时间和空间的权衡。

为什么 hash 值要与length-1相与?

把 hash 值对数组长度取模运算,模运算的消耗很大,没有位运算快。
当 length 总是 2 的n次方时,h& (length-1) 运算等价于对length取模,也就是 h%length,但是 & 比 % 具有更高的效率

HashMap的底层数据结构

在JDK1.7 和JDK1.8 中有所差别:

在JDK1.7 中,由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
在JDK1.8 中,由“数组+链表+红黑树”组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度
是 O(logn),而链表是糟糕的 O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换

HashMap 多线程操作导致死循环问题

HashMap 多线程操作导致死循环问题JDK1.7 及之前版本的 HashMap 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用 ConcurrentHashMap 。

HashMap为什么线程不安全

HashMap的线程不安全性主要体现在以下两个方面:

  1. JDK 1.7及之前版本的死循环和数据丢失问题:在多线程环境下,当HashMap进行扩容时,多个线程同时进行put操作可能会导致死循环和数据丢失的问题。具体来说,在扩容过程中,多个线程可能同时触发了扩容操作,导致多个线程同时进行桶的迁移操作。由于扩容操作涉及到改变桶的数量和重新计算元素在桶中的位置,多线程同时进行迁移操作可能导致数据丢失或死循环的情况。

  2. JDK 1.8后的数据覆盖风险:在JDK 1.8中,HashMap的实现采用了链表和红黑树的方式存储多个键值对。当多个键值对发生哈希冲突时,它们会被分配到同一个桶中。在多线程环境下,多个线程对HashMap进行put操作时,会导致线程不安全性和数据覆盖的风险。例如,两个线程同时进行put操作并发生哈希冲突,不同的线程可能在不同的时间片获得CPU执行的机会。线程1执行哈希冲突判断后被挂起,而线程2先完成插入操作。然后,线程1再次获得执行机会,由于之前已经进行过哈希冲突判断,线程1会直接进行插入操作,导致线程2插入的数据被线程1覆盖。

ConcurrentHashMap jdk1.7 和 jdk1.8的区别

数据结构:jdk1.7 Segment+HashEntry;jdk1.8 数组+链表+红黑树+CAS+synchronized

  • 实现线程安全的方式(重要):
    1.在 JDK1.7 的时候,ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
    2.到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作

具体是怎么实现并发控制的呢,sychronized和CAS在1.8中具体是怎么用的?
一般情况下,ConcurrentHashMap 使用 CAS 进行乐观并发控制,以提高并发性能。CAS 是一种无锁的原子操作,它可以在不使用锁的情况下进行并发操作,通过比较并交换的方式来实现线程安全性。
1.然而,在某些特定的操作场景下,CAS 可能会失败,例如多个线程同时尝试插入键值对到同一个桶(bucket)时。为了处理这种冲突,ConcurrentHashMap 在必要时使用 synchronized 关键字来提供悲观锁的方式,确保同一时刻只有一个线程可以执行被锁定的代码块
2.并发修改(CAS的失效情况):在多线程环境下,某个线程在执行 CAS 操作之前,另一个线程可能已经修改了目标内存位置的值。这种情况下,CAS 操作会失败,因为 CAS 会比较内存位置的当前值与预期值是否相等,如果不相等,则认为有其他线程已经修改了该值。
3.使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

Java8开始ConcurrentHashMap为什么舍弃分段锁?

  • 加入多个分段锁浪费内存空间
  • 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待

List集合:

Vector 和 ArrayList 初始化大小和容量扩充有什么区别?

  • Vector 和 ArrayList 的默认容量都为 10。
  • Vector 容量扩充默认增加 1 倍。
  • ArrayList 容量扩充默认增加大概 0.5 倍。

ArrayList 扩容机制是什么样的

在 Java 中,ArrayList 在内部使用一个数组来存储元素。当添加元素时,如果数组已满,ArrayList 将会进行扩容操作。

ArrayList 的扩容机制如下:

  • 当添加第一个元素时,ArrayList 会创建一个默认初始容量的数组(默认为 10)。
  • 当继续添加元素并且数组已满时,ArrayList 将会创建一个新的更大容量的数组,并将原数组中的元素复制到新数组中。
  • 新数组的大小通常是原数组的 1.5 倍(可以通过 grow() 方法来计算新的容量),这样可以在一定程度上减少频繁扩容的次数。
  • 复制元素的操作通过 System.arraycopy() 方法来完成,它是一个高效的底层操作,可以在较短的时间内完成复制。

比较 ArrayList 与 LinkedList,Vector:

  • 底层数据结构:ArrayList 底层使用的是 Object 数组;LinkedList 底层使用的是双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环);Vector 是 List 的古老实现类,底层用 Object[] 存储。
  • 线程安全:Vector 底层很多方法都加上了同步关键字 synchronized 保证线程安全,而 ArrayList 和 LinkedList 无法保证线程安全。

Vector 内部使用了 synchronized关键字来对所有公共方法进行同步。这意味着每个公共方法都在方法级别上被同步,确保在同一时间只有一个线程可以执行这些方法。

  • 是否支持快速随机访问:Vector 和 ArrayList 的内部结构是以数组形式存储的,因此非常适合随机访问,但非尾部的删除或新增性能较差,比如在中间插入一个元素,就需要把后续的所有元素都进行移动;LinkedList 插入和删除元素效率比较高,但随机访问性能会比以上两个动态数组慢。
  • 内存空间占用:ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)

ArrayList 可以添加 null 值吗?

ArrayList 中可以存储任何类型的对象,包括 null 值。不过,不建议向ArrayList 中添加 null 值, null 值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。

ArrayList 插入和删除元素的时间复杂度?

对于插入操作:

  • 头部插入:需要将所有元素都依次向后移动一个位置,因此时间复杂度是 O(n)。
  • 尾部插入:当 ArrayList 的容量未达到极限时,往列表末尾插入元素的时间复杂度是O(1),因为它只需要在数组末尾添加一个元素即可。当容量已达到极限并且需要扩容时,则需要执行一次 O(n)的操作将原数组复制到新的更大的数组中,然后再执行 O(1) 的操作添加元素。
  • 指定位置插入:需要将目标位置之后的所有元素都向后移动一个位置,然后再将新元素放入指定位置。这个过程需要移动平均 n/2
    个元素,因此时间复杂度为 O(n)。

对于删除操作:

  • 头部删除:需要将所有元素依次向前移动一个位置,因此时间复杂度是 O(n)。
  • 尾部删除:当删除的元素位于列表末尾时,时间复杂度为 O(1)。
  • 指定位置删除:需要将目标元素之后的所有元素向前移动一个位置以填补被删除的空白位置,因此需要移动平均 n/2 个元素,时间复杂度为
    O(n)。

LinkedList 插入和删除元素的时间复杂度?

  • 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
  • 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
  • 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。

Set集合:

LinkedHashSet 如何保证有序和唯一性?

  • LinkedHashSet 底层数据结构由哈希表和链表组成,链表保证了元素的有序即存储和取出一致,哈希表保证了元素的唯一性。

java中无序性和不可重复性的含义是什么

  • 无序性(Unordered):无序性表示集合中元素的存储和访问没有固定的顺序。存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
  • 不可重复性(Unique):不可重复性表示集合中的元素是唯一的,不会出现重复元素的情况。当你尝试向集合中添加重复的元素时,集合会自动忽略重复元素,并保持每个元素的唯一性。

比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同:

  • HashSet 是 Set 接口的实现类,底层是 HashMap,是线程不安全的,可以存储 Null 值(因为 HashSet 中只需要用到 key,而 HashMap 是 key-value 键值对,所以向 map 中添加键值对时,键值对的值固定是PRESENT)。
  • LinkedHashSet 是 HashSet 的子类,会维护“插入顺序”,而 HashSet 并不管什么顺序,内部使用 LinkedHashMap 对象来存储和处理它的元素。
  • TreeSet 是基于 TreeMap 实现的,是一个有序的二叉树,同理 TreeSet 同样也是一个有序的集合。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值