Java基础之HashMap

2019-05-07  本文已影响0人  不会游泳的金鱼_

HashMap

HashMap是什么?

hashmap是一种基于Key-Value的数据结构,相信大家应该都明白其原理,就不赘述了。

JDK中的HashMap

JDK中对HashMap的描述原文如下:

Hash table based implementation of the {@code Map} interface.This implementation provides all of the optional map operations, and permits {@code null} values and the {@code null} key. (The {@code HashMap} class is roughly equivalent to {@code Hashtable}, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

关键点如下:

HashMap是非同步的,想要线程同步的HashMap可以用HashTable或ConcurrentHashMap。

实现

HashMap在底层是用数组+链表实现的。其中数组是HashMap的主体,而链表主要是为了解决哈希冲突而存在的。查找时,如果定位到的数组位置不含链表,时间复杂度为O(1);如果定位到链表,那么需要遍历链表,存在即覆盖,不存在就添加。链表出现的越少,HashMap性能越高。

重要参数:

关于扩容因子为什么要设为0.75,注释中的解释如下:

* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins.  In
* usages with well-distributed user hashCodes, tree bins are
* rarely used.  Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0:    0.60653066
* 1:    0.30326533
* 2:    0.07581633
* 3:    0.01263606
* 4:    0.00157952
* 5:    0.00015795
* 6:    0.00001316
* 7:    0.00000094
* 8:    0.00000006
* more: less than 1 in ten million

研究表明,理想情况下,使用随机哈希码,节点出现的频率在hash桶中服从泊松分布,当桶中元素达到8个时,几乎已经不可能碰撞。即0.75作为扩容因子时,链表长度几乎不可能超过8个。

为什么数组长度一定要2的n次幂呢?

这就要从源码将起了

首先是put方法:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

可以看到Key传入方法后,用hash(key)这个方法取得hash值,那么我们来看看这个方法。

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

可以看到,先取key的hashCode,然后高16位不变,低16位和高16位异或,得到的值为最终的hash值。那么我们接着来看看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;
        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);
        ......省略其他代码
}
      

可以看到,参数传入后,首先判断是否为空,之后重点来了,n = tab.length,即n为数组长度。而i就是key在数组中的位置。

i = (n - 1) & hash。这个设计就非常巧妙了,用数组长度-1再和前面算出的hash值进行相与得到位置。由于n为2的k次幂,因此n-1的二进制肯定是全1。

举个例子:

N = 2^4 = 16 转换为二进制为10000

n - 1 = 15 转换为二进制为01111

因此,无论hash是多少,(n - 1) & hash都在0000~1111之间。这就保证了数组下标不会越界。

那么如果n-1不是全1会发生什么呢?

再举个例子:

N = 10 转换为二进制为01010

n - 1 = 9 转换为二进制为01001

hash = 1001 -> (n - 1) & hash = 1001;

hash = 1101 -> (n - 1) & hash = 1001;

显然有些结果出现的概率将会变大,而有些结果则不可能再出现(例如:1110),这显然是不符合hash算法的均匀分布原则的。
因此,数组长度一定要2的n次幂。这种情况下,只要输入的hashcode是均匀的,hash算法得到的结果就是均匀的。

为什么要重写hashCode()和equals()方法?

大家都知道,hashMap中,如果key为自定义类,那么就必须要重写hashCode()和equals()方法。那么这是为什么呢?

首先为什么要重写equals()方法呢?

还得看源码:

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;
                }
            }
        ......省略其他代码
}

可以看到put方法中,判断key是否相等,用的是equals方法。其实get方法中也是一样。那么默认的equals方法就不一定能满足我们的要求了。因此要重写equals方法。

那么又为什么要重写hashCode方法呢?

这就涉及到Object.hashCode()的通用约定了:

  1. 每次应用执行期间,在对象做equals比较所用到的信息没有修改之前,hashcode方法的返回值应该是相同的。
  2. 两个对象equals方法相同,那么hashCode值必须相同;
  3. 两个对象equals方法不同,那么hashCode值不一定不同。

如果只重写equals方法,不重写hashCode方法,那么显然会违反第二条约定。

再具体到HashMap中,有如下源码:

public V get(Object key) {
        Node<K,V> e;
        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;
            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;
    }

其中if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
这句明显可以看出,get的时候会先进行hash的比较,之后才会进行equals判断。上面源码分析过,hash的计算是基于hashCode()方法的。不重写的话,一般情况下hashCode()方法的返回值不同,那么就无法get到正确的key所对应的value。

如果只重写equals,不重写hashCode,那么存入key内容相同的对象时,会存入两个key内容相同的对象,而不是覆盖掉原有的对象。
取出的时候也取不出正确的对象。

因此必须要重写hashCode()和equals()方法。


HashMap和HashTable以及ConcurrentHashMap的区别:

Hashtable

数组+链表实现,key,value都不能为null,线程安全,实现方式是修改数据时锁住整个HashTable,效率低。
初始大小11,扩容*2+1。

HashMap

数组+链表实现,key,value都可以为null,线程不安全。
初始大小16,扩容*2。

ConcurrentHashMap

分段数组+链表实现,线程安全。用分离锁实现,允许多个修改操作并发。跨段修改时按顺序锁定所有段,再按顺序释放。

上一篇下一篇

猜你喜欢

热点阅读