【转载】HashSet与HashMap关系之源码分析

本文深入解析HashSet和HashMap之间的关系,详细讲解它们的底层实现原理,包括HashSet如何利用HashMap实现,以及如何通过hashCode和equals方法来确定元素的唯一性。同时,文章还阐述了HashMap的put方法实现细节,包括如何计算hash值、如何处理冲突等关键步骤。

题目:请说出hashCode方法,equals方法,HashSet,HasMap之间的关系?

解答:策略,分析jdk的源代码:

  1. public HashSet() {   
  2. ap = new HashMap<E,Object>();   
  3.   }  

  public HashSet() {

  map = new HashMap<E,Object>();

    }

1、HashSet底层是采用HashMap实现的。

private transient HashMap<E,Object> map;是HashSet类里面定义的一个私有的成员变量。并且是transient类型的,在序列化的时候是不会序列化到文件里面去的,信息会丢失。HashMap<E,Object>里面的key为E,是HashSet<E>里面放置的对象E(对象的引用,为了简单方便起见我说成是对象,一定要搞清楚,在集合里面放置的永远都是对象的引用,而不是对象,对象是在堆里面的,这个一定要注意),HashMap<E,Object>的value是Object类型的。

2、这个HashMap的key就是放进HashSet中对象,value是Object类型的。当我去使用一个默认的构造方法的时候,执行public HashSet() {map = new HashMap<E,Object>();},会将HashMap实例化,将集合能容纳的内型作为key,Object作为它的value。当我们去往HashSet里面放值的时候,这个HashMap<E,Object>里面的Object类型的value到底取什么值呢?这个时候我们就需要看HashSet的add方法,因为add方法可以往里面增加对象,通过往里面增加对象,会导致底层的HashMap增加一个key和value。这个时候我们看一下add方法:  public boolean add(E e) {return map.put(e, PRESENT)==null;},add方法接受一个E类型的参数,这个E类型就是我们在HashSet里面能容纳的类型,当我们往HashSet里面add对象的时候,HashMap是往里面put(E,PRESET),增加E为key,PRESET为value,PRESET是这样定义的:private static final Object PRESENT = new Object();从这里我们知道PRESET是private static final Object的常量并且已经实例化好了。HashMap<E,Object>()中的key为HashSet中add的对象,不能重复,value为PRESET,这是jdk为了方便将value设为常量PRESET,因为value是可以重复的。

3、当调用HashSet的add方法时,实际上是向HashMap中增加了一行(key-value对),该行的key就是向HashSet增加的那个对象,该行的value就是一个Object类型的常量。

接着,我们看看HashMap的源码,流程讲到map.put(e, PRESENT)==null,我们往HashSet里面add一个对象,那么HashMap就往里面put(key,value)。HashMap的put方法源码如下:

  1. public V put(K key, V value) {   
  2.         if (key == null)   
  3.             return putForNullKey(value);   
  4.         int hash = hash(key.hashCode());   
  5.         int i = indexFor(hash, table.length);   
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {   
  7.             Object k;   
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {   
  9.                 V oldValue = e.value;   
  10.                 e.value = value;   
  11.                 e.recordAccess(this);   
  12.                 return oldValue;   
  13.             }   
  14.         }   
  15.   
  16.         modCount++;   
  17.         addEntry(hash, key, value, i);   
  18.         return null;   
  19.     }  

public V put(K key, V value) {

        if (key == null)

            return putForNullKey(value);

        int hash = hash(key.hashCode());

        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;

    }

首先,如果key == null,这个key是往HashSet里面add的那个对象,它返回一个putForNullKey(value),它是一个方法,源码如下:

  1. private V putForNullKey(V value) {   
  2.        for (Entry<K,V> e = table[0]; e != null; e = e.next) {   
  3.            if (e.key == null) {   
  4.                V oldValue = e.value;   
  5.                e.value = value;   
  6.                e.recordAccess(this);   
  7.                return oldValue;   
  8.            }   
  9.        }   
  10.        modCount++;   
  11.        addEntry(0, null, value, 0);   
  12.        return null;   
  13.    }  

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

        return null;

    }

