Java HashMap 源码浅析 一

一. HashMap概述

HashMap是Java程序员用于映射(键-值对)处理的最常用数据类型。随着JDK(Java Developmet Kit)的更新,JDK 1.8使用数组+链表+红黑树优化了HashMap底层的实现。当链表的长度超过阈值(8)时,链表将转换为红黑树,从而大大减少了搜索时间。

二. HashMap继承关系

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {}

您可以看到HashMap继承自父类AbstractMap, 实现了Map<K,V>,Cloneable, Serializable接口。Map接口定义了一组常规操作;Cloneable接口意味着可以复制它;在HashMap中,它实现了浅表副本,即对复制对象的更改将影响复制对象;Serializable接口意味着HashMap已被序列化,即HashMap对象可以在本地保存然后还原。

三. 类的属性

	默认的初始容量-必须为2的幂。初始容量为16。
	注意:HashMap可以指定初始容量,如果指定的初始容量不是2的幂,则会自动转换为2的幂。
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
   例如: 
   	new HashMap<>(20, 0.8); //实际为 new HashMap<>(32, 0.8);
	最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

	默认的负载因子为0.75
 	static final float DEFAULT_LOAD_FACTOR = 0.75f;

	当存储桶上的节点大于等于此值时,它将变成一颗红黑树
    static final int TREEIFY_THRESHOLD = 8;

	当存储桶上的节点小于等于此值时,将由树变回链表
    static final int UNTREEIFY_THRESHOLD = 6;

	从存储桶中的结构转换对应于红黑树的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;
	存储元素的数组,始终为2的幂
	transient Node<K,V>[] table;

	存储一组特定的元素,以迭代元素
    transient Set<Map.Entry<K,V>> entrySet;

	存储元素的数量,请注意,这不等于数组的长度
    transient int size;

	针对地图结构的每次扩展和更改进行计数的计数器
    transient int modCount;

	当实际容量(初始容量*负载因子)超过此阈值,容量将扩大
    int threshold;

	负载因子
    final float loadFactor;

四. 链表节点Node

	static class Node<K,V> implements Map.Entry<K,V> {
	    //存放元素key的hash值
	    final int hash;
	    //存放元素的key
	    final K key;
	    //存放元素的value
	    V value;
	    //指向链表中下一个Node
	    Node<K,V> next;
	
	    Node(int hash, K key, V value, Node<K,V> next) {
	        this.hash = hash;
	        this.key = key;
	        this.value = value;
	        this.next = next;
	    }
	
	    public final K getKey()        { return key; }
	    public final V getValue()      { return value; }
	    public final String toString() { return key + "=" + value; }
	
	    public final int hashCode() {
	        return Objects.hashCode(key) ^ Objects.hashCode(value);
	    }
	
	    //覆盖链表中元素的时候返回旧值
	    public final V setValue(V newValue) {
	        V oldValue = value;
	        value = newValue;
	        return oldValue;
	    }
	
	    //重写equals方法,只有当key和value都相等时返回true
	    public final boolean equals(Object o) {
	        if (o == this)
	            return true;
	        if (o instanceof Map.Entry) {
	            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
	            if (Objects.equals(key, e.getKey()) &&
	                Objects.equals(value, e.getValue()))
	                return true;
	        }
	        return false;
	    }
	}

其中Node节点中存放一个hash字段来记录hash值而不是每次使用的时候再计算是因为每个Node的hash值都需要经过扰动函数的扰动,并且扩容的时候需要计算每个Node的hash值,所以出于空间换时间的想法,在Node节点中加入了hash字段。

五. 构造函数

  1. public HashMap(int , float )构造函数
    public HashMap(int initialCapacity, float loadFactor) {
    	//初始容量不应小于0,否则将报错。
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //初始容量不应大于最大值,否则为最大值。                                       
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
            
        //负载因子不应小于或等于0。   
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
                                               
		//初始化负载因子
        this.loadFactor = loadFactor;
        //初始化阈值大小
        this.threshold = tableSizeFor(initialCapacity);
    }

注意:tableSizeFor(初始容量)将返回大于初始容量的最小二次幂值。

    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;
    }

这里涉及到位运算,用于计算给定大小的cap大于或等于cap的最小2的幂。乍看之下,五个连续的右移操作毫无意义,但是当您仔细考虑二进制系统为0和1时,就会出现问题。
第一个右移意味着1右边的每个位置都变为1,第二个右移意味着1的最后位置已经变成两个连续的位置。接下来的五个操作仅使int成为32位大写,是否会感到惊讶?
翻转之后,所有的数字都根据最大的1的位置变成1,然后是n+1,否则就是2的幂。这里需要注意的另一点是第一行中的cap-1,因为如果cap本身是2的幂,它将导致两倍的cap,并浪费空间。

  1. public HashMap(int )构造函数
    //构造一个空的HashMap,具有指定的初始容量和默认的加载因子(0.75)。
    public HashMap(int initialCapacity) {
    	//调用HashMap(int,float)构造函数
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
  1. public HashMap()构造函数
	//构造一个空的HashMap,具有默认的初始容量(16)和默认的负载因子(0.75)。
    public HashMap() {
     	//初始化负载因子
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  1. public HashMap(Map<? extends K, ? extends V> m)构造函数
	//构造一个新的HashMap与指定的Map相同的映射。HashMap是用默认的负载因子(0.75)创建的,
	//初始容量足以容纳指定的Map中的映射。
    public HashMap(Map<? extends K, ? extends V> m) {
    	//初始化负载因子
        this.loadFactor = DEFAULT_LOAD_FACTOR;
   		//将m中的所有元素添加到HashMap 
        putMapEntries(m, false);
    }

说明:putMapEntries(Map, m, evict) 函数将m的所有元素存储到此HashMap实例中。

	final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
	        int s = m.size();
	        if (s > 0) {
	        	 //确定表是否已初始化
	            if (table == null) { // pre-size
	            	 //未初始化,s为m的实际元素个数
	                float ft = ((float)s / loadFactor) + 1.0F;
	                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
	                         (int)ft : MAXIMUM_CAPACITY);
	                 //如果计算出的t大于阈值,则获取初始化阈值。        
	                if (t > threshold)
	                    threshold = tableSizeFor(t);
	            }
	            //初始化,并且m个元素的数量大于阈值,则执行扩容方法。
	            else if (s > threshold)
	                resize();
	            //将m中的所有元素添加到HashMap中    
	            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
	                K key = e.getKey();
	                V value = e.getValue();
	                putVal(hash(key), key, value, false, evict);
	            }
	        }
	    }

这里将不再展开,为避免篇幅过长,将hashmap的resize(),get(),put()等常用方法,写到下面这篇↓

链接: HashMap源码 二

参考链接:
链接: HashMap源码分析,基于1.8对比1.7
链接: JDK1.8 HashMap源代码分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

香辣奥利奥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值