hashcode的前世今生

学习数据结构的时候

---当然,我们现在讨论的就是数据结构

我们知道

数组比链表更快定位,因为通过index算偏移直接定位到目标地址

 

现在的集合, Hashtable, HashMap

可以看成就是一个大链表

那么怎么克服链表的定位弱点

比如我要通过一个key对象,找到对应的值

很直观的一个想法就是

我需要一个一个比较

轮询比较

其实轮询没有问题

比较一下是否相等嘛

不过理解相等可能会有偏差

什么是相等?

是在内存里指向的就是同一个?

还是逻辑意义上相等,比如,虽然不是同一个内存块,但是他们名字一样? 

这个时候就需要重写equals

但是重写equals只是解决了“相等”在不同层次上的理解

对于数据结构来说毫无意义

我还是要一个一个比较

一个一个对象的比较

在链表的道路上越走越远

hash是把我们拉回来的工具

hash把我们从链表拉到数组上面来

拉到索引上面来

怎么拉回来呢

对象是链表的工作方式

索引是数组的工作方式

所以

我们需要对象到索引的一个规则,一个映射

---这是我们谈论的重点

 

先来了解下什么是hash哈希算法

哈希又叫散列

就像名字显示的

散开排列

散列的意义就在于,我把你散在各个地方,

更深层次的意义,是为了更快的从各个地方把你再找出来

因为是我撒的

所以我知道怎么把你找回来

 

从哪里找出来呢

从人群里找出来

所以,你看,hash就是伴随着集合出现的

 

以Hashtable为例

集合内部大概就是图片一这个样子的

x轴上有很多的,桶,或者叫bucket,

然后每个桶里面可以像栈一样往上堆东西,

---当然你也可以想象成风筝,比如蜈蚣风筝,

---桶就是蜈蚣,每个条目就是蜈蚣的腿,当然这是题外话

添加新项的时候就是先找到对应的桶

然后往桶里塞

获取条目的时候也是先找到对应的桶

然后找相等的条目

很简单的

 

图片一:

 

然后通过代码来看怎么操作的

下面是Hashtable添加新项的代码,

代码一:

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
        V old = e.value;
        e.value = value;
        return old;
        }
    }

    modCount++;
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

            tab = table;
            index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    Entry<K,V> e = tab[index];
    tab[index] = new Entry<K,V>(hash, key, value, e);
    count++;
    return null;
    }

代码很清晰,比照图片一

1,首先通过key的hashcode找到index,这是找到对应桶的index

2,然后在这个点的y轴上,也就是桶里面,通过equals找key相等的对象,找到就更新,找不到就在桶里塞上一个

 

再看Hashtable获取条目的代码

代码二:

    public synchronized V get(Object key) {
    Entry tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
        return e.value;
        }
    }
    return null;
    }

对比一下代码一,

结构很清晰,也是先通过把hashcode找到index,然后在Entry里轮询 

 

从集合的桶结构

---桶结构,多么形象的说法

从集合的桶结构

我们看到

若两个对象相等,hashCode必然相等

若hashCode相等,对象不一定相等,他们可能只是在同一个桶里

 

整个下来,我们可以看到有两个重要的映射规则

 * 映射一,把对象映射到hashcode值,这是hashcode函数存在的意义,也是构造的核心,稍后再说

 * 映射二,把hashcode值映射到索引,这就是上面代码定义的:

  int index = (hash & 0x7FFFFFFF) % tab.length;

 

hashcode就是把对象变为索引的那个点

hashcode把链表变为了数组

那为什么还需要equals呢

因为

没有一种规则把对象唯一映射到一个索引或者一个数值

我们只能把对象归类

把大类别划分为小类别

然后把轮询放到小类别中进行

这是我们能够做到的

所以

hashcode让我们快速定位到桶里来

equals才能给我们准确的结果

缺一不可

 

现在回到上面的话题

说说,怎么把对象映射到hashcode值,也就是hashcode函数的一些为什么

 

直观上来说

我们在散列的时候,

要尽量把东西均匀散在不同的桶里面

为什么呢

考虑极端情况

假如不均匀

那么我把所有的都撒在一个桶里,其余桶里都为空

那么

这和有没有桶有什么区别?

没有了嘛

均匀

就是和谐

均匀

只能追求而不能强求

 

看看String类是怎么追求均匀的

代码三:

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

---这里还能看到String把hashCode缓存到了hash变量里,这样就不需要每次都计算

相信大家了解过hashCode的都知道

上面的代码其实是字符串在31进制上的映射

对应成数学式就是

a*31^n+b*31^(n-1)+...+c

 

其实还是毫无头绪

那么我们从实际使用来说

int index = (hash & 0x7FFFFFFF) % tab.length;

这里有三个值hash,  0x7FFFFFFF, tab.length

0x7FFFFFFF 忽略,作用是把hash规范到四字节内

所以看成这样个子的

int index = hash % tab.length;

这个意思就是条目的hash值对集合长度求模,得到的就是桶的索引

所以

hash值可以独立于集合设计

需要考虑的就是,

hash值对于某一个数求模后尽量唯一随机不重

 

tab.length是集合的长度,看代码

protected void rehash() {
    int oldCapacity = table.length;
    Entry[] oldMap = table;

    int newCapacity = oldCapacity * 2 + 1;
    ......
    }

这里的newCapacity就是集合的新长度,是个奇数

 

然后来看看集合的默认长度

public Hashtable() {
    this(11, 0.75f);
    }

11就是集合的默认长度,也是个奇数

 

从初始默认和新长度我们可以了解到Hashtable的长度最好设定为奇数

---这是支线任务带来的奖励

 

现在回到hashCode

假定这就是hashCode的生成规则:

a*31^n+b*31^(n-1)+...+c

---这是求余最好的方式

那么只剩下一个问题

为什么系数是31

 

假定集合是abc

假定集合长度是11

假定映射二的规则就是求余

那么求索引就是

 

(a*31^2+b*31+c) % 11

 

系数为什么是31呢,为什么不是22

因为如果是22

22 = 11 * 2

所以

(a*22^2+b*22+c) % 2 == c % 2

(a*22^2+b*22+c) % 11 == c % 11

(a*22^2+b*22+c) % 22 == c % 22

就是说

只要c等于22,

不管ab为何

abc都会落到同一个桶里

所以这里系数不选择22

是为了避免当集合的长度(2, 11, 22)与系数(31,22)有某种关系(比如是约数)的时候,造成某个桶过于庞大

过于庞大的后果就是失去均匀

失去均匀就是失去hashCode存在的意义

就算不失去

也是极大削弱

所以这里的系数,最好是个质数,31就是个质数

 

质数有很多,为何是31

因为编译器会把乘31做一个优化

a*31 = a<<5 -1

位移总是最快的

所以31是个编译器做过优化的质数

 

其实这种优化成位移的质数不少

为何单单是31呢?

31是证明比较好使的

如果太大,hashCode值可能溢出,溢出虽然不是问题,但是总溢出就控制不了落点

如果太小,造成产生的hashCode过于密集,不利于分散,散列散列,当然散一点比较好

所以

选择31

不是唯一的选择

只是某种选择

 

最后来看看

hashCode把一个元素快速从集合里区分出来

从一种意义上说

就是快速和别的元素做一个比较

元素和元素的关系

比较总是最基本的一种

人都是爱攀比的

比来比去的

所以

虽然hashCode从集合产生

我们也可以用在任何比较的地方

---当然,这还是支线任务,主线是他的来龙去脉

 

 

posted on 2014-04-30 14:20  tirestay  阅读(219)  评论(0)    收藏  举报

导航