这里面有一个Entry,我们看看它的源码:

  1. static class Entry<K,V> implements Map.Entry<K,V> {   
  2.         final K key;   
  3.         V value;   
  4.         Entry<K,V> next;   
  5.         final int hash;   
  6.   
  7.         /**  
  8.          * Creates new entry.  
  9.          */  
  10.         Entry(int h, K k, V v, Entry<K,V> n) {   
  11.             value = v;   
  12.             next = n;   
  13.             key = k;   
  14.             hash = h;   
  15.         }   
  16.   
  17.         public final K getKey() {   
  18.             return key;   
  19.         }   
  20.   
  21.         public final V getValue() {   
  22.             return value;   
  23.         }   
  24.   
  25.         public final V setValue(V newValue) {   
  26.         V oldValue = value;   
  27.             value = newValue;   
  28.             return oldValue;   
  29.         }   
  30.   
  31.         public final boolean equals(Object o) {   
  32.             if (!(o instanceof Map.Entry))   
  33.                 return false;   
  34.             Map.Entry e = (Map.Entry)o;   
  35.             Object k1 = getKey();   
  36.             Object k2 = e.getKey();   
  37.             if (k1 == k2 || (k1 != null && k1.equals(k2))) {   
  38.                 Object v1 = getValue();   
  39.                 Object v2 = e.getValue();   
  40.                 if (v1 == v2 || (v1 != null && v1.equals(v2)))   
  41.                     return true;   
  42.             }   
  43.             return false;   
  44.         }   
  45.   
  46.         public final int hashCode() {   
  47.             return (key==null   ? 0 : key.hashCode()) ^   
  48.                    (value==null ? 0 : value.hashCode());   
  49.         }   
  50.   
  51.         public final String toString() {   
  52.             return getKey() + "=" + getValue();   
  53.         }   
  54.   
  55.         /**  
  56.          * This method is invoked whenever the value in an entry is  
  57.          * overwritten by an invocation of put(k,v) for a key k that's already  
  58.          * in the HashMap.  
  59.          */  
  60.         void recordAccess(HashMap<K,V> m) {   
  61.         }   
  62.   
  63.         /**  
  64.          * This method is invoked whenever the entry is  
  65.          * removed from the table.  
  66.          */  
  67.         void recordRemoval(HashMap<K,V> m) {   
  68.         }   
  69.     }  

static class Entry<K,V> implements Map.Entry<K,V> {

        final K key;

        V value;

        Entry<K,V> next;

        final int hash;

 

        /**

         * Creates new entry.

         */

        Entry(int h, K k, V v, Entry<K,V> n) {

            value = v;

            next = n;

            key = k;

            hash = h;

        }

        public final K getKey() {

            return key;

        }

        public final V getValue() {

            return value;

        }

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

        }

        public final String toString() {

            return getKey() + "=" + getValue();

        }

        /**

         * 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) {

        }

    }

static class Entry表示它是一个静态的内部类,注意,外部类是不能用static的,对类来说,只有内部类能用static来修饰。这个Entry它实现了一个Map.Entry<K,V>接口,我看看Map.Entry<K,V>的源码:

  1. interface Entry<K,V> {   
  2.     K getKey();   
  3.     V getValue();   
  4.     V setValue(V value);   
  5.     boolean equals(Object o);   
  6.     int hashCode();   
  7.     }  

interface Entry<K,V> {

K getKey();

V getValue();

V setValue(V value);

boolean equals(Object o);

int hashCode();

    }

Map.Entry<K,V>它是一个接口,里面定义了几个方法,Map.Entry<K,V>它实际上是一个内部的接口(inner interface),Map.Entry干什么用的呢?我看看Map接口有一个 Set<Map.Entry<K, V>> entrySet();方法,它返回一个Set集合,它里面是Map.Entry<K, V>类型的,这个方法用得非常多。表示你在调用一个Map的entrySet()方法,它会返回一个Set集合,这个集合里面放置的就是你的Map的key和value这两个对象所共同组成的Map.Entry类型的那样的对象,Map.Entry类型对象里面放置的就是你的HashMap里面的key和value,所以说当你去调用一个Map(不管是HashMap还是Treemap)的entrySet()方法,会返回一个Set集合,个集合里面放置的就是你的Map的key和value这两个对象所共同组成的Map.Entry类型的那样的对象。

HashMap的底层是怎样维护的呢?我们看一下源码:

  1. /**  
  2.    * The table, resized as necessary. Length MUST Always be a power of two.  
  3.    */  
  4.   transient Entry[] table;  

  /**

     * The table, resized as necessary. Length MUST Always be a power of two.

     */

    transient Entry[] table;

它是一个Entry类型的数组,table数组里面放Entry类型的,Entry的源码有:

  1. final K key;   
  2.    V value;  

     final K key;

        V value;

这里的K表示HashMap的key,V表示HashMap的value,所以我们可以大胆的断定,HashMap的底层是用数组来维护的。理解这一点非常重要,因为java的集合,底层大部分都是用数组来维护的,顶层之所以有那么高级,是因为底层对其进行了封装。

4、HashMap底层采用数组来维护。数组里面的每一个元素都是Entry,而这个Entry里面是Key和value组成的内容。

我们看HashMap的put方法,如果key == null,返回putForNullKey(value),看putForNullKey方法:

