HashSet
HashSet实现原理
由于set是基于map实现的 这里主要介绍map
- 是基于
HashMap
实现的,默认构造函数是构建一个初始容量为16
,负载因子为0.75
的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个PRESENT
,它是一个静态的 Object 对象。
HashSet 中数据是如何保证不重复的
- HashSet 源码分析
通过上面的 HashSet 源码我们可以看到,在 HashSet 的构造函数中,它创建了 HashMap
在 HashSet 的 add() 方法中是调用了 HashMap 的 put() 方法的,要 put 的 key 是要添加到 HashSet 中的元素,而value 则是一个静态常量值 PRESENT - 当向 HashSet 中添加元素时,如果此时添加一个 HashSet 已存在的元素时
此时肯定会发生 Hash 冲突的
那么接下来,会进行该元素的 equals() 方法或 == 的比较,如果它们的 equals() 方法或 == 比较的结果为 true 即相等,则 HashMap 会使用新 value 值覆盖旧 value 值
而到此时,HashMap 的 put() 方法的返回值为一个 oldValue,故而 HashSet 的 add 方法的返回值为 false,即添加一个已存在的元素时会失败
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
HashMap
- 先看成员变量
//HashMap初始容量大小(16) 并且是2次幂 在我们第一次put值得时候才会给到初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子 当容量被占满0.75时就需要reSize扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表长度到8,就转为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 树大小为6,就转回链表
static final int UNTREEIFY_THRESHOLD = 6;
//链表转变成树之前,还会有一次判断,只有数组长度大于 64 才会发生转换
static final int MIN_TREEIFY_CAPACITY = 64;
- 为什么是2次幂?
(n-1)&hash等效于hash % n
,在计算元素该存放的位置的时候,用到的算法是将元素的hash与当前map长度-1进行与运算,n-1是为了让低位都等于1,低位都为1与哈希值相与, 高位都为0与哈希值高位相与自然也等于0, 所以与出来的范围在 0-(n-1)
如果map的长度不是2的幂次,比如为15,那长度-1就是14,二进制为1110,无论与谁相与最后一位一定是0,0001,0011,0101,1001,1011,0111,1101这几个位置就永远都不能存放元素了,空间浪费相当大。也增加了添加元素是发生碰撞的机会。减慢了查询效率。所以Hashmap的大小建议是2的幂次。
- 什么是加载因子?为什么是0.75
加载因子也叫扩容因子或负载因子,用来判断什么时候进行扩容的,假如加载因子是 0.5,HashMap 的初始化容量是 16,那么当 HashMap 中有 16*0.5=8 个元素时,HashMap 就会进行扩容。
那加载因子为什么是 0.75 而不是 0.5 或者 1.0 呢?
这其实是出于容量和性能之间平衡的结果:
当加载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生 Hash 冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低;
而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,此时元素的存储就比较稀疏,发生哈希冲突的可能性就比较小,因此操作性能会比较高。
所以综合了以上情况就取了一个 0.5 到 1.0 的平均数 0.75 作为加载因子。
put方法
- put方法是map中核心方法之一,所以一步一步分析
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//......
static final int hash(Object key) {
//根据参数,产生一个哈希值
int h;
//这里为什么不直接返回key.hashCode() 还要与 h>>>16来异或呢?
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//......
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; //临时变量,存储"哈希表"——由此可见,哈希表是一个Node[]数组
Node<K,V> p;//临时变量,用于存储从"哈希表"中获取的Node
int n, i;//n存储哈希表长度;i存储哈希表索引
if ((tab = table) == null || (n = tab.length) == 0)//判断当前是否还没有生成哈希表
//这里就是默认初始化容量为16
n