关于HashMap的源码分析

集合作为java中非常重要的一部分,在开发中经常被用到,而通过java源码实现可以更好理解底层实现。

下面看java中的HashMap的源代码:


首先,HashMap底层是通过数组和hash表实现的,底层的元素都是entry形式,通过计算每个entry的hash值来决定把entry放到哪个位置,如果出现hash冲突,则用链表解决。


首先看entry类的代码:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;

        /**
         * entry包含的成员变量:key,value,next指向下一个节点,(在链表中使用,解决hash冲突),hash值
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

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

        public final String toString() {
            return getKey() + "=" + getValue();
        }

     //这个方法实际上没有实现,但是在linkedhashmap中实现了,而且有重要作用
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }

  //这个方法是用来加入元素时调用的
    void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }


好,下面看put方法

 public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);//如果key为空,则进入putForNullKey方法,下面会分析
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);//根据key的hash值找到在数组中的索引
        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;
            }
        }
       //否则将这个元素加入到最前面<pre name="code" class="java">

modCount++; addEntry(hash, key, value, i); return null; }
 下面进入putForNullKey方法 

这个方法处理key为空的情况,根据代码看table[0]存放key为空的entry,新加入的entry会覆盖value,但是链表长度为1吗,只是说把空值放在这个位置,键值不为空也有可能放在这里啊!

private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);//key为0的话,hash也为0
        return null;
    }


下面看一个有意思的计算hash值的函数:

 static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }


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


注意put()方法的两行代码:

int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);


即通过 hash()函数计算出hash值,再通过indexFor计算出索引,那么indexFor中,为什么是h&(length-1)呢?

下面有一个解释,是摘抄别人的:

这个我们要重点说下,我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低,HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。

 

    接下来,我们分析下为什么哈希表的容量一定要是2的整数次幂。首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

对了,遗忘一个加载因子的参数:


其中loadFactor加载因子是表示Hsah表中元素的填满的程度.

若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.链表长度会越来越长,查找效率降低。

反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)

冲突的机会越大,则查找的成本越高.

因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.

  如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。

 

接下来我们看get()方法:


 public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        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.equals(k)))
                return e.value;
        }
        return null;
    }

private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

上面的代码就很好理解了.

最后看一下resize()方法:


void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);//赋值原来元素到新数组中,消耗性能
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

补充:

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;
            }
        }
我们注意到,如果hashMap 放入元素的时候,如果key值相同,那么会覆盖value值,又因为hashSet底层实际上是用hashMap实现的,hashSet是怎么保证放入的元素不会重复呢?

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
所以hashSet加入的过程同样会调用上面判断的方法判断,

if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 
我们主要看这个判断是否为同一个值的条件:
</pre>e.hash==hash:个人感觉很奇怪,因为上面已经是根据hash算出来的index,如果index相同,那么hash也应该相同。。。。好像不对,<p></p><p></p><pre code_snippet_id="1661232" snippet_file_name="blog_20160426_16_8743048" name="code" class="java">static int indexFor(int h, int length) {
        return h & (length-1);
    }

这是那么indexFor函数,如果返回的index相同,不一定hash相同,而hash是对象的代表(本来hashCode代表对象,而那个hash值又是根据hashCode算出来的,所以hash值间接代表对象(

(k=e.key)==key这里为什么用==,而不用equals?我想了一下,不一定每个对象都有equals方法,比如int类型的

key.equals(k) 这里不用多说,用equals方法判断


通过上面我们可以看出,当使用集合的增加对象时,hashCode()和equal()方法会自动调用,那么我们可以根据情况自己重写hashCode 和equals 来判断能否加进去。

那什么时候可以加进去呢?通过上面的判断条件也可以看出:

1.如果对象的hashCode不同,直接加进去。

2.如果对象的hashCode相同,再比较equals方法(如果没有equals,比较==),如果equals(或==)返回true,加不进去,否则,直接加进去。


那么equals 和 == 的区别呢?

其中一个,null 没有equals方法,所以

String a=null;

String b="b";

a.equals(b)会抛异常,而a==b可以比较


下面摘抄一段它们的异同:

java中的数据类型,可分为两类: 
1.基本数据类型,也称原始数据类型。byte,short,char,int,long,float,double,boolean 
  他们之间的比较,应用双等号(==),比较的是他们的值。 
2.复合数据类型(类) 
  当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。 JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地 址,但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了。
  对于复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals方法也是用双等号(==)进行比较的,所以比较后的结果跟双等号(==)的结果相同。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值