Java代码

  1. private V putForNullKey(V value) {   
  2.         for (Entry<K,V> e = table[0]; e != null; e = e.next) {   
  3.             if (e.key == null) {   
  4.                 V oldValue = e.value;   
  5.                 e.value = value;   
  6.                 e.recordAccess(this);   
  7.                 return oldValue;   
  8.             }   
  9.         }   
  10.         modCount++;   
  11.         addEntry(0, null, value, 0);   
  12.         return null;   
  13.     }  

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

        return null;

    }

首先做一个for循环,从Entry数组的第一个元素开始,e.next是static class Entry<K,V> implements Map.Entry<K,V>的一个成员属性 Entry<K,V> next;next也是Entry<K,V>类型的,next也可以指向Entry类型的对象,当我知道一个Entry类型的对象,就可以通过它的next属性知道这个Entry类型的对象的后面一个Entry类型的对象,通过这个next变量来寻找下一个。next指向的Entry类型的对象不在数组中。

接着判断e.key == null,如果e.key 为null,说明HashMap里面没有这个对象,这个时就会将我们add到Set里面的对象真真正正的放置到Entry数组里面去。

我们在看HashMap的put方法里面:int hash = hash(key.hashCode());如果key不为null,这个key就是我们往HashSet里面放置的那个对象。它就会调用key.hashCode()方法,这是流程转到hashCode方法里面去了,调用hashCode()方法返回hash码,接着调用hash方法,我们看看hash方法源码:

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

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

    }

这个hash方法异常复杂。并且jdk5.0和jdk6.0对这个方法的实现方式也是不一样的。hash方法就是我们在数据结构中讲的散列函数。它是经过放进HashSet里面的对象作为key得到hashCode码,在进行散列得到的一个整数。

接着执行put方法里面的int i = indexFor(hash, table.length);语句,我们看看indexFor方法的源码:

  1. * Returns index for hash code h.   
  2.     */   
  3.    static int indexFor(int h, int length) {   
  4.        return h & (length-1);   
  5.    }  

 * Returns index for hash code h.

     */

    static int indexFor(int h, int length) {

        return h & (length-1);

    }

indexFor方法也返回一个整型值,将上面通过hash方法得到的int值和数组的长度进行与运算,然后返回。他是往HashSet里面放置对象位置的索引值,它的值永远在0到数组长度减1之间,它是不会超过数组长度的。它是有hash方法和indexFor方法来保证不会超过的。

所以对于put方法,如果indexFor返回2,那么就在数组的第2个位置,然后执行for循环,如果第2个位置没有元素,则跳出for循环,调用addEntry方法将这个元素添加到数组中第2个位置,addEntry(hash, key, value, i);这是比较简单的情况。比较复杂的情况,数组的第2个位置已经有元素了,不为null,那么它是怎么做的呢?我们看一下:

  1. Object k;   
  2.           if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {   
  3.               V oldValue = e.value;   
  4.               e.value = value;   
  5.               e.recordAccess(this);   
  6.               return oldValue;   
  7.           }  

  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;

            }

它用这种方式方式进行比较,(e.hash == hash && ((k = e.key) == key || key.equals(k))它会医用k的equals方法来个第2个位置上的元素进行比较,如果这个equals方法返回true,表示这个对象确实是一个对象,这个时候还没有完,如果e.hash == hash并且key.equals(k)表示真的是相同的,这个时候就会用新的相同的对象的值替换就的那个值,同时它把旧的那个被替换的值给返回。如果不相同呢,它会根据next指向的对象再去比较,如果又不成功又会根据它的next链去寻找比较,一直到最后一个,当为null的时候下一个就不存在了。这就是put方法的for过程。我们往HashMap里面放值的时候,两种情况,要么放进去了增加(key-value)要么替换掉(将旧的值替换掉了,其实是一样的)了。如果都不成功,表示在链上不存在,这个时候就直接添加到数组,同时将添加的这个Entry的next执行往外替换的那个Entry,这样做的好处就是减少索引时间。不管你链接有多长,每次查找时间固定(用hash方法)。  

5、调用增加的那个对象的hashCode方法,来得到一个hashCode值,然后根据该值来计算出一个数组的下表索引(计算出数组中的一个位置)

6、将准备增加到map中的对象与该位置上的对象进行比较(equals方法),如果相同,那么就将该位置上的那个对象(Entry类型)的value值替换掉,否则沿着该Entry的链接继承重复上述过程,如果到链的最后仍然没有找到与此对象相同的对象,那么这个时候就会将该对象增加到数组中,将数组中该位置上的那个Entry对象链到该对象的后面。

7、对于HashSet,HashMap来说,这样做就是为了提高查找的效率,使得查找时间不随着Set或者Map的大小而改变。

转自http://blog.163.com/zhangbingwu2006@126/blog/static/3111052120093924536946/


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值