您的位置:首页 > 编程语言 > Java开发

JavaSE第五十二讲:HashSet 与HashMap源代码深度剖析

2012-12-26 11:24 363 查看
开篇寄语:HashSet底层是用Map来实现的,而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> {
final K key;
V value;
Entry<K,V> next;
final int hash;
结合put()方法,跟踪里面的hash()方法,这个方法可以将其看成一个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构造方法:



这边的构造方法其实原理都是一样的,第一个和第二个构造方法不外乎就是调用第三个的构造方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: