HashMap深刻学习

        HashMap 是Map接口的经常使用实现类而且 是以键值对(key,value)采用一种所谓的“Hash算法”,来决定每一个元素的存储位置。程序员

HashMap的存储实现    算法

    当程序试图将多个 key-value 放入 HashMap 中时,以以下代码片断为例: 数组

        HashMap map = new HashMap();性能

            map.put("语文",80.0);this

            map.put(“数学”,89.0);指针

            map.put(“英语”,78.2)code

当程序执行map.put(“语文”,80.0)时,系统将调用“语文”的hashCode方法获得其HashCode值-每一个Java对象都有一个HashCode方法,均可以经过该方法得到它的hashCode值。获得这个对象的hashCode值以后,系统会根据该hashCode值来决定元素的存储位置。对象

    看一下HashMap类的put()方法源码代码以下。索引

    public V put(K key, V value) {接口

        //table 为空则初始化table(Entry[])大小,和hash的掩码值(须要的时候初始化)

    if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }

//key值为空则调用putForNullKey方法进行处理
        if (key == null)
            return putForNullKey(value);

        //根据key的keyCode计算Hash值
        int hash = hash(key);

    //搜索指定的hash值对应table中的索引
        int i = indexFor(hash, table.length);

//若是i索引处的Entry 不为null,经过循环不断遍历e元素的下一个元素
        for (Entry e = table[i]; e != null; e = e.next) {
            Object k;

            //找到指定key与须要放入的key相等(hash值相等,经过equals比较返回true)
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

//若是i索引处的Entry为null,代表此处尚未Entry 

        modCount++;

    //将key、value添加到i索引处
        addEntry(hash, key, value, i);
        return null;
    }

 

上面的源码程序中用到了一个重要的内部接口 Map.Entry,每一个Map.Entry实际上是一个key-value对。当系统决定存储HashMap的key-value对时,彻底没有考虑Entry 中的value,而仅仅只是根据key来计算并决定每一个Entry的存储位置。

 从上面put方法的源码能够看出,当程序试图将一个key-value对放入HashMap中时,首先根据key的hashcode()返回值决定该Entry的存储对象的位置:若是两个Entry的key的hashcode()返回值相同,那它们的存储位置相同;若是这两个Entry 的key经过equals比较返回true,新添加Entry的value将覆盖集合中的原有的Entry的value,但key不会覆盖;若是这两个Entry 的key经过equals比较返回false,新添加的Entry将与集合中原有的Entry 造成Entry链,并且新添加的Entry位于Entry链的头部-具体看AddEntry()方法说明

注意:当向HashMap中添加key-value对,由其key的hashcode()返回值决定该key-value对(Entry对象)的存储位置。当两个Entry对象的Key的hashcode()返回值相同时,将由key的equeals()比较值决定是采用覆盖行为(返回true执行),仍是产生Entry链(返回false执行)

