之所以写HashCode,是因为平时我们总听到它。但你真的了解hashcode吗?它会在哪里使用?它应该怎样写?

相信阅读完本文,能让你看到不一样的hashcode。

使用hashcode的目的在于:使用一个对象查找另一个对象。对于使用散列的数据结构,如 HashSet、HashMap、LinkedHashSet、LinkedHashMap ,如果没有很好的覆写键的hashcode()和equals()方法,那么将无法正确的处理键。

请对以下代码中 Person 覆写hashcode()方法,看看会发生什么?

// 覆写hashcode
@Override
public int hashCode() {
return age;
} @Test
public void testHashCode() {
Set<Person> people = new HashSet<Person>();
Person person = null;
for (int i = 0; i < 3 ; i++) {
person = new Person("name-" + i, i);
people.add(person);
}
person.age = 100;
System.out.println(people.contains(person));
people.add(person);
System.out.println(people.size());
}

运行结果并不是预期的 true 和 3 ,而是 false 和 4 !改变 person.age 后HashSet无法找到 person 这个对象了,可见覆写hahcode对HashSet的存储和查询造成了影响。

那么hashcode是如何影响HashSet的存储和查询呢?又会造成怎样的影响呢?

HashSet的内部使用HashMap实现,所有放入HashSet中的集合元素都会转为HashMap的key来保存。HashMap使用散列表来存储,也就是数组+链表+红黑树(JDK1.8增加了红黑树部分)。

存储结构简图如下:

HashMap存储结构简图

数组的默认长度为16,数组里每个元素存储的是一个链表的头结点。组成链表的结点结构如下:

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
}

每一个Node都保存了一个hash----键对象的hashcode,如果键没有按照任何特定顺序保存,查找时通过equals()逐一与每一个数组元素进行比较,那么时间复杂度为O(n),数组长度越大,效率越低。

所以瓶颈在于键的查询速度,如何通过键来快速的定位到存储位置呢?

HashMap将键的hash值与数组下标建立映射,通过键对象的hash函数生成一个值,以此作为数组的下标,这样我们就可以通过键来快速的定位到存储位置了。如果hash函数设计的完美的话,数组的每个位置只有较少的值,那么在O(1)的时间我们就可以找到需要的元素,从而不需要去遍历链表。这样就大大提高了查询速度。

那么HashMap根据hashcode是如何得到数组下标呢?可以拆分为以下几步:

  • 第一步: h = key.hashCode()
  • 第二步: h ^ (h >>> 16)
  • 第三步: (length - 1) & hash

分析

第一步是得到key的hashcode值;

第二步是将键的hashcode的高16位异或低16位(高位运算),这样即使数组table的length比较小的时候,也能保证高低Bit都参与到Hash的计算中,同时不会有太大的开销;

第三步是hash值和数组长度进行取模运算,这样元素的分布相对来说比较均匀。当length总是2的n次方时, h & (length-1) 运算等价于对length取模,这样模运算转化为位移运算速度更快。

但是,HashMap默认数组初始化容量大小为16。当数组长度远小于键的数量时,不同的键可能会产生相同的数组下标,也就是发生了哈希冲突!

对于哈希冲突有开放定址法、链地址法、公共溢出区法等解决方案。

开放定址法就是一旦发生冲突,就寻找下一个空的散列地址。过程可用下式描述:

f i (key) = (f(key) + d i ) mod m (d i =1,2,3,...,m-1)

例如键集合为 {12,67,56,16,25,37,22,29,15,47,48,34} ,表长 n = 12 ,取 f(key) = key mod 12 。

前5个计算都没有冲突,直接存入。如表所示

数组下标
0 12
1 25
2  
3  
4 16
5  
6  
7 67
8 56
9  
10  
11  

当 key = 37 时, f(37) = 1 ,与25的位置冲突。应用公式 f(37) = (f(37) + 1) mod 12 = 2,所以37存入数组下标为2的位置。如表所示

数组下标
0 12
1 25
2 37
3  
4 16
5  
6  
7 67
8 56
9  
10  
11  

