HashMap源码学习

HashMap源码学习(jdk1.8)

HashMap的基本使用

	// 创建一个hashmap
        HashMap<Integer, String> map = new HashMap<>();
        // 往hashmap中添加五个元素
        map.put(1,"小明");
        map.put(2,"小红");
        map.put(3,"小刚");
        map.put(2,"小张");
        map.put(4,"小天");
				// 移除key为2的元素
        map.remove(2);
				// 获取key为1的元素
        System.out.println(map.get(1));
        // 输出map
        System.out.println(map.toString());
        // 输出map的长度
        System.out.println(map.size());

得到结果:

可以看到,添加的2号小红不见了,换成了小张,而且我们添加了五个元素,但是map中只存了四个元素,这是为什么呢?

源码分析:

HashMap继承自AbstractMap,而AbstractMap实现了Map接口,但是,HashMap又实现了Map接口。

据java集合框架的创始人Josh Bloch描述,这样的写法是一个失误;后来不认为这个小小的失误值得去修改,所以就这样存在下来了

public class HashMap<K,V> 
  	extends AbstractMap<K,V>
  	// Clonealbe表示可以进行克隆,Serializable表示可以进行序列化
  	implements Map<K,V>, Cloneable, Serializable {
}

重要属性:

	// 序列化版本号,有兴趣的可以去看我写的关于序列化、反序列化的博客,在这里就不过多介绍了
  	private static final long serialVersionUID = 362498820763181265L;
  	// 中文的意思是:默认初始容量;见名知意,就是hashmap的默认初始化容量
  	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	// 最大容量,1左移30位相当于乘以2的30次幂,一般不会超出这个范围
 	static final int MAXIMUM_CAPACITY = 1 << 30;
	// 默认加载因子
  	static final float DEFAULT_LOAD_FACTOR = 0.75f;
	// 桶的树化阈值,即当链表长度超过8时(同时要满足最小树形化容量阈值),会转成红黑树结构
  	static final int TREEIFY_THRESHOLD = 8;
  	// 桶的链表还原阈值,即当红黑树长度小于6时会转回链表
  	static final int UNTREEIFY_THRESHOLD = 6;
  	// 最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表,默认64,这个值不能小于4*TREEIFY_THRESHOLD
  	static final int MIN_TREEIFY_CAPACITY = 64;
  	// transient代表这个属性无法被序列化
  	// 代表hashmap中的数组,hashmap由数组+链表(红黑树)的形式组成
  	transient Node<K,V>[] table;
  	// map的每个键值对都当作一个对象存入了set集合中。那么我们就可以用set集合来迭代这个map的所有值
  	transient Set<Map.Entry<K,V>> entrySet;
	// hashmap中的元素个数 The number of key-value mappings contained in this map.
  	transient int size;
  	// 阈值
  	int threshold;
	// 加载因子
  	final float loadFactor;

重要方法

1、构造方法
// 无参构造,只传入了一个默认加载因子;一般建议通过这种方式创建map
public HashMap() {
 	this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted  所有其他字段均为默认值
}
// 传入自定义的初始化容量和默认的加载因子
public HashMap(int initialCapacity) {
  	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 传入自定义的初始化容量和加载因子进行初始化
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
      	// 传入小于0的初始容量,会抛出非法参数异常
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
      	// 当传入的初始化容量超过给的最大值时,将最大值赋值给初始容量
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
      	// 传入的负载因子不合规,isNaN的意思是 is not a number,代表传入的不是一个数字
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}
 // 传入另一个Map的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
