集合常见面试题

说说常见的集合有哪些吧?

答:Map 接口和 Collection 接口是所有集合框架的父接口:

  • Collection 接口的子接口包括:List 接口和 Set 接口;
    • List 接口的实现类主要有:ArrayListLinkedListStack以及Vector等;
    • Set 接口的实现类主要有:HashSetTreeSetLinkedHashSet等;
  • Map接口的实现类主要有:HashMapTreeMapHashtableConcurrentHashMap以及Properties等;

HashMapHashtable的区别?

  1. HashMap 没有考虑同步,是线程不安全的;Hashtable使用了synchronized关键字,是线程安全的;

  2. HashMap 允许K/V 都为null;后者K/V都不允许为null

  3. HashMap继承自AbstractMap类;而Hashtable继承自Dictionary类;

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

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

面试官:那怎么解决呢?

答:HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均。

在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储:

  1. 一来是比取余操作更加有效率;
  2. 二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length
  3. 三来解决了“哈希值与数组大小范围不匹配”的问题。

面试官:为什么数组长度要保证为2的幂次方呢?

答:只有当数组长度为2的幂次方时,h&(length - 1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率。

如果 length 为 2 的次幂 则 length - 1 转化为二进制必定是 11111…… 的形式,在于 h 的二进制与操作效率会非常的快,而且空间不浪费;如果 length 不是 2 的次幂,比如 length 为 15,则 length - 1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0 ,而 0001001101011001101101111101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。

面试官:那为什么是两次扰动呢?

答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的。

HashMapJDK 1.7JDK 1.8中有哪些不同?

不同 JDK 1.7 JDK 1.8
存储结构 数组 + 链表 数组 + 链表 + 红黑树
初始化方式 单独函数:inflateTable() 直接集成到了扩容函数resize()
hash值计算方式 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则 无冲突时,存放数组;冲突时,存放链表 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
插入数据方式 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) 尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1) 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

为什么HashMapStringInteger这样的包装类适合作为K

答:StringInteger等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率。

  • 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况;
  • 内部已重写了equals()hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

面试官:如果我想要让自己的Object作为K应该怎么办呢?

答:重写hashCode()equals()方法。

  • 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;

  • 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值xx.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;

Java集合的快速失败机制 “fail-fast”?

答:是Java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。

例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生 fail-fast 机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。

解决办法:

  1. 在遍历过程中,所有涉及到改变 modCount 值得地方全部加上synchronized

  2. 使用CopyOnWriteArrayList来替换ArrayList

ArrayListVector 的区别?

答:这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合,即存储在这两个集合中的元素位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引来取出某个元素,并且其中的数据是允许重复的,这是与 HashSet 之类的集合的最大不同处,HashSet 之类的集合不可以按索引号去检索其中的元素,也不允许有重复的元素。

ArrayListVector 的区别主要包括两个方面:

  1. 同步性Vector 是线程安全的,也就是说它的方法之间是线程同步(加了 synchronized 关键字)的,而 ArrayList 是线程不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用 ArrayList,因为它不考虑线程安全的问题,所以效率会高一些;如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。

  2. 数据增长:ArrayListVector 都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加 ArrayListVector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要去的一定的平衡。Vector 在数据满时(加载因子1)增长为原来的两倍(扩容增量:原容量的 2 倍),而 ArrayList 在数据量达到容量的一半时(加载因子 0.5)增长为原容量的 (0.5 倍 + 1) 个空间。

ArrayListLinkedList 的区别?

答:LinkedList 实现了 ListDeque 接口,一般称为双向链表;ArrayList 实现了 List 接口,动态数组;

  1. LinkedList 在插入和删除数据时效率更高,ArrayList 在查找某个 index 的数据时效率更高;
  2. LinkedListArrayList 需要更多的内存;

ArrayArrayList 有什么区别?什么时候该应 Array 而不是 ArrayList 呢?

答:它们的区别是:

  1. Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。
  2. Array 大小是固定的,ArrayList 的大小是动态变化的。
  3. ArrayList 提供了更多的方法和特性,比如:addAll()removeAll()iterator() 等等。

对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

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

答:HashSet的底层其实就是HashMap只不过我们HashSet是实现了Set接口并且把数据作为K值,而V值一直使用一个相同的虚值来保存,我们可以看到源码:

public boolean add(E e) {
	// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
    return map.put(e, PRESENT)==null;
}

由于HashMapK值本身就不允许重复,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V,那么在HashSet中执行这一句话始终会返回一个false,导致插入失败,这样就保证了数据的不可重复性。

发布了94 篇原创文章 · 获赞 0 · 访问量 722

猜你喜欢

转载自blog.csdn.net/qq_46578181/article/details/105411856