HashMap源码分析

  • 一、哈希(hash)
  • 二、HashMap原理
  • 1、Map继承体系图
  • 2、HashMap存储结构
  • 3、Node数据结构
  • 4、PUT数据原理分析
  • 5、为什么引入红黑树(解决hash碰撞)
  • 三、HashMap源码
  • 1、核心属性
  • 2、构造方法
  • 3、PUT方法



一、哈希(hash)

1、简介
hash也称散列,哈希,基本原理就是把任意长度的输入,通过hash算法变成固定长度的输出,这个映射的规则就是对应的hash算法,而原始数据映射后的二进制串就是哈希值

2、hash的特点
1、从hash值不可以反向推导出原始的数据
2、输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值
3、hash算法的指向效率很高效,长的文本也能快速计算出hash值
4、hash算法的冲突概率要小

3、缺点
由于hash的原理是将输入空间的值映射成hash空间内,而hash空间远小于输入的空间
根据抽屉原理,一定会存在不同的输入被映射成相同的输出的情况

抽屉原理:把多于n个的物体放到n个抽屉里,则至少有一个抽屉里的东西不少于两件



二、HashMap原理

1、Map继承体系图

哈希级联算法 opencv 哈希算法源码_ci

2、HashMap存储结构

从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示:

哈希级联算法 opencv 哈希算法源码_哈希算法_02

3、Node数据结构

Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个黑色圆点就是一个Node对象。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;    //用来定位数组索引位置
        final K key;
        V value;
        Node<K,V> next;   //链表的下一个node

        Node(int hash, K key, V value, Node<K,V> next) { ... }
        public final K getKey(){ ... }
        public final V getValue() { ... }
        public final String toString() { ... }
        public final int hashCode() { ... }
        public final V setValue(V newValue) { ... }
        public final boolean equals(Object o) { ... }
}

4、PUT数据原理分析

1、map.put("鸡汤","来咯"); 2、获取"鸡汤"字符串的hash值
3、经过hash扰动函数,使此hash值更散列
4、构造出node对象

hash->1122
key->"鸡汤"
value->"来咯"
Next->null

5、路由算法找出node应当存放在数组的位置

(table.length-1) & node.hash

(16-1) & 1122
B0000 0000 1111 & B0100 0110 0010 ->B0010 = 2

5、为什么引入红黑树(解决hash碰撞)

字符串经hash后得出的二进制后四位相同,经过路由算法会算出同样的值,会导致链表无限长导致效率降低(JDK1.8 红黑树解决)



三、HashMap源码

1、核心属性

//缺省table大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//table最大长度
static final int MAXIMUM_CAPACITY = 1 << 30;
//缺省负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/* 
 * 链表树化需要满足
 * 1、哈希表中的所有元素超过64
 * 2、当前链表长度大于树化阈值
 */
//树化阈值(大于该值时允许链表转红黑树)
static final int TREEIFY_THRESHOLD = 8;
//树降级阈值(小于该值时允许红黑树转链表)
static final int UNTREEIFY_THRESHOLD = 6;
//最小树容量(哈希表中的所有元素大于该值是允许树化)
static final int MIN_TREEIFY_CAPACITY = 64;

//哈希表什么时候初始化?
transient Node<K,V>[] table;
//当前哈希表元素个数
transient int size;
//当前哈希表结构修改次数
transient int modCount;

//扩容阈值,当哈希表元素超过该阈值触发扩容
int threshold; //threshold = capacity * loadFactor
//负载因子
final float loadFactor;

2、构造方法

public HashMap(int initialCapacity, float loadFactor) {
	//capacity必须大于0且小于MAX_CAP
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
       
    //loadFactor必须大于0
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    //接下部分代码块
    this.threshold = tableSizeFor(initialCapacity);
}
/**
 * 返回一个大于等于当前cap的数字,且这个数字一定是2的次方数
 * 
 * cap = 10
 * n = 10 - 1 = 9
 * 右移:二进制右移几位就是左边加几个0,右边边去掉几位
 * 0b1001 | 0b0100 => 0b1101
 * 0b1101 | 0b0011 => 0b1111
 * 0b1111 | 0b0000 => 0b1111
 * 。。。
 * 0b1111 = 15
 * return 16
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

3、PUT方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}