说说常见的集合有哪些吧?
答:Map
接口和 Collection
接口是所有集合框架的父接口:
Collection
接口的子接口包括:List
接口和Set
接口;List
接口的实现类主要有:ArrayList
、LinkedList
、Stack
以及Vector
等;Set
接口的实现类主要有:HashSet
、TreeSet
、LinkedHashSet
等;
Map
接口的实现类主要有:HashMap
、TreeMap
、Hashtable
、ConcurrentHashMap
以及Properties
等;
HashMap
与Hashtable
的区别?
-
HashMap
没有考虑同步,是线程不安全的;Hashtable
使用了synchronized
关键字,是线程安全的; -
HashMap
允许K/V
都为null
;后者K/V
都不允许为null
; -
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
)来获取数组下标的方式进行存储:
- 一来是比取余操作更加有效率;
- 二来也是因为只有当数组长度为2的幂次方时,
h&(length-1)
才等价于h%length
; - 三来解决了“哈希值与数组大小范围不匹配”的问题。
面试官:为什么数组长度要保证为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
,而 0001
,0011
,0101
,1001
,1011
,0111
,1101
这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。
面试官:那为什么是两次扰动呢?
答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&
均匀性,最终减少Hash
冲突,两次就够了,已经达到了高位低位同时参与运算的目的。
HashMap
在JDK 1.7
和JDK 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 原位置 + 旧容量) |
为什么HashMap
中String
、Integer
这样的包装类适合作为K
?
答:String
、Integer
等包装类的特性能够保证Hash
值的不可更改性和计算准确性,能够有效的减少Hash
碰撞的几率。
- 都是
final
类型,即不可变性,保证key
的不可更改性,不会存在获取hash
值不同的情况; - 内部已重写了
equals()
、hashCode()
等方法,遵守了HashMap
内部的规范(不清楚可以去上面看看putValue
的过程),不容易出现Hash
值计算错误的情况;
面试官:如果我想要让自己的Object
作为K
应该怎么办呢?
答:重写hashCode()
和equals()
方法。
-
重写
hashCode()
是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash
碰撞; -
重写
equals()
方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null
的引用值x
,x.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
值,是的话就返回遍历;否则抛出异常,终止遍历。
解决办法:
-
在遍历过程中,所有涉及到改变
modCount
值得地方全部加上synchronized
。 -
使用
CopyOnWriteArrayList
来替换ArrayList
;
ArrayList
和 Vector
的区别?
答:这两个类都实现了 List
接口(List
接口继承了 Collection
接口),他们都是有序集合,即存储在这两个集合中的元素位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引来取出某个元素,并且其中的数据是允许重复的,这是与 HashSet
之类的集合的最大不同处,HashSet
之类的集合不可以按索引号去检索其中的元素,也不允许有重复的元素。
ArrayList
与 Vector
的区别主要包括两个方面:
-
同步性:
Vector
是线程安全的,也就是说它的方法之间是线程同步(加了synchronized
关键字)的,而ArrayList
是线程不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用ArrayList
,因为它不考虑线程安全的问题,所以效率会高一些;如果有多个线程会访问到集合,那最好是使用Vector
,因为不需要我们自己再去考虑和编写线程安全的代码。 -
数据增长:
ArrayList
与Vector
都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加ArrayList
和Vector
的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要去的一定的平衡。Vector
在数据满时(加载因子1)增长为原来的两倍(扩容增量:原容量的 2 倍),而ArrayList
在数据量达到容量的一半时(加载因子 0.5)增长为原容量的 (0.5 倍 + 1) 个空间。
ArrayList
和 LinkedList
的区别?
答:LinkedList
实现了 List
和 Deque
接口,一般称为双向链表;ArrayList
实现了 List
接口,动态数组;
LinkedList
在插入和删除数据时效率更高,ArrayList
在查找某个index
的数据时效率更高;LinkedList
比ArrayList
需要更多的内存;
Array
和 ArrayList
有什么区别?什么时候该应 Array
而不是 ArrayList
呢?
答:它们的区别是:
Array
可以包含基本类型和对象类型,ArrayList
只能包含对象类型。Array
大小是固定的,ArrayList
的大小是动态变化的。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;
}
由于HashMap
的K
值本身就不允许重复,并且在HashMap
中如果K/V
相同时,会用新的V
覆盖掉旧的V
,然后返回旧的V
,那么在HashSet
中执行这一句话始终会返回一个false
,导致插入失败,这样就保证了数据的不可重复性。