1、Hash值有什么用?
HashMap、HashTable、HashSet,所以涉及到使用Hash值进行优化存储的地方,都会用到HashCode。HashCode是Key,这种计算为提高计算的性能。想想看,一般来说,数组算是比较快的集合类了吧,直接用index定位元素,简直就是O(1)的级别。但是添加元素就不这么乐观了。但是使用hash类的集合,添加元素,移动的元素少,只影响一小块,并且查找元素,由于hash值已经进行了定位分组,所以也会大大缩小涉及面,快速定位。
2、Hash值应该符合什么原则?
A、等幂性。不管执行多少次获取Hash值的操作,只要对象不变,那么Hash值是固定的。如果第一次取跟第N次取不一样,那就用起来很麻烦,需要记录当前是第几次操作,这种需要记录状态的事情,可不是什么好事。
B、对等性。若两个对象equal方法返回为true,则其hash值也应该是一样的。举例说明:若你将objA作为key存入HashMap中,然后new了一个objB。在你看来objB和objA是一个东西(因为他们equal),但是使用objB到hashMap中却取不出来东西。
C、互异性。若两个对象equal方法返回为false,则其hash值最好也是不同的,但这个不是必须的,只是这样做会提高hash类操作的性能(碰撞几率低)。
3、Hash值应该怎么计算?
A、简单计算就是组成成员的hash值直接相加即可。比如ObjectA有三个属性,propA、propB和propC,最直接的计算方式就是propA.hashcode+propB.hashcode+propC.hashcode。
B、但是如果遇到有顺序相关的怎么办?比如String类型是由char数组组成,并且这些数组是有顺序的。如果使用第一种计算方法,则“ABCD”和“BCDA”就会产生同样的hashCode,那么怎么办呢?最直接想到的办法就是加权,不同的index加不同的权值,这个权值的确定最直接的方法就是某个常数值的几次幂。比如为String的计算hash值为K^0*A.hashCode+K^1*B.hashCode+K^2*C.hashCode+K^3*D.hashCode。K的选择也有说法,最好不要是偶数,因为偶数的相乘会造成信息的丢失(乘以2就是左移1位,一旦溢出就会造成信息的丢失,这种计算会造成溢出后的值与某个看似不相关的数值得到的结果是一样的),所以最好是奇数,在这一点上比较推荐使用7,因为7=8-1=2^3-1,这样计算的时候,直接左移几位再进行一次普通的加减法即可(Java中常用的是31(32-1=2^5-1))。
=================================
DK6的源码:
-
/**
-
* 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) {
-
int off = offset;
-
char val[] = value;
-
int len = count;
-
for (int i = 0; i < len; i++) {
-
h = 31*h + val[off++];
-
}
-
hash = h;
-
}
-
return h;
-
}
以字符串"123"为例:
字符'1'的ascii码是49
hashCode = (49*31 + 50)*31 + 51
或者这样看:
hashCode=('1' * 31 + '2' ) * 31 + '3'
可见实际可以看作是一种权重的算法,在前面的字符的权重大。
这样有个明显的好处,就是前缀相同的字符串的hash值都落在邻近的区间。
好处有两点:
1.可以节省内存,因为hash值在相邻,这样hash的数组可以比较小。比如当用HashMap,以String为key时。
2.hash值相邻,如果存放在容器,比好HashSet,HashMap中时,实际存放的内存的位置也相邻,则存取的效率也高。(程序局部性原理)
以31为倍数,原因了31的二进制全是1,则可以有效地离散数据。
最后看下,两个字符串,由Eclipse生成的代码是如何计算hash值的:
-
public class Name{
-
String firstName;
-
String lastName;
-
@Override
-
public int hashCode() {
-
final int prime = 31;
-
int result = 1;
-
result = prime * result
-
+ ((firstName == null) ? 0 : firstName.hashCode());
-
result = prime * result
-
+ ((lastName == null) ? 0 : lastName.hashCode());
-
return result;
-
}
-
@Override
-
public boolean equals(Object obj) {
-
if (this == obj)
-
return true;
-
if (obj == null)
-
return false;
-
if (getClass() != obj.getClass())
-
return false;
-
Name other = (Name) obj;
-
if (firstName == null) {
-
if (other.firstName != null)
-
return false;
-
} else if (!firstName.equals(other.firstName))
-
return false;
-
if (lastName == null) {
-
if (other.lastName != null)
-
return false;
-
} else if (!lastName.equals(other.lastName))
-
return false;
-
return true;
-
}
-
}
可见,还是以31为倍数, hashCode = firstName.hashCode() * 31 + lastName.hashCode() 。
BTW:Java的字符串的hash做了缓存,第一次才会真正算,以后都是取缓存值。
eclipse生成的equals函数质量也很高,各种情况都考虑到了。
总结:字符串hash函数,不仅要减少冲突,而且要注意相同前缀的字符串生成的hash值要相邻