面试题七:集合框架
Map接口
HashMap
-
HashMap的容量有什么特点?
HashMap的默认容量是16,默认的最大容量是2^30,默认的加载因子是0.75,在容量达到12*0.75=12时会触发扩容。扩容时,如果扩容后的容量超过最大值,那么容量为最大值,一般情况下为原始容量的2倍。
-
HashMap是怎么进行扩容的?
- 如果使用默认的构造方法,那么第一次插入元素时初始化为默认值,初始容量为16,负载因子为0.7,下一次扩容的门槛是12;
- 如果使用非默认的构造方法,那么第一次插入元素时初始化为扩容门槛,扩容门槛的大小等于传入容量向上最近的2的n次方;
- 如果原始容量大于0,将容量扩充为原来的2倍,扩容门槛也扩充为原来的2倍,扩充后的容量不能大于最大容量;
- 按照新的容量创建一个Node数组;
- 迁移元素,原来的链表拆为2个链表,低位链表保留在原始的位置,高位链表迁移到旧位置+旧容量的新位置;
-
HashMap的put过程?
- 调用
hash(key)
计算key的hash值,如果key为null,返回0,否则调用key.hashcode()
获取hashcode后,让hashcode值得高16位与整个hashcode值异或位操作,以使计算出的hash值更分散; - 如果桶的数量为0,则初始化桶;
- 如果key所在的桶没有元素,直接插入元素;
- 如果key所在的桶中的第一个元素的key与传入的key值相同,说明找到了元素,转步骤9执行;
- 如果第一个元素是树节点,则调用树节点的
putTreeVal()
寻找元素或者插入树节点; - 如果不是以上三种情况,则遍历所在桶的链表,查询key是否存在于链表中;
- 如果找到了key,则转步骤9执行;
- 如果没找到key,则将元素插入队列尾部,并且判断是否需要树化;
- 如果找到了对应的key,则判断是否需要替换旧值,并直接返回旧值;
- 如果插入了元素,则数量加1并判断是否需要扩容;
- 调用
-
HashMap的get过程?
- 调用
hash(key)
计算key的hash值,如果key为null,返回0,否则调用key.hashcode()
获取hashcode后,让hashcode值得高16位与整个hashcode值异或位操作,以使计算出的hash值更分散; - 找到key所在的桶以及第一个元素;
- 如果第一个元素的key等于待查找的key,直接返回;
- 如果第一个元素是树节点,则按照树节点的方式查找,否则按照链表的方式查找;
- 调用
-
HashMap中的元素是否是有序的?有哪些顺序的hashMap实现?
HashMap中的元素是无序的;
顺序的HashMap实现:
-
LinkedHashMap
基于元素进入集合的顺序或者被访问的先后顺序排序
-
TreeMap
基于元素的固有顺序(
Comparator
或者Comparable
决定)
-
-
HashMap何时进行树化?何时进行反树化?
树化:
当桶的数量(数组长度)小于64时,直接扩容,不进行树化;如果桶的数量等于64并且插入新节点后链表长度大于8,此时就会触发树化;
反树化:
当单个桶中元素数量小于6时,进行反树化;
-
HashMap是怎么进行缩容的?
hashMap没有缩容
-
HashMap查询、插入、删除的时间复杂度各是多少?
HashMap的数据结构包含数组、链表、红黑树,数组的查询时间复杂度是O(1),链表的查询时间复杂度是O(k),红黑树的查询时间复杂度是O(logk),k为桶中的元素个数,因此当元素数量非常多的时候,转化为红黑树能极大的提高性能。
-
HashMap中的红黑树实现部分可以用其他数据结构代替吗?
//TODO
-
HashMap的工作原理是什么?
java8中使用数组+链表+红黑树的数据结构实现,实际上HashMap是一个链表散列。
使用
put(key,value)
方法来存储对象,使用get(key)
方法从HashMap中获取对象;当我们给
put(key, value)
方法传递键和值得时候,首先调用hash(key)
方法,返回的hashcode
用来找到桶的未来以存储Entry对象。 -
当两个对象的hashCode值相等会发生什么?
因为hashcode值相等,那么就会映射到相同的桶中,出现哈希碰撞;
此时会以链表的方式存储数据;
-
如何解决哈希碰撞?
理想情况下,使用散列函数计算出来的每一个关键字的散列值都应该不同,但是现实是这种情况很难出现,如果两个不同的关键词通过散列函数获取到了相同的散列值,那么此时就会出现哈希碰撞。
解决方法:
- 直接定址法
- 开放定址法
- 链地址法(HashMap使用的就是链地址放)
- 除留余数法
链地址法的优点:
- 处理简单,且无堆积现象,即非同义词绝不会发生冲突,因此平均查找长度较短;
- 各链表上的节点空间是动态申请的,因此适合用于造表前无法确切知道表长的场景;
链地址法的缺点:
- 指针需要额外的空间;
- 查找时候需要遍历链表;
-
hashcode和equals方法为什么重要?
HashMap使用key对象的
hashcode()
和equals()
去确定键值对的索引。当从HashMap中查询数据的时候这两个方法也会被用到。如果这两个方法没有被正确实现,在这种情况下,两个不同的key也许会产生相同的
hashcode()
和equals()
输出,此时HashMap将会认为这两者是相同的,然后就会覆盖掉一个值,而不是存储到不同的地方。 -
能否使用任何类作为Map的实现?
可以,遵循以下几点即可:
- 如果类重写了
equals
方法,那么也要重写hashCode
方法; - 类的所有实例都需要遵循与
equals
和hashcode
相关的规则; - 如果一个类没有使用
equals
方法,那么不应该使用它的hashcode
方法; - 用户自定义key类的最佳实践是使之为不可变的,这样hashcode值可以被缓存起来,拥有更好的性能。
- 如果类重写了
-
HashMap的长度为什么是2的幂次方?
为了能让HashMap存取更加高效,尽量减少碰撞,也就是尽量把数据分散均匀,每个链表/红黑树的长度大致相同。
主要原因:
- 计算方便:当容量一定是2^n时,h & (length - 1) == h % length,扩容时非常方便计算新的位置,只需要是原来的位置+旧容量就是新的位置;
- 分布均匀:如果不是2的幂次方,那么有些位置永远不会被使用到,有些位置碰撞的概率大大增加,这显然于hash均匀分布的原则不符合;
- 提高效率: 二进制位操作
&
的效率高于取余操作%
;
-
HashMap在多线程环境中什么时候会出现问题?
ConcurrentHashMap
-
HashMap和ConcurrentHashMap的区别;
-
HashMap是线程不安全的,ConcurrentHashMap是线程安全的;
-
ConcurrentHashMap不能存储key或者value为null的元素,HashMap支持;
-
HashMap中有初始容量和负载因子且可以手动传入,ConcurrentHashMap没有直接传入负载因子的地方,直接写死是0.75;
-
ConcurrentHashMap中通过sizeCtl来表达各个阶段;
- -1,表示正在初始化;
- 0,默认值,表示后续在真正初始化时使用默认容量;
- >0,在初始化之前,存储的是传入的容量,在初始化或者扩容之后存储的是下一次扩容的门槛;
sizeCtl = (resizeStamp << 16) + (1 + nThreads)
,表示正在进行扩容,高位存储扩容邮戳,低位存储扩容线程数加1;
-
ConcurrentHashMap扩容时使用链表逆序的方式遍历链表;
不同之处:ConcurrentHashMap 是线程安全的,多线程环境下,无需加锁直接使用ConcurrentHashMap 多了转移节点,主要用户保证扩容时的线程安全;
相同之处:都是数组 +链表+红黑树的数据结构(JDK8之后),所以基本操作的思想一致都实现了Map接口,继承了AbstractMap 操作类,所以方法大都相似,可以相互切换.
-
-
ConcurrentHashMap的存储结构?
数组+链表+红黑树
-
ConcurrentHashMap是怎么保证并发安全的?
主要是使用CAS操作+Synchronized保证并发安全;
CAS负责并发安全的修改对象属性的值或者数据某个位置的值;
Synchronized负责给桶加锁,使用分段锁提高并发能力;
-
ConcurrentHashMap使用了哪些锁?
put过程中使用的锁有CAS,自旋锁,Synchronized, 分段锁
-
ConcurrentHashMap是怎么扩容的?
- 元素个数的存储方式类似于LongAdder类,存储在不同的段上,减少不同线程同时更新size时的冲突;
- 计算元素个数时把这些段的值及baseCount相加算出总的元素个数;
- 正常情况下sizeCtl存储着扩容门槛,扩容门槛为容量的0.75倍;
- 扩容时sizeCtl高位存储扩容邮戳(resizeStamp),低位存储扩容线程数加1(1+nThreads);
- 其它线程添加元素后如果发现存在扩容,也会加入的扩容行列中来;
-
ConcurrentHashMap的
size()
方法实现?获取元素个数时是没有加锁的;
步骤:
- 元素的个数依据不同的线程存在在不同的段里;
- 计算CounterCell所有段及baseCount的数量之和;
- 获取元素个数没有加锁;
-
ConcurrentHashMap是强一致性的吗?
ConcurrentHashMap查询操作没有加锁,因此不是强一致性的。
-
ConcurrentHashMap不能解决什么问题?
private static final Map<Integer, Integer> map = new ConcurrentHashMap<>(); public void unsafeUpdate(Integer key, Integer value) { Integer oldValue = map.get(key); if (oldValue == null) { map.put(key, value); } }
这里如果有多个线程同时调用unsafeUpdate()这个方法,ConcurrentHashMap还能保证线程安全吗?
答案是不能。因为get()之后if之前可能有其它线程已经put()了这个元素,这时候再put()就把那个线程put()的元素覆盖了。此时可以使用
putIfAbsent()
;不能听说ConcurrentHashMap是线程安全的,就认为它无论什么情况下都是线程安全的
-
ConcurrentHashMap哪些地方用到了分段锁的思想?
put
插入数据时使用了分段锁;size()
查询个数时也用到了分段思想;remove()
删除数据时使用了分段锁;transfer()
扩容时迁移元素时用到了分段锁; -
ConcurrentHashMap中
put()
过程?- 判断桶是否已经被初始化了,未初始化时需要先初始化;
- 计算待插入的元素的key的hash值,从而确定桶的位置;
- 如果桶中没有元素,则尝试(CAS)将待插入元素插入到该桶的第一个位置;
- 如果正在扩容,那么当前线程加入帮助扩容;
- 如果当前桶即非空,也没有在扩容,那么使用分段锁锁住该桶;
- 如果当前桶中以链表的方式存储数据,那么在链表中查找或者插入数据;
- 如果当前桶中以树的方式存储数据,那么在红黑树中查找或者插入数据;
- 如果元素存在,那么就返回旧值;
- 如果元素不存在,整个Map的个数加1,并检查是否需要扩容;
put
过程中的分段锁为什么使用Synchronized而不是ReentrantLock?因为Synchronized经过优化之后,某些情况下性能并不比ReentrantLock差。
-
扩容期间在未迁移到的hash桶插入数据会发生什么?
只要插入的位置扩容线程还未迁移到,就可以插入,当迁移到该插入的位置时,就会阻塞等待插入操作完成再继续迁移 。
-
正在迁移的hash桶遇到 get 操作会发生什么?
在扩容过程期间形成的 hn 和 ln链 是使用的类似于复制引用的方式,也就是说 ln 和 hn 链是复制出来的,而非原来的链表迁移过去的,所以原来 hash 桶上的链表并没有受到影响,因此如果当前节点有数据,还没迁移完成,此时不影响读,能够正常进行。如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时get线程会帮助扩容。
-
正在迁移的hash桶遇到 put/remove 操作会发生什么?
如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时写线程会帮助扩容,如果扩容没有完成,当前链表的头节点会被锁住,所以写线程会被阻塞,直到扩容完成。
-
如果 lastRun 节点正好在一条全部都为高位或者全部都为低位的链表上,会不会形成死循环?
在数组长度为64之前会导致一直扩容,但是到了64或者以上后就会转换为红黑树,因此不会一直死循环 。
-
扩容后 ln 和 hn 链不用经过 hash 取模运算,分别被直接放置在新数组的 i 和 n + i 的位置上,那么如何保证这种方式依旧可以用过 h & (n - 1) 正确算出 hash 桶的位置?
如果 fh & n-1 = i ,那么扩容之后的 hash 计算方法应该是 fh & 2n-1 。 因为 n 是 2 的幂次方数,所以 如果 n=16, n-1 就是 1111(二进制), 那么 2n-1 就是 11111 (二进制) 。 其实 fh & 2n-1 和 fh & n-1 的值区别就在于多出来的那个 1 => fh & (10000) 这个就是两个 hash 的区别所在 。而 10000 就是 n 。所以说 如果 fh 的第五 bit 不是 1 的话 fh & n = 0 => fh & 2n-1 == fh & n-1 = i 。 如果第5位是 1 的话 。fh & n = n => fh & 2n-1 = i+n 。
-
并发情况下,各线程中的数据可能不是最新的,那为什么 get 方法不需要加锁?
get操作全程不需要加锁是因为Node的成员val是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
-
ConcurrentHashMap 和 Hashtable 的区别?
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
底层数据结构:JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable是采用 数组+链表 的形式。
实现线程安全的方式(重要):
① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。
② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
-
扩容过程中,读访问能否访问的到数据?怎么实现的?
可以的。当数组在扩容的时候,会对当前操作节点进行判断,如果当前节点还没有被设置成fwd节点,那就可以进行读写操作,如果该节点已经被处理了,那么当前线程也会加入到扩容的操作中去。
-
为什么超过冲突超过8才将链表转为红黑树而不直接用红黑树?
默认使用链表, 链表占用的内存更小正常情况下,想要达到冲突为8的几率非常小,如果真的发生了转为红黑树可以保证极端情况下的效率
-
ConcurrentHashMap 和HashMap的扩容有什么不同?
HashMap的扩容是创建一个新数组,将值直接放入新数组中,JDK7采用头链接法,会出现死循环,JDK8采用尾链接法,不会造成死循环;
ConcurrentHashMap 扩容是从数组队尾开始拷贝,拷贝槽点时会锁住槽点,拷贝完成后将槽点设置为转移节点。所以槽点拷贝完成后将新数组赋值给容器;
-
ConcurrentHashMap 是如何发现当前槽点正在扩容的?
ConcurrentHashMap 新增了一个节点类型,叫做转移节点,当我们发现当前槽点是转移节点时(转移节点的 hash 值是 -1),即表示 Map 正在进行扩容.
-
描述一下 CAS 算法在 ConcurrentHashMap 中的应用?
CAS是一种乐观锁,在执行操作时会判断内存中的值是否和准备修改前获取的值相同,如果相同,把新值赋值给对象,否则赋值失败,整个过程都是原子性操作,无线程安全问题;
ConcurrentHashMap 的put操作是结合自旋用到了CAS,如果hash计算出的位置的槽点值为空,就采用CAS+自旋进行赋值,如果赋值是检查值为空,就赋值,如果不为空说明有其他线程先赋值了,放弃本次操作,进入下一轮循环
LinkedHashMap
-
LinkedHashMap是怎么实现的?
LinkedHashMap继承了HashMap并且实现了Map接口,拥有HashMap的所有特性。
在HashMap的数组+链表+红黑树的基础上,又添加了双向链表的结构存储数据的顺序,因此在删除和添加元素的过程中,除了要维护数组、链表、红黑树三种数据结构外,还需要维护LinkedList中的存储,效率要比HashMap低。
-
LinkedHashMap是有序的吗?怎么个有序法?
默认是按照插入顺序排序,可以通过传入参数实现按照访问顺序排序;
-
LinkedHashMap如何实现LRU缓存淘汰策略?
只需要设置
accessOrder=true
,并且重写removeEldestEntry()
方法即可,示例代码如下:class LRU<k,v> extends LinkedHashMap<k,v> { private int capacity; public LRU(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, true); this.capacity = initialCapacity; } @Override protected boolean removeEldestEntry(Map.Entry<k, v> eldest) { return size()>capacity; } }
原理:
- 在插入元素后,回调钩子方法
afterNodeInsertion(boolean evict)
,HashMap的put()
方法默认参数evict
为true; - ``afterNodeInsertion()
在HashMap中为空实现,在LinkedHashMap中有具体的实现,判断
evict && (first = head) != null && removeEldestEntry(first),即判断evict 为true,双向链表头结点不为空,以及
removeEldestEntry()的返回值,默认该方法返回
false`,因此只需要重写该方法即可; - 在节点被访问之后,
put()
或者get()
被调用,将回调afterNodeAccess()
方法,如果此时accessOrder=true
,那么将当前节点移动到链表的末尾,末尾是最新访问的元素;
- 在插入元素后,回调钩子方法
TreeMap
-
TreeMap中是怎么遍历的?
按照key值的大小进行遍历
-
TreeMap插入、删除、查询元素的时间复杂度各是多少?
约等于O(n)
-
TreeMap就有序的吗?怎么个有序法?
按照key值大小排序,初始化时要么传入比较器,要么就将key值实现Comparable接口
-
TreeMap是否需要扩容?
没有扩容的概念
-
TreeMap和LinkedHashMap的区别?
- 数据结构不同;
- 有序的方式不同,LinkedHashMap是插入序或者访问序有序,TreeMap是key值大小排序;
-
TreeMap数据结构?
只有一颗红黑树
WeakHashMap
-
WeakHashMap使用的数据结构?
数组+链表
-
WeakHashMap具有什么特性?
- 没有实现Clone和Serializable接口,所以不具有克隆和序列化的特性。
- 内部的key会存储为弱引用,当jvm gc的时候,如果这些key没有强引用存在的话,会被gc回收掉,下一次当我们操作map的时候会把对应的Entry整个删除掉,基于这种特性,WeakHashMap特别适用于缓存处理。
-
WeakHashMap通常用来做什么?
WeakHashMap特别适用于缓存处理
-
WeakHashMap使用String作为key是需要注意些什么?为什么?
使用String作为key时,一定要使用new String()这样的方式声明key,才会失效,其它的基本类型的包装类型是一样的;
List接口
-
ArrayList和LinkedList有什么区别?
数据结构不同,ArrayList是数组,LinkedList是双链表结构;
LinkedList不仅可以作为list,还可以作为双端队列以及栈;
ArrayList支持随机访问,LinkedList不支持随机访问;
ArrayList查询效率较高,LinkedList插入删除数据效率较高;
-
ArrayList是怎么扩容的?
- 检查是否需要扩容;
- 如果数组对象等于
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
则初始化为默认容量10; - 扩容到原始容量的1.5倍(
oldCapacity + (oldCapacity >> 1)
),扩容后的容量还是小于实际需要容量,则以实际需要容量为准; - 创建新容量的数组,并把内容拷贝到新数组;
-
ArrayList插入、删除、查询元素的时间复杂度各是多少?
添加元素到末尾,时间复杂度O(1);
添加到指定位置,时间复杂度O(n);
查询指定位置元素,时间复杂度O(1);
删除指定位置元素/指定元素,时间复杂度为O(n);
-
怎么求两个集合的并集、交集、差集?
求并集:
addAll()
求交集:
retainAll()
单方向差集:
removeAll()
-
ArrayList是怎么实现序列化和反序列化的?
实现Serializable接口
-
集合的方法toArray()有什么问题?
容易出现类型转换异常
ClassCastException
解决方案,使用
toArray(T[] a)
-
什么是fail-fast?
java集合的一种异常检测机制。当多个线程对部分集合进行结构上的改变的操作时,有可能会产生fail-fast机制。这个时候会抛出
ConcurrentModificationException
。异常重现:使用增强for循环遍历list时,在循环体中调用
add()
或者remove()
方法时就会出现该异常。核心代码:
final void checkForComodification() { if (modCount != expectedModCount) //modCount在集合类中,随着集合改变次数而增加 //expectedModCount,集合内部的类中的属性,只有使用迭代器修改集合该值才会改变 throw new ConcurrentModificationException(); }
原因:
modCount在集合类中,随着集合改变次数而增加;
expectedModCount,集合内部的类中的属性,只有使用迭代器修改集合该值才会改变,在开始遍历前该值等于modCount。
增强for循环的本质是迭代器,在循环体中使用
add()
或者remove()
方法时会改变modCount的值,导致modCount和expectedModCount不相等,这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改!所以,在使用Java的集合类的时候,如果发生CMException,优先考虑fail-fast有关的情况,实际上这里并没有真的发生并发,只是Iterator使用了fail-fast的保护机制,只要他发现有某一次修改是未经过自己进行的,那么就会抛出异常。
-
LinkedList是单链表还是双链表实现的?
双向链表
-
LinkedList除了作为List还有什么用处?
实现了Queue和Deque接口,可以作为双端队列来使用(无界队列),也可以当作栈来使用。
-
LinkedList插入、删除、查询元素的时间复杂度各是多少?
在队列头部或者尾部添加/删除元素,时间复杂度为O(1);
在队列中间添加/删除元素,时间复杂度是O(n);
-
什么是随机访问?
实现RandomAccess接口,支持快速随机访问策略;
实现了该接口的集合,使用for循环遍历优于使用迭代器遍历。
-
哪些集合支持随机访问?他们都有哪些共性?
ArrayList
CopyOnWriteArrayList
共性:实现了RandomAccess接口,提供随机访问能力
-
CopyOnWriteArrayList是怎么保证并发安全的?
使用可重入锁ReentrentLock
-
CopyOnWriteArrayList的实现采用了什么思想?
读写分离的思想,写数据时都复制一个新的数组进行操作,操作完成后再替换旧数组
-
CopyOnWriteArrayList是不是强一致性的?
CopyOnWriteArrayList只支持最终一致性,但是不能保证实时一致性;
-
CopyOnWriteArrayList适用于什么样的场景?
由于写的时候占用内存比较多(复制数据),空间复杂度是O(n),但是读操作支持随机访问,时间复杂度是O(1),适合读多写少的场景;
-
CopyOnWriteArrayList插入、删除、查询元素的时间复杂度各是多少?
查询的时间复杂度是O(1);
添加元素到末尾,时间复杂度O(1);
添加到指定位置,时间复杂度O(n);
删除指定位置元素,时间复杂度为O(n);
-
CopyOnWriteArrayList为什么没有size属性?
因为底层是拷贝数据,直接使用数组长度即可获取到集合中元素数量,因此不需要size属性
-
CopyOnWriteArrayList和Vector 比较?
- 都是线程安全的
- 实现原理不同,Vector 使用Synchronized加锁,CopyOnWriteArrayList使用ReentrentLock加锁;
- 并发性能CopyOnWriteArrayList高于Vector
-
如何比较两个list完全相等?
private static <T> boolean eq(List<T> list1, List<T> list2) { if (list1.size() != list2.size()) { return false; } // 标记某个元素是否找到过,防止重复 boolean matched[] = new boolean[list2.size()]; outer: for (T t : list1) { for (int i = 0; i < list2.size(); i++) { // i这个位置没找到过才比较大小 if (!matched[i] && list2.get(i).equals(t)) { matched[i] = true; continue outer; } } return false; } return true; }
设定一个标记数组,标记某个位置的元素是否找到过
Set接口
-
HashSet怎么保证添加元素不重复?
HashSet的值作为底层HashMap的key,因此需要计算hashcode值来判断在桶的位置,同时与其他的值的hashcode值进行比较。
如果没有发现相同hashcode值,HashSet会假设对象没有重复出现;
如果发现hashcode值有重复的,调用
equals
放比较对象是否真的相同,如果两者相同,那么就是重复的,加入失败,如果两者不相同,那么就是不重复的,可以加入。 -
HashSet是有序的吗?
无序的,底层实现为HashMap,HashMap的key值是无序的
-
HashSet是否允许null元素?
允许,HashMap的key值允许是null
-
Set是否有get()方法?
没有,只有一个
contains()
方法 -
LinkedHashSet底层使用什么存储数据?
底层使用LinkedHashMap存储数据
-
LinkedHashSet与HashSet有什么不同?
LinkedHashSet是有序的,HashSet是无序的;底层存储数据的结构不同,LinkedHashSet是LinkedHashMap,HashSet是HashMap。
-
LinkedHashSet是有序的吗?怎么个有序法?
有序的,按照插入顺序排序
-
LinkedHashSet支持按元素访问顺序排序吗?
不支持,LinkedHashSet构造方法中没有提供修改accessOrder值的方法,默认是false,因此是按照插入顺序排序;
-
TreeSet真的是使用TreeMap来存储元素的吗?
源码中使用的是
NavigableMap<E,Object> m
,不一定是个TreeMap;默认构造方法是使用TreeMap实现,代码如下:
public TreeSet() { this(new TreeMap<E,Object>()); }
-
TreeSet是有序的吗?怎么个有序法?
是有序的,实现了SortedSet接口,它的有序性主要依赖于NavigableMap的有序性,而NavigableMap又继承自SortedMap,这个接口的有序性是指按键的自然排序保证的有序性,而键的自然排序又有两种实现方式,一种是密钥实现Comparable接口,一种是构造方法放置Comparator比较器
-
TreeSet和LinkedHashSet有何不同?
LinkedHashSet并没有实现SortedSet接口,它的有序性主要依赖于LinkedHashMap的有序性,所以它的有序性是指按照插入顺序保证的有序性;
而TreeSet实现了SortedSet接口,它的有序性主要依赖于NavigableMap的有序性,而NavigableMap又继承自SortedMap,这个接口的有序性是指按键的自然排序保证的有序性,而键的自然排序又有两种实现方式,一种是密钥实现Comparable接口,一种是构造方法放置Comparator比较器。
-
TreeSet和SortedSet有什么区别和联系?
TreeSet实现了NavigableSet接口,NavigableSet接口继承了SortedSet接口,因此可以说TreeSet实现了SortedSet接口。
-
CopyOnWriteArraySet是用Map实现的吗?
不是,使用的CopyOnWriteArrayList实现的,添加时调用CopyOnWriteArrayList的
addIfAbsent()
方法,只有元素不存在时才添加 -
CopyOnWriteArraySet是有序的吗?怎么个有序法?
有序的,底层是CopyOnWriteArrayList,数组是有序的
-
CopyOnWriteArraySet怎么保证并发安全?
底层是CopyOnWriteArrayList,使用ReentrantLock加锁保证并发安全,且是读写分离的;
-
CopyOnWriteArraySet以何种方式保证元素不重复?
使用的CopyOnWriteArrayList实现的,添加时调用CopyOnWriteArrayList的
addIfAbsent()
方法,只有元素不存在时才添加 -
如何比较两个Set中的元素是否完全一致?
因为Set中的元素并不重复,所以只要先比较两个Set的元素个数是否相等,再作一次两层循环就可以了
private static <T> boolean eq(Set<T> set1, Set<T> set2) { if (set1.size() != set2.size()) { return false; } for (T t : set1) { // contains相当于一层for循环 if (!set2.contains(t)) { return false; } } return true; }
Queue
// TODO
Deque
// TODO