到了 key = 48 ,与12所在的0冲突了。继续往下找,发现一直到 f(48) = (f(48) + 6) mod 12 = 6 时才有空位。如表所示

数组下标
0 12
1 25
2 37
3  
4 16
5 29
6 48
7 67
8 56
9  
10 22
11 47

所以在解决冲突的时候还会出现48和37冲突的情况,也就是出现了 堆积 ,无论是查找还是存入效率大大降低。

链地址法解决冲突的做法是:如果哈希表空间为 [0~m-1] ,设置一个由m个指针分量组成的一维数组 Array[m] , 凡哈希地址为i的数据元素都插入到头指针为 Array[i] 的链表中。

它的基本思想是:为每个Hash值建立一个单链表,当发生冲突时,将记录插入到链表中。如图所示:

链地址法

链表的好处表现在:

  1. remove操作时效率高,只维护指针的变化即可,无需进行移位操作
  2. 重新散列时,原来散落在同一个槽中的元素可能会被散落在不同的地方,对于数组需要进行移位操作,而链表只需维护指针。 
    但是,这也带来了需要遍历单链表的性能损耗。

公共溢出法就是我们为所有冲突的键单独放一个公共的溢出区存放。

例如前面例子中 {37,48,34} 有冲突,将他们存入溢出表。如图所示。

公共溢出法

在查找时,先与基本表进行比对,如果相等则查找成功,如果不等则在溢出表中进行顺序查找。公共溢出法适用于冲突数据很少的情况。

HashMap解决冲突采取的是链地址法。整体流程图(暂不考虑扩容)如下:

HashMap存储流程简图

理解了hashcode和哈希冲突即解决方案后,我们如何设计自己的hashcode()

方法呢?

Effective Java一书中对覆写hashcode()给出以下指导:

  • 给int变量result赋予某个非零常量值

  • 为对象内每个有意义的域f计算一个int散列码c

域类型 计算
boolean c = (f ? 0 : 1)
byte、char、short、int c = (int)f
long c = (int)(f ^ (f >>> 32))
float c = Float.floatToIntBits(f)
double long l = Double.doubleToIntLongBits(f)
  c = (int)(l ^ (l >>> 32))
Object c = f.hashcode()
数组 每个元素应用上述规则
boolean c = (f ? 0 : 1)
boolean c = (f ? 0 : 1)
  • 合并计算得到散列码 result = 37 * result + c

现代IDE通过点击右键上下文菜单可以自动生成hashcode方法,比如通过IDEA生成的hashcode如下:

@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}

但是在企业级代码中,最好使用第三方库如 Apache commons 来生成hashocde方法。使用第三方库的优势是可以反复验证尝试代码。下面代码显示了如何使用 Apache Commons hash code 为一个自定义类构建生成hashcode。

public int hashCode(){
HashCodeBuilder builder = new HashCodeBuilder();
builder.append(mostSignificantMemberVariable);
........................
builder.append(leastSignificantMemberVariable);
return builder.toHashCode();
}

如代码所示,最重要的签名成员变量应该首先传递然后跟随的是没那么重要的成员变量。

总结

通过上述分析,我们设计hashcode()应该注意的是:

  • 无论何时,对同一个对象调用hashcode()都应该生成同样的值。
  • hashcode()尽量使用对象内有意义的识别信息。
  • 好的hashcode()应该产生分布均匀的散列值。
  • java学习群669823128

