风也温柔

计算机科学知识库

java hash算法原理-java集合的底层原理(Map的底层原理 一)

  此文承接java集合的底层原理(List的底层原理),具体可以此文的开头讲述,此处简要概述的map的结构如下
  Map 接口 键值对的集合 (双列集合)
  ├——— 接口实现类, 同步, 线程安全
  ├——— 接口实现类 ,没有同步, 线程不安全-
  │—————–├ 双向链表哈希表实现
  │—————–└
  ├ ——– 红黑树对所有的key进行排序
  └———
  一、 1.1 概述
  基于Map接口实现,元素以键值对的方式存储,并且允许使用null 建和null值,因为key不允许重复,因此只能有一个键为null,另外不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。是线程不安全的
  在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,也不例外。实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
  
  从上图中可以看出java hash算法原理,底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个的时候,就会初始化一个数组。
  java源码如下:
   /**

     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry[] table;
    static class Entry implements Map.Entry {
        final K key;
        V value;
        Entry next;
        final int hash;
        ……

  1.实现存储读取元素
  先看存储源码:
   public V put(K key, V value) {

        //调用putVal()方法完成
        return putVal(hash(key), key, value, false, true);
    }
     
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        //判断table是否初始化,否则初始化操作
        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 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)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);
                        //链表长度8,将链表转化为红黑树存储
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //key存在,直接覆盖
                    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;
    }

  根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
  hash(int h)方法根据key的重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。
  我们可以看到在中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过的数据结构是数组和链表的结合,所以我们当然希望这个里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。
  根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入中时,程序首先根据该 key的 () 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 () 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 () 方法的说明。
  读取元素源码
   public V get(Object key) {

        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry e = table[indexFor(hash, table.length)];
            e != null;
            e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;

  从中get元素时,首先计算key的,找到数组中对应位置的某一元素,然后通过key的方法在对应位置的链表中找到需要的元素。
  总结起来就是:
   在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时java hash算法原理-java集合的底层原理(Map的底层原理 一),也会根据hash算法找到其在数组中的存储位置,再根据方法从该位置上的链表中取出该Entry。
  1.3 的扩容
  当中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对的数组进行扩容,数组扩容这个操作也会出现在中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置java hash算法原理,并放进去,这就是。
  所以扩容必须满足两条件
  那么什么时候进行扩容呢?当中的元素个数超过数组大小时,就会进行数组扩容,的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知中元素的个数,那么预设元素的个数能够有效的提高的性能。比如说,我们有1000个元素new (1000), 但是理论上来讲new (1024)更合适,不过上面已经说过,即使是1000,也自动会将其设置为1024。 但是new (1024)还不是更合适的,因为0.751000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new (2048)才最合适,既考虑了&的问题,也避免了的问题。
  总结:
  1、是基于哈希表的Map接口的非同步实现,允许使用null值和null键,但不保证映射的顺序。
  2、底层使用数组实现,数组中每一项是个单向链表,即数组和链表的结合体;当链表长度大于一定阈值(8)时,链表转换为红黑树(在Jdk1.8的优化),这样减少链表查询时间。
  3、在底层将key-value当成一个整体进行处理,这个整体就是一个Node对象。底层采用一个Node[]数组来保存所有的key-value对,当需要存储一个Node对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Node时,也会根据key的hash算法找到其在数组中的存储位置,再根据方法从该位置上的链表中取出该Node。
  4、进行数组扩容需要重新计算扩容后每个元素在数组中的位置,很耗性能
  5、采用了Fail-Fast机制,通过一个值记录修改次数,对内容的修改都将增加这个值。迭代器初始化过程中会将这个值赋给迭代器的,在迭代过程中,判断跟是否相等,如果不相等就表示已经有其他线程修改了Map,马上抛出异常
  二、 2.1 概述
  和一样,也是一个散列表,它存储的内容是键值对映射。继承于,实现了Map、、java.io.接口。的函数都是同步的,这意味着它是线程安全的。它的Key、Value都不可以为null。此外,中的映射不是有序的。
  的实例有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量就是哈希表创建时的容量。注意,哈希表的状态为open:在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。加载因子是对哈希表在其容量自动增加之前可以达到多满的一个尺度。初始容量和加载因子这两个参数只是对该实现的提示。关于何时以及是否调用方法的具体细节则依赖于该实现。通常,默认加载因子是0.75。
  2.2数据结构
  与Map关系如下
  
   public class Hashtable

        extends Dictionary  
        implements Map, Cloneable, java.io.Serializable 

  并没有去继承,而是选择继承了类,是个被废弃的抽象类
  2.3实现原理
  成员变量跟基本类似,但是更加规范,内部还定义了一些常量,比如默认的负载因子,默认的容量,最大容量等。
<p><pre>public Hashtable(int initialCapacity, float loadFactor) {//可指定初始容量和加载因子

    if (initialCapacity < 0)  
        throw new IllegalArgumentException("Illegal Capacity: "+  
                                           initialCapacity);  
    if (loadFactor = Holder.ALTERNATIVE_HASHING_THRESHOLD);  
}  
/** 
 * Constructs a new, empty hashtable with the specified initial capacity 
 * and default load factor (0.75). 
 */  
public Hashtable(int initialCapacity) {  
    this(initialCapacity, 0.75f);//默认负载因子为0.75  
}  
public Hashtable() {  
    this(11, 0.75f);//默认容量为11,负载因子为0.75  
}  
/** 
 * Constructs a new hashtable with the same mappings as the given 
 * Map.  The hashtable is created with an initial capacity sufficient to 
 * hold the mappings in the given Map and a default load factor (0.75). 
 */  
public Hashtable(Map