综上,HashMap<K,V>就是一个用来存储<键、值>数据对的机制,其中键key“映射”到保存值(value)的存储地址,映射过程使用了哈希函数。也就是键(key)通过哈希函数运算后能够获得值(value)的地址。
对上面电话号码簿的例子,电话号码簿体现为HashMap<K,V>的一个实例,键key为手机号,值(value)也为手机号。键(手机号)通过哈希函数运算(取手机号后4位)后能够获得值(手机号)的地址。
这个问题与前2个问题的区别是,要查询的数据不是单个数字,这就很难利用前2个示例中的方法构建一个易于查询的花名册。可是能够试想,假如能够经过某种运算将名字变成一个0到10000之间的一个数字,并且名字不一样时,产生的数字不一样,那么就能够利用上述的方法构建一个易于查询的花名册。
4 哈希函数(Hash Function)的定义
上例中某种运算(将名字变成一个0到10000之间的一个数字)就能够被称做是哈希函数。
哈希函数更专业的定义是:哈希函数是任意一种算法,它能够将任意长度的原数据映射为固定长度的结果数据。
由于哈希函数一般将可变长度的原数据,“切碎(hash)”成固定长度数据,对各部分处理后造成一个固定长度的数据,因此被形象的称为哈希函数。
号码簿问题中,取电话号码中的后4位这个运算,就是将一个长数据映射为了一个短数据,因此也能够称为哈希函数。
因为产生的数据长度固定,因此结果数据就能够用来做为数组的索引值,在相应位置保存原数据,就能够加快查询。
- 从十进制角度看,若是产生的数据在0-10000之间,也就是4位十进制数时,就能够建立一个10000个数据的数组,用哈希函数的结果作为索引值。
- 从二进制角度看,若是产生的数据在0-0x7F之间,也就是8位二进制数时,就能够建立一个128个数据的数组,用哈希函数的结果作为索引值。
5 如何设计合适的哈希函数
能够想象为了减小冲突,加快查询,不一样原数据通过哈希运算后产生的数值应该最大可能的不一样。因此一个优秀的哈希函数必然具备这样的性质。
注意:如下内容的叙述从数学理论的角度并不彻底严密与准确,且缺乏证实。更严谨的学习应该查看相关著做或者参加专门课程。
质数与求模运算正好具备这样的性质:
假若有一个质数Z,其远大于数S,那么对于运算:
( n * Z ) % S
其中n表明从1到无穷的任意整数,*为乘法运算,%为求模运算
对应任意n,运算的结果均匀的分布在0到S之间。
好比对于质数211和数8:
(1*211) % 8 = 3 (67*211) % 8 = 1
(2*211) % 8 = 6 (68*211) % 8 = 4
(3*211) % 8 = 1 (69*211) % 8 = 7
(4*211) % 8 = 4 (70*211) % 8 = 2
(5*211) % 8 = 7 (71*211) % 8 = 5
(6*211) % 8 = 2 (72*211) % 8 = 0
(7*211) % 8 = 5 (73*211) % 8 = 3
(8*211) % 8 = 0 (74*211) % 8 = 6
因此对于上面花名册的例子,若是能够将名字通过哈希运算获得0到10000之间的数值,就能够实现快速查询。因为字符在电脑中一般用Unicode代码表示,查出名字的Unicode代码,“张”的Unicode十进制代码为24352,“三”的Unicode十进制代码为19977,选取质数9656717,进行如下运算:((24352 + 19977) * 9656717) % 10000 = 5168。这样就获得了0到10000之间的数值,参照以前的例子,就能够构造一个数组来加快查询。
Unicode代码查询网址:
http://www.unicode.org/charts/unihan.html
质数表,Table of Primes from 1 to 1 000 000 000 000:
http://www.walter-fendt.de/m14e/primes.htm
5.1 java.lang.String类中字符串的哈希函数
在Oracle公司的Java API实现中,String类的hashcode()函数计算了字符串的哈希值,源代码以下。从注释和程序中能够看出,计算公式为hashall = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],是将字符串各个字符的UTF-16代码乘以31的ni次方后相加获得的。31为质数,31^(ni)虽然不是质数,可是性质接近质数。但并无发现显式的求模运算%,这是由int类型数据算术运算后获得的,若是值超过了int类型的最大值时,高位被自动抛弃,这就至关于对2147483648(十六进制0x7FFF)求模,因此结果在0到2147483648之间。
/**
* Returns a hash code for this string. The hash code for a
* <code>String</code> object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using <code>int</code> arithmetic, where <code>s[i]</code> is the
* <i>i</i>th character of the string, <code>n</code> is the length of
* the string, and <code>^</code> indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
6 Java API中的HashMap类实现简介
6.1 HashMap类中哈希值的计算方法
经过查看其源代码,能够看出HashMap类中哈希值的计算方法。
类中的哈希值是经过final int hash(Object k)函数实现的,首先根据键(key)对象的hashcode函数计算键对象的hash值:k.hashCode(),而后内部再进行相应的移位和求异或运算,获得内部使用的hash值。能够看出hash值由int类型表示,则其值在0到Interger.MAX_VALUE之间。但实际内部存储用的数组长度由HashMap的容量决定,因此根据hash值获得对象在数组中的索引值,还须要近一步计算,下段中进行了说明。
/**
* Retrieve object hash code and applies a supplemental hash function to the
* result hash, which defends against poor quality hash functions. This is
* critical because HashMap uses power-of-two length hash tables, that
* otherwise encounter collisions for hashCodes that do not differ
* in lower bits. Note: Null keys always map to hash 0, thus index 0.
*/
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();//计算键对象的hash值,以后与0求异或运算
// 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值更分散下降冲突可能
}
6.2 根据键(key)对象查询值(value)对象
根据键对象查询<K,V>对象的方法,涉及到的源代码以下。
首先public V get(Object key)函数中,调用getEntry(key)函数,由键对象得到相应值的Entry<K,V>的地址entry。
从Entry<K,V>源代码(这里没有粘贴过来)能够看出,Entry<K,V>是类中定义的新类,继承至Map.Entry<K,V>。该对象中保存了键(key)对象和相应的值(value)对象,并包含有指向下个Entry<K,V>地址的变量,这样能够实现链表功能,用于解决冲突。若是冲突产生时(不一样键对象的hash值相同),将hash值相同的对象其依次放在此链表中。
getEntry(key)函数中首先由hash(key)计算键对象的hash值。
而后由indexFor(hash, table.length)函数根据hash值得到Entry<K,V>[]数组的索引值,该函数中h & (length-1)运算将hash值由原来的0到Interger.MAX_VALUE之间映射到0到(length-1)之间,这样就能够看成该数组的索引值。
而后Entry<K,V> e = table[indexFor(hash, table.length)]根据索引值,将须要的数据找到。
table是Entry<K,V>[]类型的数组,其中保存了指向相应Entry<K,V>的地址。
for程序段中,若是有冲突,则依次遍历此链表,找到与指定键对象对应的值对象。将Entry<K,V>对象返回get(Object key)函数。
最后get(Object key)函数调用entry.getValue()得到相应的值对象。
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
/**
* Returns the entry associated with the specified key in the
* HashMap. Returns null if the HashMap contains no mapping
* for the key.
*/
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
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 != null && key.equals(k))))
return e;
}
return null;
}
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
6.3 HashMap类的容量
从以前的例子中,能够知道查询速度的改进是因为用空间换取了时间,因此HashMap类的容量越大,效率越高,可是空间占用约多。
通过权衡,类中定义了填充率(loadFactor),默认为0.75;容量(capacity),默认值为16。源代码以下:
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
类始终保持类中保存的数据量小于门限(threshold) = 容量(capacity)* 填充率(loadFactor)。每次添加的新的数据时,都检测数据量(size)是否超过门限(threshold)。若是超限则调用resize(2 * table.length)函数,将类的容量增大。源代码以下:
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
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);
}
6.4 调整HashMap类的容量对性能的影响
调整HashMap类的容量的函数resize(int newCapacity)源代码以下,从新调整大小,须要新建一个Entry[]数组,而后调用transfer(newTable, rehash)函数将以前数组中的值调整到新数组中。
transfer(newTable, rehash)函数中调用hash(e.key)函数从新计算了键对象的哈希值,根据哈希值将旧Entry[]数组中数据放到新Entry[]数组中。
因此调整HashMap类的容量形成了如下影响:
- 新建一个Entry[]数组,须要格外的空间
- 从新计算了键对象的哈希值,须要格外的运行时间
- 因为Entry[]数组长度变化,各元素在HashMap中的内部位置发生了改变
综上,要根据时间状况,设计HashMap类的容量和填充率,尽少调整容量的次数。
/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
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];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
6.5 最后一个例子,电话簿PhoneBook
以前示例中的电话簿中没有人名,这里添加人名。PhoneBook 扩展了HashMap类。这样能够直接使用其函数。电话簿中的每条内容由<String 人名, String 号码>组成。将人名做为键,号码做为值,因此能够根据人名得到他/她的电话号码。
因为使用了字符串做为键,因此能够利用其已经实现的hashcode()函数实现hash值的计算。
因为HashMap要求键值各不相同,因此此电话簿,不能有重名,还须要进一步改进。
import java.util.HashMap;
// PhoneBook 扩展了HashMap类。这样能够直接使用其函数。
// 电话簿中的每条内容由<String 人名, String 号码>组成。
// 将人名做为键,号码做为值,因此能够根据人名得到他/她的电话号码
public class PhoneBook extends HashMap<String,String> {
PhoneBook(){
super();
}
//测试
public static void main(String[] args) {
PhoneBook pb = new PhoneBook();
String[][] intial = new String[][]{
{ "张三","286 3545 1285" },
{ "李四","250 4592 8502" },
{ "王五","239 2085 1032" },
{ "赵六","230 1932 0543" },
{ "王二麻子","259 1937 1408" },
{ "段誉","251 8592 1459" },
{ "王语嫣","252 2309 7934" },
{ "虚竹","249 2942 9285" },
{ "梦姑","289 0103 8482" },
{ "乔峰","279 0094 1342" }
};
//将电话保存在电话簿中
for(int i = 0; i < intial.length; i++) {
pb.put(intial[i][0], intial[i][1]);
}
//测试
System.out.println("电话簿中共保存了" + pb.size() + "个电话号码。" );
String name = new String("乔峰");
Boolean bl = pb.containsKey(name);//查询是否包含该人名
System.out.println("电话簿中" + ( bl ? "查到" : "未查到" ) + name
+ "的电话号码。"
+ ( bl ? ("电话号码是" + pb.get(name) + "。") : ""));
//测试
name = new String("王语嫣");
bl = pb.containsKey(name);
System.out.println("电话簿中" + ( bl ? "查到" : "未查到" ) + name
+ "的电话号码。"
+ ( bl ? ("电话号码是" + pb.get(name) + "。") : ""));
//测试
name = new String("星秀老仙");
bl = pb.containsKey(name);
System.out.println("电话簿中" + ( bl ? "查到" : "未查到" ) + name
+ "的电话号码。"
+ ( bl ? ("电话号码是" + pb.get(name) + "。") : ""));
}
运行程序后,根据输出能够看出电话簿正常工做:
电话簿中共保存了10个电话号码。
电话簿中查到乔峰的电话号码。电话号码是279 0094 1342。
电话簿中查到王语嫣的电话号码。电话号码是252 2309 7934。
电话簿中未查到星秀老仙的电话号码。
7 参考资料
[1] Hash function http://en.wikipedia.org/wiki/Hash_function [2] 麻省理工学院公开课:算法导论> 哈希表 http://v.163.com/movie/2010/12/R/E/M6UTT5U0I_M6V2TG4RE.html [3] Java官方API(Oracle Java SE7)源代码,下载安装JDK后,源代码位于安装根目录的src.zip文件中 http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html [4] OpenJDK源代码下载(包括了HotSpot虚拟机、各个系统下API的源代码,其中API源代码位于openjdk\jdk\src\share\classes文件夹下): https://jdk7.java.net/source.html