HashMap1.7知识点

212 阅读7分钟

HashMap1.7底层原理

1.数据结构

HashMap1.7中底层数据结构采用数组+链表实现,也成为拉链法。

结构描述
数组(主)核心底层: table[ ],又称哈希数组
数组下标(indexFor):根据key值计算hash后与数组length-1进行&运算
数组元素(Entry):1个键值对=1个链表(头结点)
数组大小:capacity
链表(副)每个链表(bucket):哈希表的桶
链表的节点值:一个键值对
链表长度:桶的大小
存储对象Entry类,属性包括:键(key),值(value),下一节点(next)

2.存储流程

  1. 接收键值对
  2. 根据键值对中的key计算hash值
  3. 根据hash值计算存储存储的数组下标
  4. 判断该位置是否发生hash冲突?
  5. 若发生冲突,则将原位置的数据转移到链表中,并将键值对存储于数组中
  6. 若未发生冲突,则将键值对直接存储于该位置

3.使用api

V get(Object key); //获取指定键的值
V put(K key,V value); //添加键值对
void putAll(Map<? extends K, ? extends V> m);  //将指定Map中的键值对 复制到 此Map中
V remove(Object key);  //删除该键值对

boolean containsKey(Object key); //判断是否存在该键的键值对;是 则返回true
boolean containsValue(Object value);  //判断是否存在该值的键值对;是 则返回true

Set<K> keySet();  //单独抽取key序列,将所有key生成一个Set
Collection<V> values();  //单独value序列,将所有value生成一个Collection

void clear(); //清除哈希表中的所有键值对
int size();  //返回哈希表中所有 键值对的数量 = 数组中的键值对 + 链表中的键值对
boolean isEmpty(); //判断HashMap是否为空;size == 0时 表示为 空 

4.重要参数

capacity:HashMap中数组的容量默认为16
static final int DEFAULT_INITIAL_CAPACITY = 1<<4;
static final int MAXIMUM_CAPACITY = 1 << 30;

loadFactor:加载因子默认为0.75f
final float loadFactor;
static final float DEFAULT_LOAD_FACTOR=0.75f

threshold:扩容阈值=容量*加载因子,当哈希表的大小>=扩容阈值时,哈希表进行扩容
int threshold;

table:存放key-value键值对的数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  

size:存储键值对的数量(数组上键值对+所有链表上键值对)
transient int size;

5.源码分析

1.构造参数

HashMap中存在4个构造函数,分别为:

  • 默认构造函数(无参)
  • 指定"容量大小"的构造函数
  • 指定"容量大小"和"加载因子"的构造函数
  • 包含"子Map"构造函数
HashMap<String,Integer> map = new HashMap<String,Integer>();

     /*
     * 构造函数1:默认构造函数(无参)
     */
     public HashMap() {
        // 实际上是调用构造函数3:指定"容量大小"和"加载因子"的构造函数
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); 
     }

    /*
     * 构造函数2:指定"容量大小"的构造函数
     */
     public HashMap(int initialCapacity) {
        // 实际上调用指定"容量大小"和"加载因子"的构造函数
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /*
     * 构造函数3:指定"容量大小"和"加载因子"的构造函数
     */
     public HashMap(int initialCapacity, float loadFactor) {
        // HashMap的最大容量只能是MAXIMUM_CAPACITY
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 设置 加载因子
        this.loadFactor = loadFactor;
        // 设置 扩容阈值 = 初始容量
        // 注:此处不是真正的阈值,是为了扩展table,该阈值后面会重新计算 
        threshold = initialCapacity;   

        init(); // 一个空方法用于未来的子对象扩展
    }

    /*
     * 构造函数4:包含"子Map"的构造函数
     * 即 构造出来的HashMap包含传入Map的映射关系
     * 加载因子 & 容量 = 默认
     */
     public HashMap(Map<? extends K, ? extends V> m) {
        // 设置容量大小 & 加载因子 = 默认
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        // 该方法用于初始化 数组 & 阈值,下面会详细说明
        inflateTable(threshold);

        // 将传入的子Map中的全部元素逐个添加到HashMap中
        putAllForCreate(m);
    }

2.put()方法流程

  1. 判断table是否初始化,如未初始化则进行初始化
  2. 判断key值是否为null,若为null,则将该键-值存放到数组table中的第1个位置
  3. 若不为null,则根据key值计算出hash值
  4. 根据key的hash值与(table.length-1)进行与操作获得key对应存放的数组Table中位置
  5. 判断数组Table中位置是否存在该key,若存在,则使用新value替换旧value
  6. 若不存在,则将该key-value添加入table中
map.put("张三",1);

public V put(K key, V value)
        /* 
         * 若哈希表未初始化(即table为空)table 
         * 则使用构造函数时设置的阈值(即初始容量)初始化数组
         */ 
        if(table == EMPTY_TABLE){
            inflateTable(threshold);  //方法1
        }
        
        /* 
         * 判断key是否为空值null
         * 若key == null,则将该键-值存放到数组table中的第1个位置,即table[0]
         *(本质:key = Null时,hash值 = 0,故存放到table[0]中)
         */ 
        if (key == null)
            return putForNullKey(value);  //方法2
        
        /* 
         * 若 key ≠ null,则计算存放数组table中的位置(下标、索引)
         * 根据键值key计算hash值后根据hash值与(数组.length-1)进行
         * 与操作最终获得key对应存放的数组Table中位置
         */
        int hash = hash(key);  //方法3
        int i = indexFor(hash,table.length);  //方法4
        
        /* 
         * 判断该key对应的值是否已存在(通过遍历以该数组元素为头结点的链表逐个判断)
         */
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //若该key已存在(即key-value已存在),则用新value替换旧value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue; //返回旧的value
            }
        }
        modCount++;

        //若该key不存在,则将"key-value"加到table中
        addEntry(hash, key, value, i);  //方法5
        return null;
    }
    
 方法1inflateTable(threshold)
 private void inflateTable(int toSize){
     int capacity = roundUpToPowerOf2(toSize);  //将传入的容量转化为大于容量的最小二次幂
     threshold = (int)Math.min(capacity*loadFactor,MAXIMUM_CAPACITY + 1);  //重新计算阈值
     table = new Entry[capacity]; //用大于容量的最小二次幂作为容量初始化table  
 }
 
 private static int roundUpToPowerOf2(int number){
     //若number大于MAXIMUM_CAPACITY则返回MAXIMUM_CAPACITY否则返回传入容量大小的最小的2的次幂
     return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : 
         (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; 
}

方法2putForNullKey(value)
private V putForNullKey(V value){
     // 遍历以table[0]为首的链表,寻找是否存在key==null 对应的键值对
     // 若有:则用新value替换旧value;同时返回旧的value值
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null){
            V oldValue = e.value;  
            e.value = value;  
            e.recordAccess(this);  
            return oldValue;
        }
        
        //若没有,则调用addEntry(),将空键 & 对应的值封装到Entry中,并放到table[0]
        addEntry(0,null,value,0);
}


