lukaliou123 / lukaliou123.github.io

lukaliou123在2022年的面试用知识点总结
Other
5 stars 0 forks source link

Java容器篇 #1

Open lukaliou123 opened 2 years ago

lukaliou123 commented 2 years ago

前置顶:容器的关系

image

Java 集合, 也叫作容器,主要是由两大接口派生而来: 一个是 Collection接口,主要用于存放单一元素; 另一个是 Map 接口,主要用于存放键值对。 对于Collection 接口,下面又有三个主要的子接口:List、Set 和 Queue。

1. 说说 List, Set, Queue, Map 四者的区别?

List(对付顺序的好帮手):存储的元素是有序的、可重复的 Set(注重独一无二的性质):存储的元素是无序的、不可重复的 Queue(实现排队功能的叫号机):按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的 Map(用key来搜索的专家):使用键值对(key-value)存储,类似于数学上的函数,“x”代表key, “y”代表value,key是无序的,不可重复的,value是无序的,可重复的,每个键最多映射到一个值。

2. Arraylist 与 LinkedList 区别?

  1. 是否线程安全:两个都不安全
  2. 底层数据结构:ArrayList底层使用的是Object数组;Linklist底层用的是双向链表数据结构
  3. 插入和删除是否是元素位置影响:1.ArrayList采用数组储存,所以插入和删除时间复杂度受元素位置影响,O(n-i)2.LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入
  4. 是否支持快速随机访问:LinkedList不支持,ArrayList支持,使用get()通过元素序号快速获取
  5. 内存空间占用:ArrayList浪费在list列表结尾一定要留预留空间,Linkedlist则是每个元素都要留地方放前驱后置和数据

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

    插入和删除尾部都是O(1), 但是操作中间的或头部的节点会导致数组移动,要变为O(n)

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

    头尾都是O(1),但是中间的因为涉及到搜索所以O(n)

    2.补充:使用场景?

    1.使用数组的场合:如果你需要快速访问数据或者知道数据的索引,那么数组是一个很好的选择。因为数组提供了 O(1) 的随机访问性能。比如你正在处理一些数学问题,或者你需要将数据存储到硬盘上,然后再次读取它们,数组可能是一个更好的选择。

2.使用链表的场合:如果你经常需要在列表的中间位置插入或删除元素,那么链表可能是一个更好的选择。链表的插入和删除操作都是 O(1) 的时间复杂度,而数组的这些操作则需要 O(n) 的时间复杂度。比如你在实现一个缓存策略,例如 LRU (Least Recently Used),这样的策略经常需要删除中间的元素,并且插入新的元素,这时候使用链表会很方便。

3. ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?

  1. ArrayList是List的主要实现类,底层使用Object[]存储,适用于频繁的查找工作,线程不安全
  2. Vector是List的古老实现类,底层使用Object[]存储,线程安全

3.补充:为什么现在vector少了?

首先,Vector的所有公共方法都是同步的,这就意味着如果你在一个线程中访问一个Vector,而在另一个线程中也尝试访问同一个Vector,那么后一个线程就需要等待第一个线程完成操作。这会导致性能问题,尤其是在多线程的环境中。

其次,现在我们有了更多的选择。如果你需要一个线程安全的集合,Java的并发库提供了许多更好的选择,比如CopyOnWriteArrayList等。

补充:CopyOnWriteArrayList

至于CopyOnWriteArrayList,你的记忆没错,它确实是通过克隆数组实现线程安全的。每当我们对CopyOnWriteArrayList进行修改操作(如add或者set等)的时候,它都会创建一个新的数组,并将原数组的内容复制到新数组中,然后再进行修改。这种方法叫做“写时复制”

这种方式的优点是对CopyOnWriteArrayList的读操作可以完全不需要加锁,因为它们总是操作的是不会被修改的数组。这对于读操作远远多于写操作的应用场景非常有用,比如在事件监听器的列表中。

但是,它的缺点也是很明显的。因为每次修改都会复制整个数组,所以如果数组非常大,或者修改操作非常频繁,那么这将导致大量的内存开销和性能开销

所以,CopyOnWriteArrayList是一种特殊的工具,只有在特定的场景下,比如读多写少的情况,才更适合使用。

补充:链表的升级版--跳表

跳表是通过添加多级索引的方式来提高链表的查找效率的。你可以把跳表理解为一个对链表的“升级版”。在原有的链表结构之上,跳表增加了多级索引,每一级索引都是原链表的一个子集每一级的索引节点数都比上一级的少。这样的话,当我们进行查找操作时,可以先在较高级别的索引中进行,快速定位到一个区间,然后再逐级降低,精确查找,大大减少了查找的步骤,提高了效率。

具体实现上,我们可以设定链表的每个节点都可能成为上一级索引的节点,规定比如每2个或者3个节点抽取一个到上一级,如此类推,形成多级索引。跳表的查找、插入、删除等操作的时间复杂度都可以达到O(logn),所以其效率是相当高的。