2、put()方法
// 初始化完成后,需要往里面添加元素
// 注意:调用构造方法时,hashmap中的数组并没有被创建,(除了传入map的构造函数,不过其也是通过调用putVal()方法,putVal()中又调用了resize()方法进行初始化)
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
// hash方法
static final int hash(Object key) {
    int h;
  	/* 当key等于null时(由此可见,hashmap是允许key为null的,不会抛出异常),返回0
  	   当key不等于null时,通过Object中的hashCode()方法得到key的哈希码,然后与他本身右移16位进行异或运算
  	   a.保证高16位也参与计算,我们知道int占4字节 32位,16是中位数
           b. 因为大部分情况下,都是低16位参与运算,高16位可以减少hash冲突
	   总而言之,就是为了通过这个运算来减少hash冲突
  	*/
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
  	/* 当table为null,或者长度为0时,说明该数组还未被创建(说明hashmap是在put第一个元素时才创建数组),					 				   
           所以调用resize()方法进行初始化。
  	*/
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /* 通过 tab[i = (n - 1) & hash将hash()方法所计算出的hash值与数组长度减一进行与运算,计算出元素应该存储的位置下标,这样做的目的也是为了减少hash冲突;如果计算出的p位置没有任何元素,就通过newNode方法新建一个node元素并将hash,key,value等元素都存入其中,null位置是next属性,链表中指向的下一个元素,这里还没有产生链表所以是null
  	*/
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
      	/* 在p不为null的情况下,即p位置已经存在元素的时候,通过与新元素的hash值比较,以及比较key是否是同一个对象,如果是,则直接用新对象替代原对象,如果不是,继续通过equals方法进行比较,如果equals相等,同样会替换。
      	*/
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
      	// 在新的key值与原key值不同的情况下,判断是否是树节点,如果是,直接调用putTreeVal()方法存入
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
      	// 否则为链表节点
        else {
          	// 使用尾插法(1.8)在链表尾部插入
            for (int binCount = 0; ; ++binCount) {
              	// 如果p指向的链表下一个元素为null,即到达了链表的最后一个元素
                if ((e = p.next) == null) {
                  	// 在链表末尾插入新节点
                    p.next = newNode(hash, key, value, null);
                  	// 如果链表长度大于等于桶的树化阈值-1的时候(还需要判断hashmap数组长度是否满足树化条件),执行treeifyBin方法转成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                  	// 跳出循环
                    break;
                }
              	// 同上,在遍历的过程中将链表中的每个元素与新传入的元素进行比较
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
      	// 当e不为null。即发生了hash冲突时
        if (e != null) { // existing mapping for key
          	// 用e中的value替代原来的value,并将原来的value以返回值的形式返回
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
          	// 访问后回调
            afterNodeAccess(e);
            return oldValue;
        }
    }
  	// 修改次数加一,在进行迭代时,通过比较迭代前后的modCount值,来防止在迭代过程修改map
    ++modCount;
  	// 如果map长度达到了阈值,则需要进行扩容
    if (++size > threshold)
        resize();
  	// 插入后回调
    afterNodeInsertion(evict);
    return null;
}
3、resize()方法
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
  	// 扩容前数组长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
  	// 扩容前的阈值
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
      	// 新数组长度为旧数组长度左移一位,即乘2;如果旧数组长度大于等于默认的初始化容量,那么新的阈值也需要乘2
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
  	// 初始容量置于阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
  	// 零初始阈值表示使用默认值
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
		// 如果新阈值等于0,则将新的数组长度乘以加载因子得到新的阈值
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
  	// @SuppressWarnings注解是jse提供的注解。作用是屏蔽一些无关紧要的警告。使开发者能看到一些他们真正关心的警告。         			从而提高开发者的效率
    @SuppressWarnings({"rawtypes","unchecked"})
  			// 创建一个新数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
  	// 数组完成扩容,下面需要将原数组元素迁移到新数组中
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
          	// 将当前j位置的node赋值给e
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
              	// e.next为null说明这个数组下标只有当前这一个node,直接迁移到新数组中指定位置即可
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order 维持秩序
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                      	// 将当前node的hash值与原数组长度进行与运算,如果为0,则加到lo链表中
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                      	// 如果不为0,则将当前node加到hi链表中
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null); // 退出循环条件,即当前node的next为null,为该链表最后一个元素
                    if (loTail != null) {
                      	// 将原数组的node链表转移到新数组中,下标位置不变
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                      	// 将原数组的node链表转移到新数组中,下标位置变为原下标+原数组长度
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
4、get()方法
public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
// getNode()方法
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
          	// 桶中第一个元素hash等于传入的hash值,并且key是同一对象或者equals()方法返回true,直接返回第一个元素
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
          	// 当第一个元素不符合条件时,继续在桶中向下搜索
            if ((e = first.next) != null) {
              	// 树节点,在树中搜索
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
              	// 在剩余的链表中搜索
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
  			// 如果查询不到符合条件的值,则返回null
        return null;
    }
5、remove()方法
public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
// removeNode方法
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
  			// 当前数组不为空且长度大于0,并且通过计算得到的下标位置的桶不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
          	// 找到目标元素,将p赋值给node
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
              	// 在红黑树中查找
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                  	// 在链表中查找
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
          	// 如果node不为空,即成功找到了该节点;matchValue默认传入false,!即为true
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
              	// 在树上移除该节点
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                // 在链表上移除
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
              	// 数量减一
                --size;
              	// 移除后回调
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

hashmap()中还有一些其他的方法就不一一介绍了,有兴趣的可以自己去读一读源码,这些方法其实大同小异,能看懂一个方法后,再去看其他的方法就比较轻松了!

常见面试题:

1、为什么数组长度要求必须是2的整数倍?

The default initial capacity - MUST be a power of two.

  1. 能利用 & 操作代替 % 操作,可以提升性能

    计算桶位置时其实是通过key的hash%l数组的length来得到下标,这样做的目的是为了减少哈希冲突;然而,相比于&运算,%运算的效率会低很多,而且,当数组的长度是2的整数倍是index = (n - 1) & hash就等价于hash%n;

  2. hashmap扩容的元素迁移过程中,由于数组大小是2次幂的巧妙设定,使得只要检查 “ 特殊位 ” 就能确定该元素的最终定位。

2、为什么hashmap的默认加载因子是0.75

The load factor used when none specified in constructor.

这其实是出于时间和空间平衡的结果,是在经过无数次试验测试后得出来的

当加载因子设置比较大的时候,扩容发生的频率比较低,此时发生 Hash冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低;而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,空间利用率低。

3、JDK1.7 VS JDK1.8 比较

不同点 JDK1.7 JDK1.8
存储结构 数组 + 链表 数组 + 链表(红黑树)
hash值计算 4次位运算+5次异或运算 1次位运算+1次异或运算
数据插入方式 头插法(多线程下可能会出现死循环) 尾插法(不会出现死循环,但可能会造成数据丢失)
扩容后存储位置的计算方式 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)
存放数据的规则 无冲突时,存放数组;冲突时,存放链表 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
初始化方式 单独函数:inflateTable() 直接集成到了扩容函数resize()
posted @   KDking  阅读(30)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示