方法3hash(key)
JDK1.7中做了9次扰动处理 = 4次位运算 + 5次异或运算
static final int hash(int h){
    h ^= k.hashCode(); 
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
JDK 1.8 简化了扰动函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算
static final int hash(Object key){
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


方法4indexFor(hash,table.length)
static int indexFor(int h,int length){
    //将对哈希码扰动处理后的结果与运算(&)(数组长度-1)
    //得到存储在数组table的位置(即数组下标、索引)
    return h & (length-1);
}


方法5addEntry(hash, key, value, i)
void addEntry(int hash, K key, V value, int bucketIndex){
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //扩容2倍
        resize(2 * table.length);  
        //重新计算该Key对应的hash值
        hash = (null != key) ? hash(key) : 0;
        //重新计算Key对应的hash值的存储数组下标
        bucketIndex = indexFor(hash, table.length);  
      }
       //若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中
      createEntry(hash, key, value, bucketIndex); //方法6
}


方法6createEntry(hash, key, value, bucketIndex);
void createEntry(int hash, K key, V value, int bucketIndex) { 
    //把table中该位置原来的Entry保存  
    Entry<K,V> e = table[bucketIndex];

    //在table中该位置新建一个Entry:将原头结点位置(数组上)的键值对 放入到(链表)后1个节点中、将需插入的键值对 放入到头结点中(数组上)-> 从而形成链表
    //即在插入元素时,是在链表头插入的,table中的每个位置永远只保存最新插入的Entry,旧的Entry则放入到链表中(即解决Hash冲突)
    table[bucketIndex] = new Entry<>(hash, key, value, e);  

    //哈希表的键值对数量计数增加
    size++;  
}   

3.resize()方法流程

  1. 插入键值对时,发现容量不足
  2. 保存旧的数组
  3. 根据新容量(2倍)新建数组
  4. 遍历旧数组的每个数据并计算每个数据在新数组中的存储位置
  5. 将旧数组上的每个数据逐个转移到新数组中
  6. 将新数组table引用到HashMap的table属性上
  7. 重新设置扩容阈值
void resize(int capacity){
    //保存旧数组
    Entry[] oldTable = table;
    
    //保存旧容量(old capacity),即数组长度
    int oldCapacity = oldTable.length;
    
    // 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出    
    if (oldCapacity == MAXIMUM_CAPACITY) {  
        threshold = Integer.MAX_VALUE;  
        return;  
    }  
  
    //根据新容量(2倍容量)新建1个数组,即新table  
    Entry[] newTable = new Entry[newCapacity];  

    //将旧数组上的数据(键值对)转移到新table中,从而完成扩容
    transfer(newTable);  //方法1

    //将新数组table引用到HashMap的table属性上
    table = newTable;  

    //重新设置阈值  
    threshold = (int)(newCapacity * loadFactor); 
}


方法1:transfer(newTable)
void transfer(Entry[] newTable){
    //src引用旧数组
    Entry[] src = table;
    //获取新数组容量
    int newCapacity = newTable.length;
    //遍历旧数组,将旧数组上的数据(键值对)转移到新数组中
    for(int j = 0; j < src.length; j++){
        //获取旧数组上的每个元素
        Entry<K,V> e = src[j];
         if (e != null){
             //释放旧数组的对象引用
             src[j] = null;
             do{
                 // 遍历以该数组元素为首的链表
                 // 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开
                 Entry<K,V> next = e.next; 
                 //重新计算每个元素的存储位置
                 int i = indexFor(e.hash, newCapacity); 
                 //将元素放在数组上:采用单链表的头插入方式
                 e.next = newTable[i]; 
                 newTable[i] = e;  
                 //访问下一个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点
                 e = next;             
             }while(e != null)
         }           
    }
}