你所不知道的 Java 之 HashCode的更多相关文章

  1. 【总结】你所不知道的Java序列化

    我们都知道,Java序列化可以让我们记录下运行时的对象状态(对象实例域的值),也就是我们经常说的对象持久化 .这个过程其实是非常复杂的,这里我们就好好理解一下Java的对象序列化. 1. 首先我们要搞 ...

  2. 你所不知道的java编程思想

    读thinking in java这本书的时候,有这么一句话“在编译单元的内部,可以有一个公共(public)类,它必须拥有与文件相同的名字” 有以下疑问: 在一个类中说可以有一个public类,那是 ...

  3. 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制

    你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...

  4. 你所不知道的五件事情--java.util.concurrent(第二部分)

    这是Ted Neward在IBM developerWorks中5 things系列文章中的一篇,仍然讲述了关于Java并发集合API的一些应用窍门,值得大家学习.(2010.06.17最后更新) 摘 ...

  5. Android中Context详解 ---- 你所不知道的Context

    转自:http://blog.csdn.net/qinjuning/article/details/7310620Android中Context详解 ---- 你所不知道的Context 大家好,  ...

  6. 你所不知道的 URL

    0.说明 第一幕 产品:大叔有用户反映账户不能绑定公众号.大叔:啊咧咧?怎么可能,我看看?大叔:恩?这也没问题啊,魏虾米.大叔:还是没问题啊,挖叉类.大叔:T T,话说产品姐姐是不是Java提供接口的 ...

  7. 你所不知道的C++

    C++与C的不同 C++从诞生之初就号称和C是兼容的,正是这种兼容,使C++得以迅猛发展,然而也正是这种兼容,让C++背上了沉重的历史包袱.且不论其利弊,让我们来看看C++在兼容C的那部分中,与C语言 ...

  8. Android Context完全解析,你所不知道的Context的各种细节

    Context相信所有的Android开发人员基本上每天都在接触,因为它太常见了.但是这并不代表Context没有什么东西好讲的,实际上Context有太多小的细节并不被大家所关注,那么今天我们就来学 ...

  9. Android中Context详解 ---- 你所不知道的Context(转)

    Android中Context详解 ---- 你所不知道的Context(转)                                               本文出处 :http://b ...

随机推荐

  1. 199. Binary Tree Right Side View -----层序遍历

    Given a binary tree, imagine yourself standing on the right side of it, return the values of the nod ...

  2. sql 语句 名称解析,是 由内向外的。

    子查询内  找不到的 字段 会 向外 寻找,还是找不到 就报错:找到了就不报错,但是 子查询语句就毫无意义了: 解决办法:  字段前面要跟上表的名称.  一般 字段无效 立刻 报错.

  3. Django---自定义admin组件思维导图

  4. Apache 虚拟主机配置

    开放虚拟主机文件 修改主配置文件 解开注释,使用虚拟主机配置文件. vim /usr/local/apache2/conf/httpd.conf Include conf/extra/httpd-vh ...

  5. XAMPP安装指南

    首先下载一个安装包 按照默认选项,依次安装: 去掉不必要的选项: 选择安装路径: 显示下图说明已经成功安装完成了. 打开XAMPP,启动Apache服务: 如果显示Apache服务无法启动,有如下错误 ...

  6. 详解Linux系统下PXE服务器的部署过程

    在大规模安装服务器时,需要批量自动化方法来安装服务器,来减少日常的工作量. 但是批量自动化安装服务器的基础是网络启动服务器(bootserver). 下面我们就介绍一下 网络启动服务器的 安装和配置方 ...

  7. Python 对象学习一

    # 对象的基本理论 # 什么是对象? # 万物皆对象 # 对象是具体物体 # 拥有属性 # 拥有行为 # 把很多零散的东西,封装成为一个整体 # 举例:王二小 # 属性 # 姓名 # 年龄 # 身高 ...

  8. Spring容器创建过程

    Spring容器的refresh()   创建刷新 1  prepareRefresh() 刷新前的预处理 1) initProPertySources() 初始化一些属性设置: 子类定义个性化的属性 ...

  9. Effective C++ 条款11:在operator=中处理"自我赋值"

    "自我赋值"发生在对象被赋值给自己时: class Widget { ... }; Widget w; ... w = w; // 赋值给自己 a[i] = a[j]; // 潜在 ...

  10. OCR训练数据生成方法

    有的时候我们训练网络的时候,数据集在收集的过程中由于种种原因导致图像收集的不完整,比如某些种类很少,或者没有,这个时候我们就可以考虑自己生成数据集. 这个和data augmentation还不太一样 ...