Hash表与HashMap知识总结 - Xiasm/Java-Android-Learn GitHub Wiki

回顾

上面我总结了顺序表和链表,大的方面来说,顺序表优点有查找元素很容易(寻址容易),尾插、尾删容易,缺点有头插、中间插或删除需要移动下标被影响的元素,性能消耗较大;链表的优点有插入或删除一个元素很容易,缺点是查找元素需要从头或尾遍历找到,那么有没有一种数据结构集合两者的优点,既实现查找元素容易,且插入或删除元素也容易呢?答案是肯定的,下面我们就来讲解一下这种数据结构。

Hash表介绍

哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
百度百科上有这么一句解释我觉得很好“给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。”,那么只要通过特定的算法,在表的内存区域中能从关键码快速的找到关键值,我们就可以称这种数据结构为Hash表。总而言之,hash表有两个重要的角色,一个是通过key找到value的函数,另一个就是存放数据的表。
注意,这个函数的结果是有要求的,就是结果是尽可能的散列开来的,那么如果有两个不同的关键码通过这个函数求得相同结果怎么办呢?这个也需要考虑处理方式。

HashMap原理分析

HashMap是Hash表的一种实现,下面我就通过HashMap原理的分析来一步步带大家了解Hassh表。

  • HashMap是如何存储的? 首先我们看put()方法:
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    //HashMap中的hash算法
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    //通过hash值将value放入特定的区域
    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;
    }

putVal()方法很长,我们不再详细介绍,它的目的就是将value按照hash函数的散列结果放入特定的内存中。下面我们来看关键点:putVal()方法中有一个变量table,看下它的类型:

    transient Node<K,V>[] table;

table是Node<K,V>类型的,我们接着看Node类

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

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        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类是一个链表,它有个属性next指向一个Node,并且Node里面存储着当前Value的Key和hash值,所以可以看出来,整个HashMap的存储结构就是一个顺序表和链表的组合,好多人称之为拉链。用一个图来表示:

由图我们可以看出,HashMap在处理hash值冲突的解决办法是,如果两个key计算得到的哈希值相同,那么就在数组当前索引加入一个链表,依次存储hash值相同的key-value。

HashMap的缺点

这样的数据结构有没有什么缺点呢?缺点也是有的,其一就是这种结构增大了内存开销,但这不是最明显的缺点,最明显的缺点就是数组的长度是固定的,如果我们存入的键值对超过了数组的长度怎么办呢?肯定要队数组扩容,而这个扩容的方法是最复杂的,因为一旦扩容之后就需要重新计算每个Key在新数组里的hash值,然后将键值对放入对应的位置中。
所以,我们在用HashMap时候,最好要知道我们大概有多少数据,在初始化的时候指定好HashMap的容量。

⚠️ **GitHub.com Fallback** ⚠️