Java HashMap基础解读

2 篇文章 0 订阅
2 篇文章 0 订阅

Hash表科普

Hash表是存储数据的一种方式。该方式需要申请大量的存储空间(相对存储的数据量而言),并且需要构建一个对应的Hash函数用于计算数据在Hash表中的存储位置。当需要存储数据时,先使用Hash函数对该数据进行处理,得到一个对应于Hash表中的存储位置,判断该位置上是否有值:

  1. 如果无值,则将该数据存储到该位置中;
  2. 如果有值,则说明出现了碰撞(一个好的Hash函数可以有效减少碰撞),碰撞出现后有两种解决方案:
    ①,在该空间中建立一个指向下一位置的引用,将数据存入该引用所指向的位置(本次介绍的HashMap便是采用这种方式解决碰撞);
    ②,从该位置往后遍历Hash表,直到搜索到一个空位置,将该数据存储在这个空位置上。此方案缺点明显,将数据存储在Hash值不一致的空间中,会极大地增加Hash碰撞的可能性,最终导致Hash表失去了其存在的价值。

由于Hash函数的存在,可以保证用户在时间复杂度为O(1)的情况下获得到指定的数据。Hash表算是一种空间换时间的典型算法。

HashMap中的Hash函数

注:以下针对HashMap的分析建立在JDK1.8的基础上。
要分析一个Hash表,最最最重要的便是确定其使用的Hash函数。HashMap的Hash函数如下:

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

通过代码可以看出,HashMap获取Key的Hash值主要是对Key的hashCode进行一定的计算得到的,如果有两个不同的Key,但是对于的hashCode一致的话,则计算出来的Hash值也会一致,导致初步的碰撞(此处保留“初步”两字,是因为HashMap存在其他的方式判断,不单纯只是比较Hash值,这一点会在后面的存储方式中进行说明)。在实际的数据存储中,会对该Hash值进一步计算,得到该数据实际存储的位置。

HashMap实现方式##

HashMap存储数据时,是将数据存储在一个Node类型的数组中,即其本质是采用数组实现的:

 transient Node<K,V>[] table;

Node属于HashMap的一个内部类,该内部类的结构如下所示:

/**
 * Basic hash bin node, used for most entries.  (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
 static class Node<K,V> implements Map.Entry<K,V> {
     final int hash;
     final K key;
     V value;
     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);
     }
     ......
}

在Node的类结构中,存在一个Node类型的引用指向下一个Node节点,此处便可看出HashMap在解决碰撞的方案中采用了开头所说中的第一种方案。

HashMap存储方式

在平时使用HashMap时,我主要是使用put方式进行数据存储,此处主要分析一下put存储的具体实现:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
/**
 1. Implements Map.put and related methods
 2.  * @param hash hash for key
 3. @param key the key
 4. @param value the value to put
 5. @param onlyIfAbsent if true, don't change existing value
 6. @param evict if false, the table is in creation mode.
 7. @return previous value, or null if none
 */
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)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
        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();
    afterNodeInsertion(evict);
    return null;
}

源码中可以看出put()是由putVal()方法实现的,putVal()方法的参数在注释中已作说明,此处不再说明。现对putVal()方法进行详细说明:

  1. 存储数据前,先判断该hashMap是否为空,此处的resize()方法为更新HashMap的存储空间,会在以后的博客中进行详细说明;
  2. 局部变量n标识当前hashMap中的Node数组的长度,计算数据存储位置的表达式为:(n-1)& hash ;“&”符号表示“按位与”操作,旨在将key的hash值将低到Node数组的下标范围内,此操作得到的结果不会超过(n-1);
  3. 如果该位置没有存储数据,则直接将数据存储到该位置中;
  4. 如果该位置存有数据,先对比该数据的hash值是否与所需存储数据的hash值一致p.hash == hash,如果一致,接着对比两数值的key是否一致(k = p.key) == key || (key != null && key.equals(k)),此处对应前文中对“初步的碰撞”中的说明,如果key任然一致,此处则需要将原数据更新为新数据,更新操作在后面的代码中操作,此处将原数据赋值给局部变量e。
  5. 此处暂不讨论TreeNode情况
  6. 如果hash一致,但Key不一致时。则将该数据保存入该节点的next引用所指向的节点,如果该引用不为空,则继续深入。
  7. 当e不为空时,将e的值设置为新的值。此时当且仅当hash一致key一致时,e不为空。

HashMap取值方式

使用HashMap时,我都是使用get(key)的方式取值,此处重点介绍get(key)该方法的具体实现:

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
 * Implements Map.get and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @return the node, or null if none
 */
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;
        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);
        }
    }
    return null;
}

通过代码可以发现get(key)方法主要通过getNode()方法实现,具体的参数已在注释中进行了说明。getNode()方法相对简单,此处便只说明一下思路,不再逐句分析:根据所给key的hash值,获取该hash值所在位置的第一个节点first,如果first节点的key与对应的key一致,则直接返回该节点,不一致,则进行一遍do...while循环,遍历first节点下一个节点,重复比较key是否一致,知道查询到对应的结果。

阅读体会

HashMap本质是采用的Hash表的设计思路实现的,Hash表能在查询时提现他的价值。从源码中可以看到,如果要查询碰撞数据的话,此时查询的时间复杂度不再是O(1),而是O(n)(此处n表示碰撞数据的链式结构的长度),所以如果一个Hash表中有大量的碰撞数据,导致其中的链式存储结果足够长,则此时需要修改Hash函数,以尽可能得减少链式存储,保证查询的效率。在HashMap中,修改Hash函数可以通过重写keyhashCode()方法和equals()方法实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值