面试必备八股文第二篇(集合容器)

面试必备八股文第二篇(集合容器)

List和Set、Map的区别

List 以索引来存取元素,有序的,元素是允许重复的,可以插入多个null。
Set 不能存放重复元素,无序的,只允许一个null
Map 保存键值对映射,映射关系可以一对一、多对一

List 有基于数组、链表实现两种方式
Set、Map 容器有基于哈希存储和红黑树两种方式实现
Set 基于 Map 实现,Set 里的元素值就是 Map的键值

写一段代码在遍历 ArrayList 时移除一个元素

1、foreach删除会导致快速失败问题
2、fori顺序遍历会导致重复元素没删除,所以正确解法如下:

Iterator itr = list.iterator();
while(itr.hasNext()) {
      if(itr.next().equals("jay")) {
        itr.remove();
      }
}

Collection 和 Collections的区别?

Collection是集合类的上级接口,继承于他的接口主要有Set 和List
Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作

Collections.sort和Arrays.sort的实现原理

Collection.sort是对list进行排序,Arrays.sort是对数组进行排序。
Collections.sort方法调用了list.sort方法,list.sort方法调用了Arrays.sort的方法

你了解过ArrayList源码吗?

jdk1.7:底层类似饿汉式,底层创建了长度是10的 Object[] elementData,扩容1.5倍
jdk1.8:懒汉式,第一次调用add()时,底层才创建了长度10的数组,扩容1.5倍

你了解过LinkedList的源码吗?

默认size=0,核心里面有一个Node节点,里面有前指针和后指针

快速失败机制和安全失败

快速失败机制:
是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 failfast 机制。
底层就是在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount 的值。
每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测 modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

安全失败:
采用安全失败机制的集合容器(CopyOnWriteArrayList),在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

怎么确保一个集合不能被修改?

可以使用 Collections.unmodifiableCollection(Collection c) 方法来创建一个 只读集合,
这样改变集合的任何操作都会抛出 Java.lang.UnsupportedOperationException 异常

如何边遍历边移除 Collection 中的元素?

Iterator<Integer> it = list.iterator(); 
while(it.hasNext()){ 
  it.remove();
}

Iterator 和 ListIterator 有什么区别?

Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

谈谈使用Iterator 遇到过的问题?或者注意事项?

1、遍历时候,禁止使用next方法判断有没有存在元素,禁止调用两次next方法,否则元素会跳着输出并且抛NoSuchElementException
while(null != iterator.next()){
print(iterator.next());
}
2、禁止在调用next()之前使用remove()方法
3、正确用法:使用hasNext()

如何实现数组和 List 之间的转换?

数组转 List:使用 Arrays.asList(array) 进行转换。
List 转数组:使用 List 自带的 toArray() 方法。

ArrayList 、 Vector、LinkedList 的区别是什么?

1、ArrayList线程不安全,Vector线程安全
2、ArrayList和Vector是动态数组,LinkList是链表
3、ArrayList扩容1.5倍,Vector扩容2倍,LinkList是链表就没有扩容
4、ArrayList和Vector底层默认初始化容量都是10,Vector初始化就加载,ArrayList在JDK1.8变成懒加载,add()才进行初始化

多线程场景下如何使用 ArrayList?

Collections.synchronizedList(list)

List和Set区别?

List:有序,可以重复
Set:无序,不可以重复

HashSet如何检查重复?HashSet是如何保证数据不可重复的?

hashcode和equals比较

谈谈TreeMap底层?

TreeMap实现了SotredMap接口,它是有序的集合。
TreeMap底层数据结构是一个红黑树,每个key-value都作为一个红黑树的节点。
如果在调用TreeMap的构造函数时没有指定比较器,则根据key执行自然排序。

HashMap工作原理

JDK1.8,Node数组+链表+红黑树实现

插入元素putVal(key,val)
1、首先采用高16位进行异或计算hash,如果初始化容量为空,进行第一次resize()
2、确定索引i = (n - 1) & hash
3、如果hashCode相同,key相同则替换元素,否则就是散列冲突插入到链表或红黑树(采用拉链法,jdk1.8使用尾插法避免高并发下形成循环链表)

获取元素get(key)
1、根据key获取元素,和插入一样,首先对key进行hash,再确定索引位置
2、单个Node类型直接返回(桶内链表遍历链表/红黑树查找)

HashMap默认初始容量是16,如果指定为18,那初始容量会变成多少?

如果指定为18,那会自动设置成32,即都是2的n次方
源码里面通过不断右移计算出最接近于初始化容量的2的n次方

HashMap链表转为红黑树的条件

数组长度大于64且链表大于8时,转成红黑树;数组容量未到64只进行扩容;resize()的时候红黑树节点小等6时退化成链表;