上面程序中还调用了addEntry(hash,key,value,i);代码,其中addEntry是hashMap提供的一个包的访问权限的方法,该方法仅用于添加一个key-value对,下面是该方法的源码

 void addEntry(int hash, K key, V value, int bucketIndex) {

    //若是Map中的key-value对数量超过了极限,当size>=threshold时,HashMap会自动调用resize方法扩充HashMap的容量。每扩充一次,hashMap就增大一倍。
        if ((size >= threshold) && (null != table[bucketIndex])) {

            //把table对象的长度扩充2倍
            resize(2 * table.length);
            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) {

    //获取指定bucketIndex索引处的Entry
        Entry e = table[bucketIndex];  //1

    //将建立的Entry仿佛bucketIndex索引处,并让新的Entry指向原来的Entry
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

上面的源代码很简单,系统将新添加的Entry对象放入table数组的bucketIndex索引处,若是bucketIndex索引出已经有一个Entry对象,新添加的Entry对象指向原有的Entry对象(产生一个Entry链);若是bucketIndex索引处没有Entry对象,也就是1处的e变量为null,即新放入的Entry对象指向null,就没有产生Entry链。(注意:在同一个bucketIndex存储Entry链的状况下,新放入的Entry老是位于bucketIndex索引中,而最先放入该bucketIndex索引位置的Entry则位于Entry链的最末端)

 size:改变量保存了该HashMap中全部包含key-value对的数量。

threshold:该变量包含了HashMap能容纳的key-value对的极限,它的值等于HashMap容量乘以负载因子(DEFAULT_LOAD_FACTOR)

table是一个普通的数组,每一个数组都有一个固定的长度,这个数组的长度就是HashMap的容量。

jdk1.7之前,建立HashMap时,系统会自动建立一个table数组来保存HashMap 的Entry。jdk1.7中是当掉用put方法时才对建立table数组。下面源码:

jdk1.6HashMap构造方法

public HashMap(int initialCapacity, float loadFactor) {

        //初始容量不能为负数
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);

        //若是初始容量大于最大容量,让初始容量等于最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;

        //负载因子必须是大于0的数值
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        //========如下方法是jdk1.7之前版本的内容。

    //计算出大于initialCapacity的最小的2的n次方值   

     int capacity =1;

        while(capacity<initialCapacity)

            capacity<<=1;

        this.loadFactor = loadFactor;

//    设置容量极限等于容量乘以负载因子

        threshold =(int)(capacity*loadFactory);

    table = new Entry[capacity];

//=========================

      this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }

jdk1.7的put方法:

  public V put(K key, V value) {

    //若是table值为空则建立
        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;
    }

对于HashMap和子类而言,它们采用Hash算法来决定集合中元素的存储位置。当系统开始初始化HashMap时(jdk1.7之前版本)或者调用put(key,value)(jdk1.7以上版本)方法时。系统会建立一个长度为capacity的Entry数组。这个数组存储元素的位置被称为“桶(bucket)”,每一个bucket都有指定的索引,系统能够根据其索引快速访问该bucket里存储的元素。

不管什么时候,HashMap的每一个“桶”只能存储一个元素(即一个Entry),因为Entry对象包含一个引用变量(就是entry构造器的最后一个参数)用于指向下一个Entry,所以可能出现:HashMap的bucket中的只有一个Entry,但这个Entry指向另外一个Entry----造成一个Entry链。以下图:

当HashMap的每一个bucket里存储的Entry只是单个Entry,即没有经过指针产生Entry链时,此时的HashMap具备最好的性能。当程序经过key取出对应的value值时,系统只要先计算出该key的hashcode()返回值,在根据系统HashCode返回值找出该key在table数组中的索引,而后取出该索引出的Entry,最后返回该key对应的value。HashMap对应的get(K key)方法源码以下:

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

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

 

   */
    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;
    }

 

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

代码中能够看出,若是hashMap的每一个bucket里只有一个Entry,HashMap能够根据索引快速的取出该bucket里的Entry.在发生Entry冲突状况下,单个bucket里存储的不是一个Entry,而是一个Entry链,系统只能按书序遍历每一个Entry,直到直到想要的Entry为止。若是下号要搜索的Entry位于该Entry链的末端(该Entry最先放入该bucket链,那么系统必须循环到最后才能找到元素)。

总结:HashMap在底层将key-value当成一个总体进行处理。这个总体是一个Entry对象。HashMap底层采用一个Entry[]数组来保存全部的key-value对,当须要存储一个Entry对象时,会根据Hash算法来决定其存储位置;当须要取出一个Entry时,也会根据算法找到其存储位置,直接取出该Entry。因而可知,HashMap之因此能快速存、取它所包含的Entry,彻底相似于现实生活中的:不一样的东西放在不一样的位置,须要时才能快速找到它。

当建立HashMap时,有一个默认的负载因子(load factor),其默认值为0.75.这是时间和空间成本的一种折中;增大负载因子能够减小Hash表(减小Entry数组)所占用的内存空间,但会增长查询数据的时间开销,而查询是最频繁的操做(HashMap 的get()与put()方法都要用到查询);减少负载因子会提升数据的查询性能,但会下降Hash表所占的内存空间。

掌握了上面的知识。能够在建立HashMap的时候根据实际状况适当地调整load factor的值。。若是程序比较关系内存的开销。内存比较紧张,能够适当的增长负载因子;若是程序比较关心时间开销,内存比较宽裕,则能够适当地减小负载因子,一般状况下。程序员不须要改变负载因子的值

相关文章
相关标签/搜索