集合高级面试题

1. ArrayList 和 Vector 的区别?

参考答案

  这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合,即存储在这两个集合中的元素的位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引号取出某个元素,并且其中的数据是允许重复的,这是同 HashSet 之类的集合的最大不同处,HashSet 之类的集合不可以按索引号去检索其中的元素,也不允许有重复的元素(本来题目问的与 hashset 没有任何关系,但为了说清楚 ArrayList 与 Vector 的功能,我们使用对比方式,更有利于说明问题接着才说 ArrayList 与 Vector 的区别)。
  两者的区别主要包括两个方面:
  1. 同步性:
  Vector 是线程安全的,也就是说是它的方法之间是线程同步的,而 ArrayList 是线程序不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用 ArrayList,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。
  2. 数据增长:
  ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加 ArrayList 与 Vector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。Vector 默认增长为原来两倍,而 ArrayList 的增长策略在文档中没有明确规定(从源代码看到的是增长为原来的 1.5 倍)。ArrayList 与 Vector 都可以设置初始的空间大小,Vector 还可以设置增长的空间大小,而 ArrayList 没有提供设置增长空间的方法。
  总结:即 Vector 增长原来的一倍,ArrayList 增加原来的 1.5 倍。
2. HashMap 的负载因子是什么意思,数值是多少?

参考答案

  负载因子表示一个哈希表的空间的使用程度,有这样一个公式:
  HashMap 的容量 = 初识化容量(initailCapacity)* 负载因子(loadFactor)
  所以负载因子越大则哈希表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,所以此时索引效率就会降低。反之,负载因子越小则链表中的数据量就越稀疏,此时会对空间造成浪费,但是此时索引效率会高。
HashMap 有三个构造函数,可以选用有参构造函数设置初始化容量和负载因子,也可以选用无参构造函数,不进行设置,此时会使用默认值,分别是 16 和 0.75。
3. HashMap 的负载因子设置过大或过小会有什么问题?

参考答案

  首先负载因子的大小决定了 HashMap 的数据密度的。
  负载因子越大数据密度越大,可能发生碰撞的几率越高,数组中的链表也会越容易长,这样查询和插入时的比较次数增多,性能会下降。(ps:此处可能又引出一个面试题:如何解决 Hash 碰撞问题?答:利用拉链法)
  如果负载因子越小,就会越容易触发扩容,虽然数据密度也越小,发生碰撞几率小,数组中链表越短对于查询和插入时比较次数也会少一些,性能也会提高。但是扩容也会影响性能,所以建议初始化预设大一点空间。
    
4. 你觉得 HashMap 的负载因子设置为多少较为合理?

参考答案

  HashMap 初始容量大小默认是 16,这是为了减少冲突发生的概率,当 HashMap 的数组长度到达一个临界值的时候,就会触发扩容:把所有元素 rehash 之后再放在扩容后的容器中,这是一个相当耗时且浪费性能的操作,所以扩容是件影响性能的事。而这个临界值就是由负载因子和当前容器的容量大小来确定的。
  临界值:默认容量大小 = 16 * 负载因子,即:默认 16 乘 0.75=12 此时就会发生扩容操作。考虑将负载因子设置为 0.7~0.75,因为此时平均检索长度接近于常数,这样会更好一些。
    
5. 请描述一下 HashMap 的扩容机制?

参考答案

  HashMap 的扩容会有以下接个步骤:
  1、HashMap 在 Java7 时是基于数组 + 链表存储的。默认长度是 0,第一次添加数据时数组扩容到 16,临界值(threshold)是 16 * 负载因子(loadFactor 是 0.75)= 12。
  2、如果数组的使用到了临界值 12,就会扩容到 16 * 2 = 32,新的临界值就是 32 * 0.75 = 24,依次类推。
  3、Java8 以后,HashMap 引入了红黑树机制,如果一条链表中的元素个数到达 TREEIFY_THRESHOLD(默认是 8),并且 table(哈希表)的大小 >= MIN_TREEIFY_CAPACITY(默认 64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
    
6. HashMap 的树化链表默认值为什么是 8?

参考答案

  对于 HashMap 来说,完整的表述应该是:**当链表长度为 8 时会链表转树,长度为 6 时会树转链表。** 这里可以看到中间存在一个差值,这么设计是为了防止链表和树之间频繁的转换。假如只把转化的长度限制为 8 的话,那么一个 HashMap 如果不停的插入或删除数据,并且链表长度恰巧在 8 左右徘徊,这时就会不停的树转链表,链表转树,效率很低。
  为什么链表长度为 8 时会实现链表转树呢?因为红黑树的平均查找长度是 log(n),长度为 8 的时候,平均查找长度为 3,而链表的平均查找长度为 n/2,当长度为 8 时,平均查找长度为 8/2=4。所以,当长度大于等于 8 的时候,树的查找效率更高。
  另外在 HashMap 源码中可以看出,8的长度符合数学中的泊松分布概念。一个链表中出现 8 个节点的概率不到千万分之一,概率已经非常低了,此时树化性价比会很高。既不会因为链表太长(8)导致复杂度加大,也不会因为概率太高导致太多节点树化。
    
7. HashMap 有反向树化的操作吗?

参考答案

  HashMap 解决 hash 冲突是使用了 **拉链法**。jdk1.8 中,当一个桶链表节点超过 TREEIFY_THRESHOLD=8 后,链表会转换为红黑树,当桶中节点移除或重新哈希少于 UNTREEIFY_THRESHOLD=6 时,红黑树会转变为普通的链表。
    
8. 快速失败 (fail-fast) 和安全失败 (fail-safe) 的区别是什么?(扩展知识点)

参考答案

1、快速失败(fail-fast)
  在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行修改(增加、删除、修改),则会抛出 Concurrent Modification Exception。
  原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext() / next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。
  注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改 modCount 值刚好又设置为了 expectedmodCount 值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug。
  场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
2、安全失败(fail-safe)
  采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
  原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。
  缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的
  场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

你可能感兴趣的:(java,数据结构,开发语言)