HashMap的长度为什么是 2 的幂次方

位运算&效率比取模%效率高,length是2的n次方才能保证 hash % length == hash & (length - 1) 。

例子:
10100101 11000100 00100101 hash值
& 00000000 00000000 00001111 (length - 1)=16-1=15
----------------------------------
00000000 00000000 00000101 //高位全部归零,只保留末四位

核心原因是hash函数的源码中右移了16位让低位保留高位信息,原本的低位信息不要,那么进行&操作另一个数低位必须全是1,否则没有意义,
所以len必须是2 ^ n ,这样能实现分布均匀,有效减少hash碰撞!

总结两个原因:
一:不是2的次幂的话,不管是奇数还是偶数,就肯定注定了某些脚标位永远是没有值的,而某些脚标位永远是没有值的,就意味着浪费空间,会让数据散列的不充分,这对HashMap来说绝对是个灾难!
二:扩容迁移的时候不需要再重新通过哈希定位新的位置了。扩容后,元素新的位置,要么在原脚标位,要么在原脚标位+扩容长度这么一个位置

计算hash值底层实现?

(h = k.hashCode()) ^ (h >>> 16),hash函数是先拿到通过key的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或

那你知道hash值底层为什么这么设计吗?

1、一定要尽可能降低 hash 碰撞,越分散越好;
2、算法一定要尽可能高效,因为这是高频操作, 因此采用位运算。

为什么采用 hashcode 的高 16 位和低 16 位异或能降低 hash 碰撞?hash 函数能不能直接用 key 的 hashcode?

右位移 16 位,正好是 32bit 的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
int值范围可以支持40亿的映射空间,只要哈希函数映射得比较均匀松散,一般很难碰撞,但是一个 40 亿长度的数组,内存是放不下的,所以不能直接用 key 的 hashcode

HashMap的扩容因子

默认0.75,也就是会浪费1/4的空间,达到扩容因子时,会扩容一倍,0.75 是时间与空间一个平衡值;

为什么HashMap负载因子被设置成了0.75,不是1或者是0.5?

假如把负载因子设置成1,默认容量使用初始值16,那么HashMap需要在"满了"之后才会进行扩容。
那样的话在HashMap中,最理想的状况是这16个元素通过hash算法之后分别落到了16个不同的桶中,否则就会发生哈希碰撞。而且随着元素越来越多,哈希碰撞的概率越就越来越大,查找速度也会越来越低。

总结:
loadFactor设置的太大,例如1,哈希冲突的概率就会很高,同时会大大降低查询的效率。
loadFactor的值也不能太小,例如0.5,这样就会频繁扩容,会大大浪费空间。
如果loadFactor的值是3/4的话,那么和capacity的乘积结果就可以是一个整数。

HashMap的扩容操作是怎么实现的?

1、resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容
2、每次扩展的时候,都是扩展2倍
3、在putVal()中里面使用到了2次resize(),第一次初始化会进行扩容,或者该数组的实际大小 > 临界值(第一次为16*0.75=12),采用尾插法,这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,1.7还需要重新计算hash值

JDK 1.8 对 hash 函数做了优化,1.8 还有别的优化吗?

1、数组+链表改成了数组+链表或红黑树;
2、头插法改成了尾插法
3、扩容的时候 1.7 需要对原数组中的元素进行重新 hash 定位在新数组的位置,1.8 采用更简单的判断逻辑,位置不变或索引+旧容量大小
4、在插入时,1.7 先判断是否需要扩容,再插入,1.8 先进行插入,插入完成再判断是否需要扩容。

你分别跟我讲讲为什么要做这几点优化?

1、防止发生 hash 冲突,链表长度过长,将时间复杂度由O(n)降为O(logn);
2、因为 1.7 头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环

HashMap是怎么解决哈希冲突的?

使用链地址法:数组+链表
1、将hash冲突的组成一个链表放在bucket下
2、HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4 (即2的四次方16),远小于int类型范围,单纯使用%碰撞概率大,因为单纯使用低位运算,而且最坏情况HashMap变成一个单链表
3、使用Key的hash值与该值的高16位做异或操作(jdk1.8优化)
4、引入红黑树进一步降低遍历的时间复杂度,使得遍历更快

拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?

二叉树在特殊情况下也会变成线性结构,查询效率低
因为单链表长度<=8时候,使用单链表即可,因为红黑树需要左旋右旋变色来保持平衡

能否使用任何类作为 Map 的 key?

可以使用任何类,但是重写equals,必须要重写hashCode

为什么HashMap中String、Integer、Long这样的包装类适合作为K?

因为这些都是final,不可变,保证hash值的不可更改性和计算准确性,有效减少hash冲突,而且内部已经重写equals和hashcode

HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

hashCode()方法返回的是int整数类型,其范围为(231)~(231-1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~ 2^30,
HashMap一般取不到大值,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置

HashMap 与 HashTable 有什么区别?

  1. 线程安全:HashMap 是非线程安全的,HashTable 是线程安全的;
  2. 效率:因为线程安全的问题,HashMap要比HashTable效率高一点。另外,HashTable基本被淘汰,不要在代码中使用它;
  3. HashMap支持Null key和Null Value,HashTable直接抛空指针

HashMap 内部节点是有序的吗?

是无序的,根据 hash 值随机插入。

那有没有有序的 Map?

LinkedHashMap 和 TreeMap。

HashMap 和 ConcurrentHashMap 的区别

HashMap:key和value都允许为null | 线程不安全 | 使用存储threshold和loadFactor来扩容
ConcurrentHashMap:k-V不能null | 线程安全,桶数组进行了分割分段(Segment),分段锁,jdk1.8加入CAS算法 | 采用sizeCtl来控制扩容

ConcurrentHashMap 和 Hashtable 的区别?

Hashtable:线程安全 | 全表锁,效率低
ConcurrentHashMap:线程安全 | 1.7分段锁,默认16个段,效率比较高,1.8跟HashMap一样,数组+链表+红黑树,syns+cas操作,但是保留Segment的数据结构

ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?

ConcurrentHashMap 成员变量使用 volatile 修饰,免除了指令重排序,同时保证内存可见性
jdk1.7: 数据分段,每段一把锁,一个ConcurrentHashMap里包含一个Segment数组,一个Segment包含HashEntry数组,HashEntry是链表。要对HashEntry进行操作,必须先获得Segment锁,这个Segment锁实际就是可重入锁ReentrantLock
jdk1.8: 用Node+CAS+Synchronized,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率提高

ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?

①、粒度降低了;
②、JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。
③、在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。

TreeSet和TreeMap区别与关系

TreeSet里绝大部分方法都是直接调用TreeMap的方法来实现的,底层实际上是一个TreeMap实例(红黑树),可以实现排序功能

Collections.synchronizedList和CopyOnWriteArrayList性能分析

CopyOnWriteArrayList在线程对其进行变更操作的时候,会拷贝一个新的数组以存放新的字段,因此写性能很差
Collections.synchronizedList读操作采用了synchronized,因此读性能较差

ConcurrentHashMap的红黑树实现分析

红黑树的特点:
1、根节点是黑色
2、叶子节点是黑色
3、根节点到每个叶子节点的黑色节点个数都一样
4、如果一个节点是红色,它的子节点一定是黑色

实现原理:
当链表长度达到8个时 + Node数组长度小于默认值64,先扩容2倍来缓解单链表的压力,之后才会把链表转成红黑树

红黑树构造过程:
1、遍历Node链表,生成对应TreeNode链表,里面每一个节点,就相当于红黑树的节点,非黑即红,节点的值就是hash值
2、根节点设置为黑色,后面通过比较hash值来决定当左节点或右节点,新加入的节点设置为红色
3、新增节点可能会破坏结构,使用左旋或右旋解决

ConcurrentHashMap 的并发度是什么?

默认16,当用户设置并发度时,ConcurrentHashMap 会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)

LinkedHashMap的应用,底层,原理

LinkedHashMap双重链表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序(insert-order)或者是访问顺序,其中默认的迭代访问顺序就是插入顺序,即可以按插入的顺序遍历元素,这点和HashMap有很大的不同。

LRU算法可以用LinkedHashMap实现。

Java集合类框架的最佳实践有哪些?

1.根据应用需要正确选择要使用的集合类型对性能非常重要,比如:假如知道元素的大小是固定的,那么选用Array类型而不是ArrayList类型更为合适。
2.有些集合类型允许指定初始容量。因此,如果我们能估计出存储的元素的数目,我们可以指定初始容量来避免重新计算hash值或者扩容等。
3.为了类型安全、可读性和健壮性等原因总是要使用泛型。同时,使用泛型还可以避免运行时的ClassCastException。
4.使用JDK提供的不变类(String、Integer、Long)作为Map的键可以避免为我们自己的类实现hashCode()和equals()方法。
5.编程的时候接口优于实现
6.底层的集合实际上是空的情况下,返回为长度是0的集合或数组而不是null。

ArrayList集合加入1万条数据,应该怎么提高效率

因为ArrayList的底层是数组实现,并且数组的默认值是10,如果插入10000条要不断的扩容,耗费时间,
所以我们调用ArrayList的指定容量的构造器方法ArrayList(int size) 就可以实现不扩容,就提高了性能。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值