Redis中就使用了跳表来实现有序集合(zset)数据类型,这样即保证了有序性,又可以快速地进行查找和插入操作。

例子:

1691056020671

在这个跳表中,我们依然保留了原来的链表,这是最底层(level0)。然后,我们在原链表的基础上,建立了一个新的层次 (level1),这个层次中只包含部分数字。然后,我们又在level1的基础上,建立了level2。每一层的元素都是其下一层的子集。

这样,当我们查找一个数字时,例如我们仍然要查找8。首先我们在level2层开始查找。我们发现7是最接近8且小于8的数字,于是我们在level2层向后查找,但发现后面的数字比8大,所以我们退回到level1层,从7开始查找,又发现10比8大,所以再退回到level0层,从7开始查找,最后找到了8。

lukaliou123 commented 2 years ago

4. HashMap 和 Hashtable 的区别

  1. 线程安全:HashMap是非线程安全的,HashTable线程安全,因为内置了synchronized.(不过效率还是concurrenHashMap高)
  2. 效率:线程安全问题,HashMap效率高,HashTable基本被淘汰了
  3. 对null key 和 value的支持:HashMap可以存储null的key和value,但null key只能有一个,HashTable不支持
  4. 初始容量大小和每次扩充容量大小:1.Hashtable默认初始11.每次扩充比为原来的2n+1,HashMap初始为16,每次扩容会变成原来的2倍。2.如果给了初始值,HashMap会将其扩充为2的敏次方大小,也就是说HashMap总是使用2的幂作为大小
  5. 底层数据结构:JDK1.8以后,HashMap在解决哈希冲突时,当链表长度大于阈值(默认为8),将链表转为红黑树以减少搜索时间,HashTable没有这种机制

HashMap为什么每次扩容都是扩大两倍,

这是因为它利用了位运算来计算哈希值。扩容两倍后,旧的元素在新数组中的位置要么保持不变,要么是在原位置基础上加上旧数组长度。这样,旧元素的位置只需要计算一次哈希值就能得到,这样能够提高扩容的效率

5. 什么是红黑树。

image 1691051584257

首先,红黑树是一种自平衡的二叉搜索树,每个节点包含两个子节点和一个颜色属性(可以是红色或黑色)。这种数据结构主要被应用于关联数组和Java等语言的内置库,例如TreeMap和TreeSet

红黑树的基本结构特性如下:

1.每个节点或是红色,或是黑色2.根节点是黑色。 3.每个叶节点(NIL或空节点)是黑色。 4.若节点是红色,则它的子节点必须是黑色(也就是不能有两个连续的红色节点)。 5.从任一节点到其每个叶节点的所有路径都包含相同数目的黑色节点,即黑色平衡(重要!)

对红黑树的常见操作(如插入、删除和查找)的时间复杂度都是O(log n),因为树的高度始终是对数级别的。这是红黑树与普通二叉搜索树最主要的区别,二叉搜索树在最坏情况下会退化为链表,时间复杂度变为O(n)。

为何选择红黑树? 我们选择红黑树而不是其他树,如AVL树,主要是因为红黑树在实际应用中更加高效。相比于AVL树,红黑树的平衡条件更宽松,因此在进行插入和删除操作时,需要进行的旋转次数较少,从而使得在实际应用中的效率更高。此外,红黑树的结构相对简单,易于理解和实现https://zhuanlan.zhihu.com/p/143585797

为什么一个节点到其每个叶子结点的黑色节点数量一样(黑色平衡)是优点?

这是因为它能保证最长的可能路径不会超过最短的可能路径的两倍长度。换句话说,红黑树是近似平衡的,这种平衡性质保证了红黑树的查找、插入、删除操作的时间复杂度为O(log n),避免了最坏的情况(即退化成链表)

补充:Hashmap和treeMap

1.实现:HashMap基于哈希表(数组+链表+红黑树)实现,而TreeMap基于红黑树实现。

2.排序:HashMap不保证任何特定的元素顺序,而TreeMap则根据key的自然顺序或者指定的Comparator进行排序。

3.性能:HashMap在大多数操作(get和put等)中提供常数时间的性能,因为它是基于哈希表的。但是,TreeMap提供log(n)时间的性能,因为它是基于树的。

线程安全:这两种都是非线程安全的,如果需要线程安全的话,可以考虑使用Collections.synchronizedMap()方法或者ConcurrentHashMap。

lukaliou123 commented 2 years ago

6. HashMap 和 HashSet区别

HashSet是基于HashMap实现的,除了clone,writeObject(),readObject以外,其他都是调用Hashmap的方法 image

7. HashMap的底层实现

  1. jdk1.8之前 HashMap是数组和链表结合在一起使用,也就是聊表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞 1.8和1.7的哈希码 image 拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可
  2. JDK1.8之后 a) 相比于之前的版本,jdk1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转为红黑树前会判断,如果当前数组的长度小于64,,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间

7.1.HashMap 多线程操作导致死循环问题(主要1.7之前)

.JDK1.7 及之前版本的 HashMap 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。

为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用 ConcurrentHashMap 。

7.2.HashMap 为什么线程不安全

这里以 JDK 1.8 为例进行介绍。 JDK 1.8 后,在 HashMap 中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 HashMap 的 put 操作会导致线程不安全,具体来说会有数据覆盖的风险

举个例子:两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)。不同的线程可能在不同的时间片获得 CPU 执行的机会,当前线程 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所有此时会直接进行插入,这就导致线程 2 插入的数据被线程 1 覆盖了。

lukaliou123 commented 2 years ago

8. ConcurrentHashMap 和 Hashtable 的区别(主要是线程安全的不同)

  1. 底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable和JDK1.8之前的HashMap的底层数据结构类似,都是采用数组+链表的形式,数组是HashMap的主题,链表则是主要为了解决哈希冲突而存在的
  2. 实现线程安全的方式(重要): 1.在JDK1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到了JDK1.8的时候已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用sychronized和CAS来操作。(JDK1.6以后对Synchronized锁做了很多优化)整个看起来就像是优化且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只为了兼容旧版本。
  3. Hashtable(同一把锁):使用synchronized来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低 image

JDK1.7的ConcurrentHashMap: image

JDK1.8的ConcurrentHashMap: image

lukaliou123 commented 2 years ago

9. ConcurrentHashMap线程安全的具体实现方式/底层具体实现

  1. JDK1.7 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问 ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成的。 Segment实现了ReentrantLock,所以Segment是一种可重入锁,扮演所得角色。HashEntry用于存储键值对数据。 image

一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组的链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment的锁

  1. JDK1.8 ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑而查出。Java8在链表长度超过一定阈值(8)时将链表(O(N))转为红黑树(O(log(N)) Synchronized只锁定当前链表或红黑而查出的首节点,这样只要HASH不冲突,就不会产生并发,效率提升N倍。

concurrentHashMap 1.8之后的补充:

具体来说,ConcurrentHashMap 在 JDK 1.8 中采用了以下几种技术来提高性能:

a. 分段锁优化:ConcurrentHashMap 的数据结构由原来的 Segment 数组改为 Node 数组,每个 Node 表示一个键值对。它使用了一种称为 CAS+Synchronized 的技术来替代了 JDK 1.7 版本中的分段锁。这样可以减小锁的粒度,提高并发访问效率。

b. 链表转换为红黑树:当链表中的节点数超过一定阈值时,ConcurrentHashMap 会将链表转换为红黑树,以提高查找、插入和删除操作的性能。

c. 锁粒度调整:ConcurrentHashMap 根据当前容量和并发度来动态调整锁的粒度。它会根据需求自动调整桶的数量,并对每个桶进行加锁,以减小锁的竞争,提高并发性能。

~MKRMOP$KL3~P{1ODM BZ30 在这个例子中,我们创建了一个 ConcurrentHashMap 实例 map,并添加了三个键值对。内部的 Node 数组会根据键的哈希值来确定存储位置。当我们调用 put 方法添加键值对时,ConcurrentHashMap 会根据键的哈希值找到对应的 Node,并将键值对存储在其中。当我们调用 get 方法获取键值对时,它会根据键的哈希值找到对应的 Node,并返回相应的值。

通过使用 Node 数组,ConcurrentHashMap 能够在并发访问时保证线程安全性,并且通过锁分段技术减小了锁的粒度,提高了并发性能。

lukaliou123 commented 2 years ago

10. 集合框架底层数据结构总结

  1. List: Arraylist: Object[]数组 Vector:Object[]数组 LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)

  2. Set HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素

LinkedHashSet:LinkedHashSet 是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的

TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)

  1. Map HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间

LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》

Hashtable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的

TreeMap: 红黑树(自平衡的排序二叉树) *

11. 如何选用集合?

主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 Map 接口下的集合,需要排序时选择 TreeMap,不需要排序时就选择 HashMap,需要保证线程安全就选用 ConcurrentHashMap。

当我们只需要存放元素值时,就选择实现Collection 接口的集合,需要保证元素唯一时选择实现 Set 接口的集合比如 TreeSet 或 HashSet,不需要就选择实现 List 接口的比如 ArrayList 或 LinkedList,然后再根据实现这些接口的集合的特点来选用。

lukaliou123 commented 1 year ago

12.Comparable 和 Comparator 的区别

Comparable 接口和 Comparator 接口都是 Java 中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥了重要作用: Comparable 接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序 Comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序

一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法,当我们需要对某一个集合实现两种排序方式,比如一个 song 对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo()方法和使用自制的Comparator方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort()

Comparator 定制排序

1691299904392 1691299953629

重写 compareTo 方法实现按年龄来排序

1691300029231 1691300091293