HashMap原理及源码分析

注:本文根据网络和部分书籍整理基于JDK1.7书写与1.8版本对比介绍HashMap基本原理,文中源码为JDK 1.7
       本文内容如有雷同敬请谅解,欢迎指正文中的错误之处。

哈希表

       哈希就是把任意长度的输入,通过散列算法,变成固定长度的输出,该输出就是散列值。哈希法又称散列法、杂凑法以及关键字地址计算法等,相应的表成为哈希表。哈希表是一种根据关键码去寻找值的数据映射结构。该结构通过关键码映射的位置去寻找存放值,提供快速的插入操作和查找操作。哈希表基于数组的,创建后难于扩展,某些哈希表被基本填满时,性能下降严重而且没有一种简便的方法可以以任何一种顺序(例如从小到大)遍历表中数据项。

      哈希表的特点:关键字在表中位置和它之间存在一种确定的关系。

      Hash主要有两种应用:加密和压缩。
      加密方面:Hash哈希是把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做HASH值,最广泛应用的Hash算法有MD4、MD5、SHA-1 和 SHA-2。
      压缩方面:Hash哈希是指把一个大范围映射到一个小范围,往往是为了节省空间,使得数据容易保存。

哈希函数

      哈希函数:在元素的关键字K和元素的位置P之间建立一个对应关系f,使得P=f(K),其中称这个函数f(key)为哈希函数。

      哈希函数的目的是得到关键字值的范围,用一种方式转化为数组的下标值,尽可能地保证计算简单和散列地址分布均匀。哈希函数的构造原则是:函数本身便于计算、计算出来的地址分布均匀。主要优点是:速度,对抗碰撞不太看中,只要保证hash均匀分布就可以(比如hashmap,hash值(key)存在的目的是加速键值对的查,key的作用是为了将元素适当放在各个桶里,对于抗碰撞的要求没有那么高。hash出来的key,只要保证value均匀的放在不同的桶里就可以了。整个算法的set性能,直接与hash值产生的速度有关

      Hash函数逼近单向函数,所以可以用来对数据进行加密。(单项函数:如果某个函数在给定输入的时候,很容易计算出其结果来;而当给定结果的时候,很难计算出输入来)。不同的应用对Hash函数有着不同的要求:用于加密的Hash函数主要考虑它和单项函数的差距,而用于查找的Hash函数主要考虑它映射到小范围的冲突率

      Hash的产生方式大体可以分为三种基本方法:加法、乘法和移位。哈希函数中有许多乘法和除法是不可取的,求模算法作为一种不可逆的计算方法,已经成为了整个现代密码学的根基

      Java中几个常用的哈希码(hashCode)的算法:

      Object类的hashCode. 返回对象的经过处理后的内存地址,由于每个对象的内存地址都不一样,所以哈希码也不一样。这个是native方法,取决于JVM的内部设计,一般是某种C地址的偏移。

      String类的hashCode. 根据String类包含的字符串的内容,根据一种特殊算法返回哈希码,只要字符串的内容相同,返回的哈希码也相同。

      Integer等包装类,返回的哈希码就是Integer对象里所包含的那个整数的数值,例如Integer i1=new Integer(100), i1.hashCode的值就是100 。由此可见,2个一样大小的Integer对象,返回的哈希码也一样。

      int,char这样的基础类,它们不需要hashCode,如果需要存储时,将进行自动装箱操作,计算方法同上。

哈希冲突

      当关键字集合很大时,关键字值不同的元素可能会映像到哈希表的同一地址上,即K1!=K2,但f(K1)=f(K2),这种现象称为哈希冲突或哈希碰撞,实际中冲突是不可避免的,只能通过改进哈希函数的性能来减少冲突。

      处理冲突的方法:
      1、开放地址法(发生冲突,继续寻找下一块未被占用的存储地址)三种开放地址法:线性探测、二次探测、在哈希法
      2、链地址法,而HashMap即是采用了链地址法

HashMap原理

      HashMap 是一个散列表,它存储的内容是键值对(key-value)映射,采用链地址法,也就是数组+链表的方式解决哈希冲突的

      HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。基于哈希表的 Map 接口的实现由数组+链表组成的,HashMap 的主体是一个Entry数组以 Key-Value 的形式存在,存储的对象是 Entry (包含key,value,hash 和 next 四个属性) ,Entry 来代表每个 HashMap 中的数据节点,链表则是主要为了解决哈希冲突而存在的。在HashMap中,根据hash算法来计算key-value的存储位置并进行快速存取。

      HashMap不是线程安全的它的key、value都可以为null,其映射不是有序的。

      Entry对象唯一表示一个键值对,有四个属性:

            int hash;  键对象的hash值

            final K key;  键对象

            V value; 值对象

            Entry<K,V> next; 指向链表中下一个Entry对象,可为null,表示当前Entry对象在链表尾部

      注:1、Java 容器实际上包含的是引用变量,而这些引用变量指向了我们要实际保存的 Java 对象。

      2、Java8 中由 数组+链表+红黑树 组成。使用 Node,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,当链表中的元素超过了 当链表长度超过TREEIFY_THRESHOLD 8 个以后 会将链表转换为红黑树使用 TreeNode。

重要属性

      table : 一个Entry[] 数组类型,Entry实际上就是一个单向链表。哈希表的key-value键值对都是存储在Entry数组中的。 
      size  :HashMap的大小,HashMap保存的键值对的数量。
      loadFactor :加载因子默认0.75
      capacity : 当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍
      threshold :阈值,用于判断是否需要调整HashMap的容量。threshold=容量*加载因子,当HashMap中存储数据的数量达到threshold时,就需要rehash扩容重构

      注:初始容量 和 负载因子,这两个参数是影响HashMap性能的重要参数初始容量是哈希表在创建时桶的数量,HashMap的底层数组长度是2的次幂(默认16);加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度(默认 0.75)

put操作

     1、在第一个元素插入 HashMap 的时候做一次数组的初始化,就是先确定初始的数组大小,并计算数组扩容的阈值。
     2、当key为null时,都放到table[0]

      注:HashMap最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null

     3、根据 key的 hashCode 值计算出一个位置,该位置就是此对象准备往数组中存放的位置。
      如果该位置没有对象存在,就将此对象直接放进数组当中;如果该位置已经有对象存在了,则顺着此存在的对象的链开始寻找(为了判断是否是否值相同,map不允许<key,value>键值对重复), 如果此链上有对象的话,再去使用 equals方法进行比较,如果对此链上的每个对象的 equals 方法比较都为 false,则将该对象放到数组当中。

      注:1、Java7 是插入到链表的最前面,Java8 插入到链表的最后面。
      2、Java8中对链表长度增加了一个阈值TREEIFY_THRESHOLD,超过阈值8 个以后 链表将转化为红黑树使用 TreeNode,查询时间复杂度降为O(logn),提高了链表过长时的性能。

     4、当 size>=threshold(加载因子*当前容量时,会发生扩容resize操作一般情况下,容量将扩大至原来的两倍。

     注:只有当 size>=threshold并且 table中的那个槽中已经有Entry时才会发生resize。 Java7 是先扩容后插入新值的,Java8 先插值再扩容

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

get操作

     1、根据key计hash值找到相应的数组下标:hash & (length – 1)
         
 key(hashcode)-->hash-->indexFor-->最终索引位置,找到对应位置table[i]

     2、遍历该数组位置处的链表,直到找到相等(==或equals)的 key。
           e.hash == hash很有必要,如果传入的key对象重写了equals方法却没有重写hashCode

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

    /**
     * Offloaded version of get() to look up null keys.  Null keys map
     * to index 0.  This null case is split out into separate methods
     * for the sake of performance in the two most commonly used
     * operations (get and put), but incorporated with conditionals in
     * others.
     */
    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    
    /**
     * Returns the entry associated with the specified key in the
     * HashMap.  Returns null if the HashMap contains no mapping
     * for the key.
     */
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

扩展

     LinkedHashMap HashMap 的直接子类继承了HashMap的所用特性(LinkedHashMap = HashMap + 双向链表)。主要不同之处是:LinkedHashMap维护一个双向链表保持了有序性重新定义了Entry,在Entry增加了两个指针 before 和 after,用于维护Entry插入的先后顺序

     HashSet 实现的是Set接口,所以不允许有重复的值。基于HashMap实现,底层使用HashMap来保存所有元素。

     HashTable 产生于JDK 1.1,与HashMap一样都是基于哈希表来实现键值映射的工具类,实现了Map、Cloneable、Serializable三个接口,但是HashTable继承自抽象类Dictionary。 HashMap和HashTable都使用哈希表来存储键值对。在数据结构上是基本相同的。

     HashTable和HashMap在计算hash时都用到了一个叫hashSeed的变量。这是因为映射到同一个hash桶内的Entry对象,是以链表的形式存在的,而链表的查询效率比较低,所以HashMap/HashTable的效率对哈希冲突非常敏感,所以可以额外开启一个可选hash(hashSeed),从而减少哈希冲突。这个优化在JDK 1.8中已经去掉了,因为JDK 1.8中,映射到同一个哈希桶(数组位置)的Entry对象,使用了红黑树来存储,从而大大加速了其查找效率。

     HashTable在遇到null时,会抛出NullPointerException异常;

     HashTable默认的初始大小为11,之后每次扩充为原来的2n+1,而HashMap默认的初始化大小为16,之后每次扩充为原来的2倍;

     HashTable是同步的容器,使用synchronized来保证线程安全;

    注:如果你不需要线程安全,那么使用HashMap,如果需要线程安全,那么使用ConcurrentHashMap。HashTable已经被淘汰了,不要在新的代码中再使用它。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章