知识点:Java HashMap 原理与源码分析(上)

知识点:Java HashMap 原理与源码分析(上)

知识点:Java HashMap 原理与源码分析(上)_第1张图片

从本文你可以学到什么

  1. 什么是HashMap?它有那些特点
  2. 有些那常用的方法
  3. HashMap的工作原理
  4. HashMap中的Hash是怎么实现的;为什么要这样的实现

概述

HashMap 实现了Map接口;使用键值对的数据结构组织数据。
在官方文档中这样描述HashMap:

知识点:Java HashMap 原理与源码分析(上)_第2张图片

在这段描述中说明了HashMap的特点:

  1. 基于Map接口实现;
  2. HashMap允许Null值的存在;
  3. HashMap不保存存储的顺序性,不保证顺序随着时间的推移保持恒定;

使用示例:

//创建一个Map集合
HashMap<String,String> map=new HashMap();
//向Map中插入元素
map.put("a-key","a-value");
map.put("b-key","b-value");
//获取Map中的元素
System.out.printf(map.get("a-key"));
System.out.printf(map.get("b-key"));
//遍历Map中的元素
for(Entry<String, Integer> entry : map.entrySet()) {
     
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

运行后的结果:

a-value
b-value
a-key: a-value
b-key: b-value

两个重要参数

HashMap创建实例时有一个如下构造方法

HashMap(int initialCapacity, float loadFactor)

第一个参数中指定了初始容量(initialCapacity),第二个参数指定了负载系数(loadFactor);
所以在HashMap中的两具重要参数分别外容量(Capacity)和负载系数(Load factor);
容量即指整个HashMap桶的大小;负载系数的作用用于扩容,当HashMap中的元素个数大于Capacity*loadFactor时就会触发扩容机制;默认每次扩容为当前容量大小的两倍;系统默认负载系数为0.75。

get函数解析与源码实现

方法原型:

public <V> get(Object key);

调用方法将返回HashMap集合中指定Key所映射的值,如果Key不存在于集合中将返回null。

实现思路:

  1. 对key计算hash值
  2. 使用hash值在桶中查找是否有Key
  3. Node中的第一个元素是否与Key相同(node.k==k||(key!=null&&key.equal(node.k)))
  4. 产生hash碰撞后遍历Node中的元素找出匹配的值
  5. 以上2、3、4不符合时返回null

源代码:

public V get(Object key) {
     
        Node<K,V> e;
        //计算key的hash值并调用getNode方法
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
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) {
     
            //直接命中
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
                //存在hash的Node但未直接命中Key
            if ((e = first.next) != null) {
     
            //遍历树查找key
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                    //遍历链表查找key
                do {
     
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

put函数解析与实现

方法原型:

public V put(K key, V value)

将指定值与该映射中的指定键相关联。如果该映射先前包含该键的映射,则将替换旧值。

实现思路:

  1. 计算Key的hash值
  2. 根据hash值计算出元素存放位置(Node)
  3. 是否存在碰撞,不存在直接放入Node中,Node的为链表结构
  4. 存在碰撞且当前为链表结构,在加入链表后的元素数量是否超过“TREEIFY_THRESHOLD”;超过后将链表转换成红黑树
  5. 如果Node已经存在Key;根据参数onlyIfAbsent来替换Old value;(保证key的唯一性)
  6. 如果Node满了(超过了load factor*current Capacity),进行resize操作

源代码:

public V put(K key, V value) {
     
//计算Key的hash值并调用putVal
        return putVal(hash(key), key, value, false, true);
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
     
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        //没有Node直接创建将将值给这个Node
            tab[i] = newNode(hash, key, value, null);
        else {
     
        //已经Key在槽中的了
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                //对于不同的类型进行putVal操作
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
     
                for (int binCount = 0; ; ++binCount) {
     
                    if ((e = p.next) == null) {
     
                        p.next = newNode(hash, key, value, null);
                        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;
                }
            }
            if (e != null) {
      // existing mapping for key
                V oldValue = e.value;
                //写入
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
        //resize操作
            resize();
        afterNodeInsertion(evict);
        return null;
    }

hash函数的实现与作用

在get与put操作中都会对key计算hash值,这个计算过程由hash函数实现;那这个函数的实现思路是什么样的呢?

hash函数思路:

  1. 调用key对象的hashCode函数获取对象的hash值
  2. 保持hash的高16位,将高16位与低16作异或运算并做为低16位最后得到一个32位的hash值

源代码:

static final int hash(Object key) {
     
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

官方文档中这样描述
知识点:Java HashMap 原理与源码分析(上)_第3张图片

最后在get与put函数中会使用(length-1)&hash的方式来计算key在当前集合中的索引位置。

设计者认为这方法很容易发生碰撞。为什么这么说呢?不妨思考一下,在n - 1为15(0x1111)时,其实散列真正生效的只是低4bit的有效位,当然容易碰撞了。

因此,设计者想了一个顾全大局的方法(综合考虑了速度、作用、质量),就是把高16bit和低16bit异或了一下。设计者还解释到因为现在大多数的hashCode的分布已经很不错了,就算是发生了碰撞也用O(logn)的tree去做了。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。

如果还是产生了频繁的碰撞,会发生什么问题呢?作者注释说,他们使用树来处理频繁的碰撞(we use trees to handle large sets of collisions in bins),在JEP-180中,描述了这个问题:

知识点:Java HashMap 原理与源码分析(上)_第4张图片

之前已经提过,在获取HashMap的元素时,基本分两步:

  1. 首先根据hashCode()做hash,然后确定bucket的index;
  2. 如果bucket的节点的key不是我们需要的,则通过keys.equals()在链中找。

在Java 8之前的实现中是用链表解决冲突的,在产生碰撞的情况下,进行get时,两步的时间复杂度是O(1)+O(n)。因此,当碰撞很厉害的时候n很大,O(n)的速度显然是影响速度的。

因此在Java 8中,利用红黑树替换链表,这样复杂度就变成了O(1)+O(logn)了,这样在n很大的时候,能够比较理想的解决这个问题。

扫码关注博主公众号

你可能感兴趣的:(JAVA,java)