JavaSE第五十二讲:HashSet 与HashMap源代码深度剖析
2012-12-26 11:24
363 查看
开篇寄语:HashSet底层是用Map来实现的,而HashMap底层就是用数组和链表来实现的。
查看HashSet源代码:
【说明】:从这边可以发现,当我们往Set中增加一个值的时候,这个值是存在Map中的key上面,value是一个常量,当增加第二值的时候,此时value同样是一个常量,value都是同一个对象。所以这边的Map仅仅是用到它的key,而value对它来说是无意义的。但是Map是有[key, value]所构成的所以必须要用。
1. HashSet底层是使用HashMap实现的。当使用add方法将对象添加到Set当中时,实际上是将该对象作为底层所维护的Map对象的key,而value则都是同一个Object对象(该对象我们用不上)。
查看HashSet中源代码 remove(),clear(),size(),iterator()等方法返回的都是map中这些方法,其实 iterator()方法返回:return map.keySet().iterator();是利用keySet()方法键值取出再迭代。这就是HashSet底层的实现与map是紧密相关的。现在我们来看一下HashMap的底层实现方式。
HashMap源代码剖析:
查看其构造方法:
[DEFAULT_LOAD_FACTOR : 默认的负载因子,是0.75]
Entry[] 类型数组 table; 这个table需要的时候可以调整其大小,长度总是2的指数
继续跟踪Entry
接下来继续查看其put()方法:
跟踪到这边,相比都已经很凌乱了,在这边我们来讲一下Hash表(哈希表)的思想:
一个数组,如果将一个元素放置再这个数组中,通常情况下都是按顺序存放,但是再查找(二分或其它)的时候都会涉及到排序,导致元素的移动,这种情况造成了效率的低下,所以出现了Hash(哈希),Hash是一个函数,当往这个数组中放置对象的时候会和这个函数进行运算,得出Hash值,这个值为转为一个位置,这个位置就是这个对象需要放入数组的位置。如果我需要取出这个元素,同样的也会通过Hash这个函数进行运算,得到的结果会定位到我查找的这个元素的位置,从而将其取出。
但是,这边会存在一种冲突情况是:当我通过Hash函数运算得出的位置正好已经有元素存在了,所以它会提供一个“再散列” 的函数再进行计算得到位置,然后存放,如果这个时候位置还是被别的元素占有,此时便不再提供hash函数了,因为它认为这个元素已经快满了。而会自动加长这个数组,从而将这个元素放入加长的这个数组中去。这种情况下碰撞
的概率就会低一下了。
而我们什么时候知道这个数组快满了呢?这就有loadFactor[负载因子]来决定的。loadFactor = 0.75 表示数组里面的元素占的位置超过 75%
的时候表示快满了。
HashMap 和 HashSet 都带有Hash,它的底层都是通过 hash函数运算来实现元素在数组中的存放的。
图52-1:hash实现方式
DEFAULT_INITIAL_CAPACITY :就表示初始数组的长度。
2. HashMap底层维护一个数组,我们向HashMap中所放置的对象实际上是存储在该数组当中;而这个对象不当当只是一个值而已,它是一个Entry类型的对象,而这个对象维护着几个变量,查看源代码:
如果这个元素已经存在,则又进行一些运算,继续查看put()方法中for循环语句:
如果这个元素在数组中不存放,则进行put()方法中addEntry(hash, key, value, i);方法,数组长度不够,则将其扩充2倍。
在这个方法中:addEntry(hash, key, value, i); i 是根据hashCode值通过hash函数算出的在第几个位置,i传入 bucketIndex。
Entry<K,V> e = table[bucketIndex];将这个位置上的值拿到,然后构造出Entry对象。next = n 将它赋值给n的引用。n为指向下一个元素的引用。这边如果赋值的这个元素,原来的位置已经占据了,怎么办呢?返回put()方法中的for循环: for (Entry<K,V> e = table[i]; e != null; e = e.next) 如果这个元素已经存在,则指向下一个链的结点。[注意Entry有一个next引用,可以指向下一个Entry对象]查看图52-2所示
图52-2
注意它再这个数组中实现链表的时候,是替换后把最近的元素放在理数组最近的,根据操作系统离乱:最近被使用的元素,将来最有可能被使用
【说明】:对于map这个对象,底层是一个数组,每一个数组元素的类型都是一个Entry类型,每一个Entry类型都是有一个链表这种结构,而每一个Entry里面包含的是key,value的信息,还有指向下一个链表结点的next引用,所以他是通过数组和链表的这种混合的结构。
【总结】:
当向HashMap中put一对键值时,它会根据key的hashCode值计算出一个位置,
该位置就是此对象准备往数组中存放的位置。
如果该位置没有对象存在,就将此对象直接放进数组当中;如果该位置已经有对象
存在了,则顺着此存在的对象的链开始寻找(Entry类有一个Entry类型的next成员
变量,指向了该对象的下一个对象),如果此链上有对象的话,再去使用equals方
法进行比较,如果对此链上的某个对象的equals方法比较为false,则将该对象放到
数组当中,然后将数组中该位置以前存在的那个对象链接到此对象的后面。
HashMap的内存实现布局:
图52-3
从这边可以看出数组是有多么的重要,HashSet和HashMap底层不外乎就是用数组和链表来实现的,只有数组才可以管理多个相同的对象,查看JDK Doc 文档中的HashMap构造方法:
这边的构造方法其实原理都是一样的,第一个和第二个构造方法不外乎就是调用第三个的构造方法。
查看HashSet源代码:
/** * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has * default initial capacity (16) and load factor (0.75). */ public HashSet() { map = new HashMap<E,Object>(); }在HashSet()底层,构造一个空的构造方法,这个方法体中会new一个HashMap();继续跟踪map定义.
private transient HashMap<E,Object> map;HashSet底层会维护一个HashMap类型的 map对象。查看构造方法看不出所以然来,继续跟踪add()方法,查看添加元素时候的情况。
public boolean add(E e) { return map.put(e, PRESENT)==null; }如果put增加内容为空,则返回真。其中e表示往Set中增加的对象,PRESENT表示一个常量,
// Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object();[一个假的值关联到这个对象上,再底层Map中]
【说明】:从这边可以发现,当我们往Set中增加一个值的时候,这个值是存在Map中的key上面,value是一个常量,当增加第二值的时候,此时value同样是一个常量,value都是同一个对象。所以这边的Map仅仅是用到它的key,而value对它来说是无意义的。但是Map是有[key, value]所构成的所以必须要用。
1. HashSet底层是使用HashMap实现的。当使用add方法将对象添加到Set当中时,实际上是将该对象作为底层所维护的Map对象的key,而value则都是同一个Object对象(该对象我们用不上)。
public boolean remove(Object o) { return map.remove(o)==PRESENT; }
public void clear() { map.clear(); }
public int size() { return map.size(); }
public Iterator<E> iterator() { return map.keySet().iterator(); }
查看HashSet中源代码 remove(),clear(),size(),iterator()等方法返回的都是map中这些方法,其实 iterator()方法返回:return map.keySet().iterator();是利用keySet()方法键值取出再迭代。这就是HashSet底层的实现与map是紧密相关的。现在我们来看一下HashMap的底层实现方式。
HashMap源代码剖析:
查看其构造方法:
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); }[loadFactor : 负载因子,是一个float类型的成员变量,它是底层所维护的Hash表的负载因子]
[DEFAULT_LOAD_FACTOR : 默认的负载因子,是0.75]
/** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;[DEFAULT_INITIAL_CAPACITY : 默认的初始化容量,是16,(MUST be a power of two)必须是2的指数]
/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 16;
Entry[] 类型数组 table; 这个table需要的时候可以调整其大小,长度总是2的指数
/** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry[] table;
/** * Initialization hook for subclasses. This method is called * in all constructors and pseudo-constructors (clone, readObject) * after HashMap has been initialized but before any entries have * been inserted. (In the absence of this method, readObject would * require explicit knowledge of subclasses.) */ void init() { }初始化,包级别的类型,只能再包里面使用。
继续跟踪Entry
static class Entry<K,V> implements Map.Entry<K,V>只贴出类的声明,Entry是一个内部类(HashMap$Entry.class 注意类名可以用$符号来表示,但是我们不建议这样定义,因为它是作为内部类来使用的)
接下来继续查看其put()方法:
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时候,执行putForNullKey()方法,继续跟踪这个方法:
/** * Offloaded version of put for null keys */ 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; }modCount这个表示当前这个集合被修改[增加或者移除]的次数,每个集合都有这个东西。
transient volatile int modCount;跟踪addEntry()方法:
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
跟踪到这边,相比都已经很凌乱了,在这边我们来讲一下Hash表(哈希表)的思想:
一个数组,如果将一个元素放置再这个数组中,通常情况下都是按顺序存放,但是再查找(二分或其它)的时候都会涉及到排序,导致元素的移动,这种情况造成了效率的低下,所以出现了Hash(哈希),Hash是一个函数,当往这个数组中放置对象的时候会和这个函数进行运算,得出Hash值,这个值为转为一个位置,这个位置就是这个对象需要放入数组的位置。如果我需要取出这个元素,同样的也会通过Hash这个函数进行运算,得到的结果会定位到我查找的这个元素的位置,从而将其取出。
但是,这边会存在一种冲突情况是:当我通过Hash函数运算得出的位置正好已经有元素存在了,所以它会提供一个“再散列” 的函数再进行计算得到位置,然后存放,如果这个时候位置还是被别的元素占有,此时便不再提供hash函数了,因为它认为这个元素已经快满了。而会自动加长这个数组,从而将这个元素放入加长的这个数组中去。这种情况下碰撞
的概率就会低一下了。
而我们什么时候知道这个数组快满了呢?这就有loadFactor[负载因子]来决定的。loadFactor = 0.75 表示数组里面的元素占的位置超过 75%
的时候表示快满了。
HashMap 和 HashSet 都带有Hash,它的底层都是通过 hash函数运算来实现元素在数组中的存放的。
图52-1:hash实现方式
DEFAULT_INITIAL_CAPACITY :就表示初始数组的长度。
2. HashMap底层维护一个数组,我们向HashMap中所放置的对象实际上是存储在该数组当中;而这个对象不当当只是一个值而已,它是一个Entry类型的对象,而这个对象维护着几个变量,查看源代码:
static class Entry<K,V> implements Map.Entry<K,V> {结合put()方法,跟踪里面的hash()方法,这个方法可以将其看成一个hash函数。
final K key;
V value;
Entry<K,V> next;
final int 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); }放回到put()方法:
int hash = hash(key.hashCode()); int i = indexFor(hash, table.length);
/** * Returns index for hash code h. */ static int indexFor(int h, int length) { return h & (length-1); }根据传入key的hashCode()值计算hash()方法,得到一个hash值,将这个值传入indexFor()方法中,这个方法计算得到这个元素的存放位置。
如果这个元素已经存在,则又进行一些运算,继续查看put()方法中for循环语句:
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; } }如果对应的key值已经存在,则对value值将被新放入的元素替换掉原来的元素,而将旧的元素返回。
如果这个元素在数组中不存放,则进行put()方法中addEntry(hash, key, value, i);方法,数组长度不够,则将其扩充2倍。
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
/** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
在这个方法中:addEntry(hash, key, value, i); i 是根据hashCode值通过hash函数算出的在第几个位置,i传入 bucketIndex。
Entry<K,V> e = table[bucketIndex];将这个位置上的值拿到,然后构造出Entry对象。next = n 将它赋值给n的引用。n为指向下一个元素的引用。这边如果赋值的这个元素,原来的位置已经占据了,怎么办呢?返回put()方法中的for循环: for (Entry<K,V> e = table[i]; e != null; e = e.next) 如果这个元素已经存在,则指向下一个链的结点。[注意Entry有一个next引用,可以指向下一个Entry对象]查看图52-2所示
图52-2
注意它再这个数组中实现链表的时候,是替换后把最近的元素放在理数组最近的,根据操作系统离乱:最近被使用的元素,将来最有可能被使用
【说明】:对于map这个对象,底层是一个数组,每一个数组元素的类型都是一个Entry类型,每一个Entry类型都是有一个链表这种结构,而每一个Entry里面包含的是key,value的信息,还有指向下一个链表结点的next引用,所以他是通过数组和链表的这种混合的结构。
【总结】:
当向HashMap中put一对键值时,它会根据key的hashCode值计算出一个位置,
该位置就是此对象准备往数组中存放的位置。
如果该位置没有对象存在,就将此对象直接放进数组当中;如果该位置已经有对象
存在了,则顺着此存在的对象的链开始寻找(Entry类有一个Entry类型的next成员
变量,指向了该对象的下一个对象),如果此链上有对象的话,再去使用equals方
法进行比较,如果对此链上的某个对象的equals方法比较为false,则将该对象放到
数组当中,然后将数组中该位置以前存在的那个对象链接到此对象的后面。
HashMap的内存实现布局:
图52-3
从这边可以看出数组是有多么的重要,HashSet和HashMap底层不外乎就是用数组和链表来实现的,只有数组才可以管理多个相同的对象,查看JDK Doc 文档中的HashMap构造方法:
这边的构造方法其实原理都是一样的,第一个和第二个构造方法不外乎就是调用第三个的构造方法。
相关文章推荐
- 深度剖析WinPcap之(序言)——分析WinPcap源代码的缘由
- 深度剖析WinPcap之(序言)——分析WinPcap源代码的缘由
- Java 集合框架 HashSet 和 HashMap 源码剖析
- HashMap HashTable HashSet区别剖析
- 深入源码剖析 HashSet、HashMap、HashTable
- JavaSE第一百零二讲:synchronized关键字常见陷阱深度剖析
- Java HashSet和HashMap源码剖析
- JavaSE 第八十二讲:观察者模式深度剖析
- Java集合框架HashSet和HashMap源码剖析
- Android面试题----HashMap深度剖析
- JavaSE第三十讲:String类陷阱深度剖析
- 通过 HashMap、HashSet 的源代码分析其 Hash 存储机制
- java-通过 HashMap、HashSet 的源代码分析其 Hash 存储机制
- JavaSE第九十九讲:Thread类源码深度剖析
- HashSet, HashTable,HashMap区别剖析
- 给HashMap做个深度剖析手术
- JavaSE第八十五讲:内部类深度剖析及常见使用陷阱
- JavaSE第一百零三讲:wait及notify方法全名、深度剖析
- Java 集合框架 HashSet 和 HashMap 源码剖析
- 24、HashSet与HashMap源代码分析