设为首页 加入收藏

TOP

深入理解 hash 函数、HashMap、LinkedHashMap、TreeMap (一)
2015-07-20 17:39:55 来源: 作者: 【 】 浏览:8
Tags:深入 理解 hash 函数 HashMap LinkedHashMap TreeMap

前言

Map 是非常常用的一种数据接口。在 Java 中,提供了成熟的 Map 实现。

\

图 1

最主要的实现类有 Hashtable、HashMap、LinkedHashMap和 TreeMap。在 HashTable 的子类中,还有 Properties的实现。Properties 是专门读取配置文件的类,我们会在稍后介绍。这里首先值得关注的是 HashMap 和 HashTable 两套不同的实现,两者都实现了 Map 接口。从表面上看,并没有多大差别,但是在内部实现上却有些微小的细节。

首先,HashTable 的大部分方法都做了同步,而 HashMap 没有,因此, HashMap 不是线程安全的。
其次,HashTable 不允许 key 或者 value 使用 null 值,而 HashMap 可以。
第三,在内部实现算法上,它们对 key 的 hash 算法和 hash 值到内存索引的映射算法不同。

虽然有诸多不同,但是他们的性能确实相差无几。由于 HashMap 使用广泛性,现以 HashMap 为例,阐述它的实现机理。

?

1、HashMap 的实现原理

HashMap 内部维护一个数组,并且将 key 做 hash 算法,然后将 hash 值映射到内存地址,即数组的下标索引,这样就可以通过key直接取到所对应的数据。而对于发生碰撞的位置,则会维护一个链表,所有在同一位置发生碰撞的元素都会存放在同一位置的链表中。

\

图 2

如图 2,数组中的每一个元素都是一个 Entry 实例:

?

	static class Entry
  
    implements Map.Entry
   
     { final K key; V value; Entry
    
      next; int hash; //.....省略部分 }
    
   
  

每一个实例都包含 元素key, 元素value , 元素hash值,以及指向下一个在当前位置发生冲突的 Entry实例。

?

2、Put 方法详细解析

下面我们来看一下最基本的put 操作。

?

	/*
	 * 将(key, value)放入 map
	 */
	public V put(K key, V value) {
		if (key == null)
			return putForNullKey(value);
		// 计算 key 对应的下标 。关于 hash 和 indexFor 方法,我们会在后面讲到。
		int hash = hash(key);
		int i = indexFor(hash, table.length);
		// 如果发生了冲突,那么就遍历当前冲突位置的链表。如果在链表中发现该元素已经存在(即两元素的 key 和 hash
		// 值一样),则用新值替换原来的值,并返回原来的值。
		for (Entry
  
    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;
				// 将该元素的访问存入历史记录中(在LinkedHashMap才发挥作用)
				e.recordAccess(this);
				return oldValue;
			}
		}
		// 标志容器被修改次数的计数器,在使用迭代器遍历时起作用
		modCount++;
		// 为新值创建一个新元素,并添加到数组中
		addEntry(hash, key, value, i);
		return null;
	}

	void addEntry(int hash, K key, V value, int bucketIndex) {
		// 如果数组需要扩容,则进行扩容
		if ((size >= threshold) && (null != table[bucketIndex])) {
			resize(2 * table.length);
			hash = (null != key) ? hash(key) : 0;
			bucketIndex = indexFor(hash, table.length);
		}
		// 创建新元素并添加到数组中
		createEntry(hash, key, value, bucketIndex);
	}

	/*
	 * 创建新元素,并将该新元素加到下标位置的最前端,该新元素的next引用指向该位置原来的元素(如果有)
	 */
	void createEntry(int hash, K key, V value, int bucketIndex) {
		Entry
   
     e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
   
  

应经加上了详细的注释,相信大家都能读得懂。同样的,get 操作就比这个简单多了,笔者就不再?嗦了,下面直接将最关键的部分。

?

3、HashMap 的核心算法-hash 函数的实现

HashMap的高性能需要保证以下几点:

1、hash 算法必须是高效的
2、hash 值到内存地址(数组索引)的算法是快速的
3、根据内存地址(数组索引)可以直接取得对应的值

首先来看第一点,hash 算法的高效性,在 HashMap 中,put() 方法和 hash 算法有关代码如下:

?

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        //...........省略部分
    }

?

?

final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();

        // 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);
    }
HashMap的功能是通过“键(key)”能够快速的找到“值”。下面我们分析下HashMap计算下标索引的思路:

1、 当调用put(key,value)时,首先获取key的hashcode,int hash = key.hashCode();
2、 再把hash通过一下运算得到一个int h。

hash ^= (hash >>> 20) ^ (hash >>> 12);
int h = hash ^ (hash >>> 7) ^ (hash >>> 4);

为什么要经过这样的运算呢?这就是HashMap的高明之处。先看个例子,一个十进制数32768(二进制

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇HDU-4031-Attack(树状数组) 下一篇C++ primer例子分析 C++11语法使..

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容:

·「链表」是一种怎样 (2025-12-25 19:20:51)
·C 语言中的链表有哪 (2025-12-25 19:20:48)
·c语言中的链表怎么学 (2025-12-25 19:20:45)
·Redis 分布式锁全解 (2025-12-25 17:19:51)
·SpringBoot 整合 Red (2025-12-25 17:19:48)