zoukankan      html  css  js  c++  java
  • hashmap源码

    Java里各种基础容器的实现都是链表外加引用的形式。So,hashmap也不例外。

    那小考虑下:hashmap是怎么产生的呢?

    常用的两种数据结构数组和链表各有优劣,数组寻址容易,插入和删除困难;而链表寻址困难,插入删除容易,那我们能不能综合两者的特性呢?那就是哈希表啦。hashmap是java中对于哈希表的无锁实现。

    首先,说句题外话:Hashtable是HashMap的难弟儿,hashtable是hashmap的线程安全版本,它的实现和HashMap实现基本一致,除了它不能包含null值的key和value,并且它在计算hash值和数组索引值的方式要稍微简单一些。对于线程安全的实现,Hashtable简单的将所有操作都标记成synchronized,即对当前实例的锁,这样容易引起一些性能问题,所以目前一般使用性能更好的ConcurrentHashMap。另外,对于HashMap是可以解决同步问题的,通过调用Map Collections.synchronizedMap(Map m),当然与可以自己在使用地方加锁。

    下面进入正题:

    哈希表有多种实现方法,java中实现采用的是拉链法,我们可以当成“链表的数组”,如下图所示:

    先创建一个数组,在数组中存放是是链表的头节点。Java的HashMap里面实现了一个静态内部类Entry,其重要的属性有Key Value Entry(next)。而数组中保存的就是Entry。

    我们先来看hashmap构造函数的代码。如下:

            this.loadFactor = DEFAULT_LOAD_FACTOR; //装填因子,扩容的时候使用
            threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); //默认的容量阈值
            table = new Entry[DEFAULT_INITIAL_CAPACITY];//构造一个以Entry为对象的数组
            init(); //空方法

    由以上代码可以看出,初始化hashmap的过程只是初始化了一个Entry类型数组,Entry就是刚才说的Java实现的内部类,我们来看下Entry的代码?

     static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;
            final int hash;
          .......
            public final V setValue(V newValue) {
            V oldValue = value;
                value = newValue;
                return oldValue;
            }
    
            public final boolean equals(Object o) {
                if (!(o instanceof Map.Entry))
                    return false;
                Map.Entry e = (Map.Entry)o;
                Object k1 = getKey();
                Object k2 = e.getKey();
                if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                    Object v1 = getValue();
                    Object v2 = e.getValue();
                    if (v1 == v2 || (v1 != null && v1.equals(v2)))
                        return true;
                }
                return false;
            }
    
            public final int hashCode() {
                return (key==null   ? 0 : key.hashCode()) ^
                       (value==null ? 0 : value.hashCode());
            }/**
             * This method is invoked whenever the value in an entry is
             * overwritten by an invocation of put(k,v) for a key k that's already
             * in the HashMap.
             */
            void recordAccess(HashMap<K,V> m) {
            }
            /**
             * This method is invoked whenever the entry is
             * removed from the table.
             */
            void recordRemoval(HashMap<K,V> m) {
            }
        }

    可以看出Entry是一个链表一样结构,其中有next域用于指向下一个Entry。

    既然是Entry数组,那为什么能线性插入删除呢?那么我们看一下往hashmap里put之后都发生了什么呢?来看代码。

    public V put(K key, V value) {
            if (key == null)
                return putForNullKey(value);
            int hash = hash(key); // 对key的哈希在做一个一些移位异或的操作,目的是防止一些鸡肋的key的哈希函数
            int i = indexFor(hash, table.length);//返回hash对应的数组的下标。最简单的实现肯定是用hash%length,但这里不是这样,这里的处理很巧妙,下面还会再介绍。
        //遍历链表
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
            //如果key存在的话,就替换掉
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
            modCount++; //修改次数,用于fail-fast策略
            addEntry(hash, key, value, i); //添加entry到对应的数组中,函数见下 
          void addEntry(int hash, K key, V value, int bucketIndex) {
            if ((size >= threshold) && (null != table[bucketIndex])) {
                      resize(2 * table.length);//如果大小超过阈值的话,resize阈值。resize的过程原存在的Entry会重新计算索引值,并且Entry链的顺序也会发生颠倒(如果还在同一个链中的话);而该新添加的Entry的索引值也会重新计算。,消耗还是蛮大的,所以如果知道hashmap大小的话,最好还是给个初始值
                      hash = (null != key) ? hash(key) : 0;
                      bucketIndex = indexFor(hash, table.length);
                    }
                    createEntry(hash, key, value, bucketIndex);
                      void createEntry(int hash, K key, V value, int bucketIndex) {
                        Entry<K,V> e = table[bucketIndex];
                        table[bucketIndex] = new Entry<>(hash, key, value, e);//插入到头部,e表示Entry.next
                        size++;
                      }
          }
            return null;
        }

    上文说的利用key的二次hash值求对应的下标的时候,最常规的想法就是用hash%length,但是在jdk里,没这么实现。而是

     static int indexFor(int h, int length) {
            return h & (length-1);
        }


    这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的 n 次方(可以看初始化的代码,扩容的时候也是扩容到2的那次方)。这看上去很简单,其实比较有玄机的,而当数组长度为16时,即为2的n次方时,2的n次方得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了复杂的异或和位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。这样效率提高了,而且冲突的可能性也减少了。

    知道put操作作了什么,get的话就不难了。

    就先写到这。

  • 相关阅读:
    开发一个基于 Android系统车载智能APP
    Xilium.CefGlue利用XHR实现Js调用c#方法
    WPF杂难解 奇怪的DisconnectedItem
    (转)获取安卓iOS上的微信聊天记录、通过Metasploit控制安卓
    mac 安装npm
    mac安装Homebrew
    关于面试,我也有说的
    【分享】小工具大智慧之Sql执行工具
    领域模型中分散的事务如何集中统一处理(C#解决方案)
    小程序大智慧,sqlserver 注释提取工具
  • 原文地址:https://www.cnblogs.com/babybluevino/p/3670375.html
Copyright © 2011-2022 走看看