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

JAVA源码解读---HashMap目录扩展的奥秘

2014-05-19 13:10 531 查看
JAVA源码解读---HashMap目录扩展的奥秘

摘要:为了探索JAVA1.7源码中HashMap类数据的组织方法与目录扩展方法,本文通过对JAVA1.7源码中HashMap类源码的阅读与分析,得出结论:hashmap中存储数据的数据结构采用的是链表数组,目录是个数组,数组的成员是链表。冲突解决方法:典型的链地址法,冲突后,在链表头部插入数据。目录扩展方法:已二倍的方式扩展,一直到目录的最大上限。目录扩展的触发条件:装载因子的方式触发。从java中hashmap的实现可以看出,桶数据的组织方式并不是一种非常高效的方式。对检索效率不利。同时,数据扩展简单的采用二倍的扩展方法,也只是使用了最为粗暴的扩展方式,扩展开销较大。

关键字:JAVA,HashMap,目录组织方式,目录扩展方法,目录触发条件

本文转自http://blog.csdn.net/daliaojie/article/details/26236979

散列是一种非常重要的数据结构,在JAVA与dotNet中都有相对应事先的类供调用。我们知道hashmap的容量是动态增长的,此篇博客分析了java中,hashmap中关于目录扩展的过程。

先看hashmap的成员变量:

static final int DEFAULT_INITIAL_CAPACITY = 16;

static final int MAXIMUM_CAPACITY = 1 << 30;

static final float DEFAULT_LOAD_FACTOR = 0.75f;

transient Entry[] table;

transient int size;

int threshold;

final float loadFactor;

transient int modCount;


其中
transient Entry[] table;


这便是,hashmap中的散列的目录,可以看出,他是一个数组,里面存储了类。其他成员有,默认的目录长度

 DEFAULT_INITIAL_CAPACITY。默认的目录最大长度:

 static final int MAXIMUM_CAPACITY = 1 << 30;

默认的填充因子:

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

当然这些变量可以由构造函数初始化。

对应的成员变量为:
transient int size;

int threshold;

final float loadFactor;


填充因子的意义为,达到这个比例后,目录需要扩张。为了探索,hash目录扩张的秘密:

我们从put操作看起,其实就是insert操作。

对了, 我们们的目录结构还没看完:

    transient Entry[] table;

我们看Entry是如何定义的:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;


很明显, 目录中的桶采用的数据组织方法为链表。

也就是说。hash的冲突解决方法为链地址法。

关于散列中解决冲突的方法连地址法的详细介绍可以自己百度。
直接看insert过程吧。

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

put操作的过程:

将key进行散列并计算出目录中对应的位置。从而获取该位置处的链表。并对该链表进行遍历,遍历时,检查是否该key已经存在,如果存在就用将新的value替换掉旧的value。

如果不存在。就调用

addEntry(hash, key, value, i);
进行插入。并且是在改目录位置处插入。

我们看插入操作做了些什么:

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


方法的刚开始,获取了链表。

并头早了一个新的Entry,我们看对应的构造方法:

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


这个Entry的构造函数很明白了。

table[bucketIndex] = new Entry<>(hash, key, value, e);


实现了在bucktIndex处的连标的头部插入key-value的链表节点。明显这个插入过程还是比较高效的。

我们继续往后看。

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


该方法,在链表的头部插入新的节点后判断了当前size++与目录扩展阀值的关系。当达到分裂阀值后,执行
resize(2 * table.length);
操作。我们看它对应的方法。

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


方法传入了扩展后的目录长度。我们知道,目录是以2倍的方式,进行扩展的。并且最大有个限制,这里默认是1 <<30

resize方法里,先对新的目录长度进行检测,以防止,超过目录的最大长度。

然后new了一个新长度的目录。

然后执行

transfer(newTable);
源码如下:

/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}


方法的英文解释其实很明白了。他们将源目录中的所有元素进行遍历,经过对新目录长度的散列后,放进去了新的目录中。

解读一下吧。

for()是对原目录的遍历控制。

然后do while是对目录位置处的链表进行遍历时的控制。

目录扩展后,便将就目录更换为新的目录。并更新了目录扩展的阀值。

我们看一下map的查询操作:

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


查询过程还是容易看的:首先计算key对应的目录位置,然后目录位置处对应的链表中,查询key是否存在。

做一下总结:

hashmap中存储数据的数据结构:链表数组。目录是个数组,数组的成员是链表。

冲突解决方法:典型的链地址法,冲突后,在链表头部插入数据。

目录扩展方法:已二倍的方式扩展,一直到目录的最大上限。

目录扩展的触发条件:装载因子的方式触发。

尾语:

从java中hashmap的实现可以看出,桶数据的组织方式并不是一种非常高效的方式。对检索效率不利。同时,数据扩展简单的采用二倍的扩展方法,也只是使用了最为粗暴的扩展方式。扩展开销较大。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: