集合
- 1.HashMap底层?扩容机制?1.7-1.8的升级?
- 2.HashMap的长度为什么是2的幂次方?
- 3.HashMap 插入1.7和1.8的区别?
- 4.什么是红黑树?O(logn)
- 5.HashMap为什么会使用红黑树?
- 6.ArrayList底层?扩容机制?
- 7.LinkedList底层?扩容机制?
- 8.ArrayList可以序列化,但是为什么不直接序列化?
- 9.数组和ArrayList的区别,有数组为啥还有ArrayList?
- 10.ConcurrentHashMap?
- 11.HashMap的put的理解?
- 12.HashMap的get的理解?
- 13.HashSet的底层?
- 14.HashSet是如何保证数据不重复的?
- 15.TreeMap?HashMap?HashTable?LinkedHashMap?区别?
- 16.HashSet?LinkedHashSet?TreeSet?区别?
- 17.什么是Hash?什么是Hash冲突?怎么解决的?
- 18.什么是扰动函数?
- 19.集合中的快速失败机制"fail-fast"和安全失败?
- 20.怎么确保一个集合不能被修改?
- 21.Iterator和ListIterator区别?
- 22.移除collection元素?
- 23.遍历list的不同方式?
- 24.ArrayList为什么不直接序列化其内部数组?
- 25.快排时间复杂度?
- 26.什么叫序列化?
- 27.==和equals的区别?
- 28.1<<4是什么意思?
- 29.时间复杂度的排序?
- 30.HashMap中,string、Integer这样的包装类适合作为key么?任何类都可以作为key么?Object作为HashMap的Key,应该怎么做?
- 31.Collection和Collections的区别?
- 32.Collections.sort的原理?
- 33.怎么确保一个集合不被修改?
- 34.哪些集合类是线程安全的?
持续更新中~
1.HashMap底层?扩容机制?1.7-1.8的升级?
- 底层数据结构:
- 1.7:是数组+链表;
- 1.8:底层是数组+链表+红黑树;
- 默认大小16;
- 怎么扩容;
- 创建一个新Entry空数组,是原先的两倍;
- 遍历原有的数组,把之前的数组重新Hash到新数组中;
- 什么时候扩容:负载因子是0.75f,比如100,数量到76的时候就扩容;
2.HashMap的长度为什么是2的幂次方?
为了能让HashMap存取高效,尽量减少碰撞,就是要尽量把数据分配均匀,每个链表/红黑树的长度大致相同。
3.HashMap 插入1.7和1.8的区别?
在 Java 1.7 中,HashMap 使用了数组和链表来存储键值对。插入操作大致分为以下几个步骤:
- 计算哈希值:首先,使用键的
hashCode()
方法计算其哈希值。 - 定位桶:通过哈希值与数组长度减 1 进行位与操作,确定键应该放入哪个桶中。
- 处理哈希冲突:如果桶中已经有元素,就遍历链表,查找是否有相同的键。
- 如果有相同的键,则更新对应的值。
- 如果没有相同的键,则在链表末尾添加新的键值对。
- 扩容:如果链表长度大于阈值(数组长度的 0.75 倍),则对 HashMap 进行扩容,并重新计算所有键的哈希值。
在 Java 1.8 中,HashMap 对插入过程进行了优化,引入了红黑树来处理链表过长的情况。插入操作的大致步骤如下:
- 计算哈希值:同样,使用键的
hashCode()
方法计算其哈希值。 - 定位桶:通过哈希值与数组长度减 1 进行位与操作,确定键应该放入哪个桶中。
- 处理哈希冲突:
- 如果桶中已经有元素,并且元素是链表,则遍历链表,查找是否有相同的键。
- 如果有相同的键,则更新对应的值。
- 如果没有相同的键,则在链表末尾添加新的键值对。
- 如果链表长度大于 8,并且数组长度大于 64,则将链表转换为红黑树。
- 如果桶中已经有元素,并且元素是红黑树,则在红黑树中插入新的键值对。
- 如果桶中已经有元素,并且元素是链表,则遍历链表,查找是否有相同的键。
- 扩容:如果 HashMap 的大小超过了阈值(数组长度的 0.75 倍),则对 HashMap 进行扩容,并重新计算所有键的哈希值。
从 Java 1.7 到 Java 1.8,HashMap 的插入过程在处理哈希冲突时有所不同。Java 1.7 使用链表来处理冲突,而Java 1.8 引入了红黑树来优化链表过长的情况。这种优化可以减少查找时间,提高 HashMap 的性能。
4.什么是红黑树?O(logn)
- 红黑树是一种不严格平衡二叉树,不追求绝对的平衡,允许局部不平衡;
- 根节点是黑色;
- 叶子节点是黑色的时候是空节点,叶子节点不存数据;
- 相邻节点不能同时为红色,红黑被隔开;
- 每个节点到达其可达叶子节点的所有路径,含相同数目的黑色节点。
5.HashMap为什么会使用红黑树?
利用链表对内存的使用率以及红黑树的高效检索,是一种有效的数据结构,对于AVL来说,是一种高度平衡的二叉树,查找效率高,但维持这种高度平衡,每次插入删除都要调整,复杂,耗时,所以用红黑树。
6.ArrayList底层?扩容机制?
-
底层是数组实现的,是一种随机访问模式,实现了RandomAccess接口,所以查询特别快;
-
线程不安全的;
-
查询快,删除更新慢;
-
默认长度是10,也可以指定长度;
-
扩容方式:
- 重新定义n+n/2长度的数组;
- 然后把原数组数据原封不动复制到新数组中;
- 再把原数组的地址换到新数组里;
-
新增:
- 先做一个长度判断,长度不够是需要扩容的,采用位运算,右移一位;
- 在第n位增加一个数,n后所有的元素放在n+1的位置;
-
ArrayList不会初始化数组大小;
-
插入删除一定会慢么?
- 取决于离数组末端有多远;
-
怎样实现删除?
- 删除第n个位置,就是复制n+1后的元素,放到n的位置,就是覆盖了。
-
ArrayList不适合做队列,队列是先入先出,涉及到数据迁移耗费性能;
7.LinkedList底层?扩容机制?
- 底层是双向链表(1.6之前是循环链表);
- 线程不安全的;
- 插入删除快,查询慢;
- 不存在扩容问题,会在需要的时候动态扩容;
- 链表需要内存比数组多;
双向链表是指每个数据节点都有两个指针,分别指向直接后继和直接前驱所以双向链表中的任意一个节点都可以很方便的访问它的前后。
8.ArrayList可以序列化,但是为什么不直接序列化?
出于效率考虑,长度100,但是实际只用50,剩下50不用序列化,提高效率节省空间。
9.数组和ArrayList的区别,有数组为啥还有ArrayList?
数组是一种固定长度的数据结构,可以存储相同类型的元素,并通过索引访问和修改元素。数组的主要优点是在访问元素时具有较高的性能,因为可以通过索引直接定位元素。然而,数组也有一些限制。首先,数组长度固定,无法动态调整大小,这意味着一旦数组创建后,无法添加或删除元素。其次,数组不提供内置的方法来执行常见的操作,如添加、删除、搜索等,需要手动编写代码来处理这些操作。
ArrayList是Java集合框架中的一个类,它是基于数组实现的动态数组。与数组不同,ArrayList的长度是可变的,可以根据需要动态调整大小。ArrayList提供了一组方便的方法来添加、删除、搜索和访问元素,使操作更加简单和灵活。此外,ArrayList还提供了各种实用的方法,如排序、查找、替换等,使数据操作更加方便。另一个重要的区别是数组可以存储基本类型和对象类型,而ArrayList只能存储对象类型。如果需要存储基本类型,可以使用对应的包装类来存储在ArrayList中。
10.ConcurrentHashMap?
ConcurrentHashMap是Java并发包java.util.concurrent中的一个类,它提供了线程安全的HashMap实现。ConcurrentHashMap的底层实现相当复杂,主要是为了提供高并发、高性能的线程安全Map。
底层:
- 1.7-分段数组+链表
- 1.8-Node数组+链表+红黑树+synchronized+cas
ConcurrentHashMap
在 Java 8 中的底层结构是一个分段锁的实现,具体来说是基于分段锁(Segmentation)和哈希表(Hash Table)的结合。它通过将整个哈希表划分为多个小的段(Segment),每个段都维护自己的锁,从而实现并发的读写操作。
分段锁(Segmentation)
在 Java 8 中,ConcurrentHashMap
默认将哈希表划分为 16 个段(Segment),每个段都是一个独立的哈希表,有自己的锁。当线程访问某个键时,会先根据哈希码定位到具体的段,然后对该段加锁。由于不同线程可能访问不同的段,因此它们之间不会互相阻塞,从而提高了并发性能。
哈希表(Hash Table)
每个段内部都是一个标准的哈希表,使用链表或红黑树来解决哈希冲突。当链表长度超过一定阈值(默认为 8)时,链表会转换为红黑树,以提高搜索性能。
读写操作
- 读操作:读操作不需要加锁,因为多个线程可以同时读取同一个哈希表中的数据。
- 写操作:写操作需要加锁,但是只锁定需要操作的段,而不是整个哈希表。当线程需要对某个键进行写操作时,它会先根据哈希码定位到具体的段,然后对该段加锁。其他线程可以同时访问其他段,从而实现并发写操作。
Java 8 的改进
在 Java 8 中,ConcurrentHashMap
引入了一些优化措施,包括:
- 减少锁的粒度:通过将整个哈希表划分为更小的段,可以减少锁的粒度,从而提高并发性能。
- 使用 CAS 操作:在一些场景下,
ConcurrentHashMap
使用 CAS(Compare-And-Swap)操作来更新节点的值,而不是直接加锁。这可以进一步提高并发性能。 - 红黑树优化:当链表长度超过一定阈值时,链表会转换为红黑树,以提高搜索性能。
ConcurrentHashMap
在 Java 8中的底层结构是基于分段锁和哈希表的结合,通过划分哈希表为多个小的段,每个段维护自己的锁,从而实现并发的读写操作。同时,通过引入 CAS操作和红黑树优化等措施,进一步提高了并发性能和搜索性能。
11.HashMap的put的理解?
当put的时候,用key的hash方法,计算出hashcode,放在bucket里,位置是bucketIndex,但是可能存在多个元素找到相同的位置,这时候就会发生hash碰撞了,当碰撞发生,会取到bucketIndex已存储的元素,最终通过equals比较,Hashmap
通过hashcode和equals最终判断key是否已经存在,存在就替换,不存在则插入。
当链表长度超过8后,将后面的数据存到红黑树中,加快检索,每个节点有一个存储位,表示节点颜色红或者黑,对任意一条从根节点到叶子节点的路径上各个节点着色方式限制,红黑树保证没有任意一条从根到叶子的路径超过最短路径的两倍,是一种弱平衡二叉树,对于AVL来说,红黑树旋转次数少,对于搜索插入删除多用红黑树;当元素少于8的时候,查询成本高,不用红黑树。
12.HashMap的get的理解?
首先,get
方法接收一个键作为参数,这个键是我们想要从HashMap
中检索其对应值的键。
第一步,计算哈希值。HashMap
会首先调用该键对象的hashCode()
方法来计算它的哈希值。这个哈希值是一个整数,用于确定键在HashMap
中的存储位置。
第二步,定位桶。HashMap
内部维护了一个数组,这个数组的每个元素称为一个桶。HashMap
会使用计算出的哈希值来定位到具体的桶。这通常是通过将哈希值与HashMap
的容量(capacity)减1进行位与操作来实现的。
第三步,处理哈希冲突。由于不同的键可能产生相同的哈希值(这种情况称为哈希冲突),所以HashMap
在每个桶中维护了一个链表(在Java 8及以后的版本中,当链表长度超过一定阈值时,会转换为红黑树)。get
方法会遍历这个链表(或红黑树),比较链表中的每个键是否与我们要查找的键相等。
第四步,返回对应的值。如果在链表(或红黑树)中找到了与给定键相等的元素,get
方法会返回该元素对应的值。如果没有找到匹配的键,那么get
方法会返回null
。
这个过程中,HashMap
利用了哈希表的数据结构,通过计算哈希值和链表(或红黑树)来高效地存储和检索键值对。同时,HashMap
也进行了一些优化,比如当链表过长时转换为红黑树,以提高搜索性能。"
13.HashSet的底层?
是基于Hashmap实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此HashSet的实现比较简单,相关HashSet的操作基本都是调用HashMap的相关方法来实现的,HashSet不允许重复的值;
HashSet速度慢于HashMap。
14.HashSet是如何保证数据不重复的?
- HashSet中的add()方法会使用HashMap的put方法;
- HashMap的key是唯一的,Hashset的key作为HashMap的key添加进去的,所以跟HashMap一个逻辑,key不能重复;
15.TreeMap?HashMap?HashTable?LinkedHashMap?区别?
-
HashMap:
- 非线程安全
- 初始16
- 扩容2n+1
- 键值允许为null,只允许一个
-
TreeMap:
- 自然顺序排序
- 底层是红黑树
-
HashTable:(不建议用此类)
- 线程安全(用synchronized修饰)
- 数组+链表
- 初始11
- 扩容2n
-
LinkedHashMap:
- 链表
- 按顺序迭代,比HashMap快
-
ConcurrentHashMap:
- 键值不允许为null;
- 线程安全
- 1.7-分段数组+链表
- 1.8-Node数组+链表+红黑树+synchronized+cas+volatile
16.HashSet?LinkedHashSet?TreeSet?区别?
17.什么是Hash?什么是Hash冲突?怎么解决的?
Hash被翻译为散列,就是把任意长度的输入通过散列算法,变成固定长度的输出,该输出就是散列值。
Hash冲突就是:当输入两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做哈希碰撞。
解决办法:
- 使用链地址法(使用散列表)来链接有相同hash值得数据;
- 使用2次扰动函数(哈希函数)来降低哈希冲突的概率,使得数据分布更均匀;
- 引用红黑树进一步降低遍历的时间复杂度,使得遍历的更快;
18.什么是扰动函数?
哈希算法中的扰动函数是一种用于改善哈希值均匀性的技术。这种函数通常用于对输入的原始哈希码进行处理,以消除潜在的模式、增加随机性,并确保哈希值在哈希表中更均匀地分布。扰动函数的设计目的在于确保输出哈希值的均匀性、随机性和减少规律性,以降低哈希冲突的发生概率,并提高哈希表的性能和效率。
常见的扰动算法包括位运算(如位移、按位与、按位异或等)、乘法哈希(通过乘以一个常数因子并提取整数部分来实现)以及混合操作(结合位运算、乘法和其他操作)。这些扰动算法可以使得相似的输入产生不同的输出,增加哈希值的随机性,并使得哈希码在哈希表中分布更加均匀,减少多个键被映射到同一个桶的可能性。
例如,一种常见的扰动函数是将哈希码的高16位与低16位进行异或运算,以此来加大低位的随机性,减少哈希碰撞,并优化散列效果。
19.集合中的快速失败机制"fail-fast"和安全失败?
快速失败(fail-fast)机制:是一种错误检测机制,主要应用于Java集合框架中。当在迭代集合的过程中,该集合在结构上发生改变(如添加、删除元素)时,快速失败机制会立即抛出ConcurrentModificationException异常。这种机制的设计目的是提高软件的可靠性和稳定性,避免在出现问题时让问题持续扩散并造成更严重的后果。
安全失败机制:通过在遍历之前将集合“原件”进行“复印”,使得在遍历过程中对其他线程对原集合的修改不会影响遍历操作。换句话说,当其他线程想要修改集合数据时,修改的是“复印件”,而不是原集合数据。这样,即使原集合在遍历过程中被修改,也不会抛出异常或导致数据不一致。在Java中,CopyOnWriteArrayList就是一个提供安全失败机制的集合类。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历的过程中使用一个modCount变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext、naxt遍历下一个元素前,都会检测modcount变量是否为expectedmodCount值,是的话就返回遍历;否则就抛出异常,终止遍历。
解决办法:
- 在遍历过程中,所有涉及到改变modCount值得地方都加上synchronized
- 使用CopyOnWriteArrayList来代替arrayList;
CopyOnWriteArrayList采用的是读写分离,写时先复制,然后指向新地址
20.怎么确保一个集合不能被修改?
21.Iterator和ListIterator区别?
- Iterator可以遍历Set和List集合;ListIterator只能遍历List;
- Iterator只能单向遍历;ListIterator双向遍历(向前/后遍历);
- ListIterator实现Iterator接口,然后添加了一些额外功能,比如添加、替换等。
22.移除collection元素?
边遍历变修改Collection的唯一正确的方式是使用Iterator.remove().
23.遍历list的不同方式?
- 使用for循环遍历;
- 使用for-each循环(增强型for循环)
- 使用Iterator接口
- 使用Java 8的Stream API
24.ArrayList为什么不直接序列化其内部数组?
- 灵活性:如果
ArrayList
直接序列化其内部的数组,那么每次修改ArrayList
的内部结构(例如,通过添加或删除元素)时,都需要重新序列化整个数组。这会导致不必要的开销,因为只有一部分数据实际上发生了变化。通过将ArrayList
的元素逐个序列化,可以避免这种开销。 - 可扩展性:
ArrayList
的一个主要特点是它可以动态地增长和缩小。如果ArrayList
直接序列化其内部的数组,那么当数组需要增长时,就需要创建一个新的、更大的数组,并将旧数组的内容复制到新数组中。这会增加序列化和反序列化的复杂性。通过逐个序列化元素,可以避免这种复杂性。 - 安全性:直接序列化内部数组可能会暴露
ArrayList
的内部结构,这可能会导致安全问题。例如,攻击者可能会利用这个信息来攻击ArrayList
的实现。通过逐个序列化元素,可以更好地控制哪些信息被暴露。
总的来说,通过逐个序列化元素而不是直接序列化内部数组,
ArrayList
可以提供更好的性能、可扩展性和安全性。
25.快排时间复杂度?
快速排序的时间复杂度为O(n log n);
26.什么叫序列化?
序列化(Serialization)是将对象的状态信息转换为可以存储或传输的字节流的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
27.==和equals的区别?
- 如果你关心的是两个对象是否指向同一个内存地址,那么应该使用==;
- 如果你关心的是两个对象的内容是否相同,那么应该使用equals;
28.1<<4是什么意思?
1<<4
是一个位运算表达式,表示将数字 1
的二进制表示向左移动 4
位。
首先,数字 1
的二进制表示是 0000 0001
(假设我们在这里使用8位二进制数来表示,但实际上使用的位数可能取决于具体的系统和编程语言的实现)。
当我们执行 1<<4
时,这个二进制数向左移动4位,变为 0001 0000
。
在十进制中,0001 0000
等于 16
。
因此,1<<4
的结果是 16
。
29.时间复杂度的排序?
O(1)<O(logn)<O(n)<O(n^2)
30.HashMap中,string、Integer这样的包装类适合作为key么?任何类都可以作为key么?Object作为HashMap的Key,应该怎么做?
- 包装类的特性能够保证Hash值得不可更改性和计算准确性,能够减少Hash碰撞的几率;
- 可以使用任何类作为Map的key;
- 需要重写hashCode和equals方;
31.Collection和Collections的区别?
Collection:是一个集合接口,主要提供了对集合对象进行基本操作的通过接口方法。
Collections:是针对于集合类的一个包装类,主要提供了一系列的静态方法以实现对各种集合的搜索、排序、线程安全化操作;
32.Collections.sort的原理?
Collections.sort
是Java中的一个静态方法,用于对List类型的集合进行排序。其排序原理依赖于所使用的排序算法,具体取决于待排序集合中元素的类型。
- 对于基本数据类型(如int, short, long等):
Collections.sort
通常使用快速排序算法。快速排序是一种分治策略的排序算法,通过选择一个“基准”元素,将集合分为两部分,一部分小于基准,一部分大于基准,然后递归地对这两部分进行排序。 - 对于Object类型:
Collections.sort
通常使用归并排序算法。归并排序也是一种分治策略的排序算法,它将集合不断地分为两半,分别进行排序,然后将排序好的两个集合合并成一个。归并排序是稳定的,即相等的元素在排序后保持原有的顺序。
在选择排序算法时,Collections.sort
也考虑到了性能因素。对于小规模的集合(通常是小于或等于60个元素),Collections.sort
可能会使用插入排序算法,因为插入排序在处理小规模数据时通常比快速排序和归并排序更快。
需要注意的是,Collections.sort
方法通过调用Arrays.sort
方法来实际执行排序操作,因为Collections
类中的许多方法都是基于Arrays
类中的方法实现的。Arrays.sort
方法对于Object类型的数组,如果数组元素实现了Comparable
接口,则使用归并排序;否则,如果提供了Comparator
对象,则使用Timsort算法。
33.怎么确保一个集合不被修改?
可以使用Collections.unmodifiableCollection(Collection c)方法来创建一个只读集合,这样改变集合的任何操作都会抛出Java.lang.UnsupportedOperationExection异常。
34.哪些集合类是线程安全的?
Java 集合框架中,线程安全的集合类包括:
- Vector:Vector 是线程安全的动态数组类,与 ArrayList 类似,但它是同步的。
- Stack:Stack 类是 Vector 的一个子类,它实现了一个后进先出的堆栈。
- Hashtable:Hashtable 是一个散列表,和 HashMap 类似,但是它是同步的,可以用来在多个线程之间共享键值对。
- ConcurrentHashMap:ConcurrentHashMap 是一个线程安全的散列表,它可以在多个线程之间同时使用,而不需要任何额外的同步工具。
- ConcurrentLinkedQueue:ConcurrentLinkedQueue 是一个线程安全的队列,它是非阻塞的,并且能够保证在多线程并发访问时元素顺序的正确性。
此外,Java 并发包(java.util.concurrent)下的其他集合类,如 CopyOnWriteArrayList
、CopyOnWriteArraySet
、BlockingQueue
接口的实现类(如 ArrayBlockingQueue
、LinkedBlockingQueue
、PriorityBlockingQueue
等)也是线程安全的。