彻底明白equals和hashCode
equals和hashCode方法
equals
我们知道equals是用来比较两个对象是否相等的,比如我们常用的String.equals方法
@Test
public void test() {
String str1=new String("abc");
String str2=new String("abc");
boolean equals = str1.equals(str2);
System.out.println(equals);//true
}
hashCode方法
hashCode方法是通过一定的算法得到一个hash值,一般配合散列集合一起使用,如HashMap、HashSet都是不可以存放重复元素的,那么当容器中元素个数很多时,你要添加一个元素时,难道一个一个去equals比较?当然这是可以的,但是难免效率很低,而HashMap和HashSet的底层都是使用数组+链表的方式实现的,这样有什么好处呢,当一个对象要加入集合,直接用hashCode进行一些运算得到保存的数组下标,再去数组下标对应的链表中一个一个元素比较(equals),这样显然减少了比较次数,提高了效率
那Object的hashCode方法的默认实现是怎样的呢?
public native int hashCode();
可以看到它是一个本地方法,实际上Object的hashCode方法返回是元素的地址(不同的虚拟机可能不一样,但Hotspot的是)
class Emp{
String idCord;
String name;
int age;
public Emp(String idCord, String name, int age) {
super();
this.idCord = idCord;
this.name = name;
this.age = age;
}
}
@Test
public void test2() {
Emp e=new Emp("0101001","zhangsan",20);
System.out.println(e.hashCode());//1717159510
System.out.println(e);//com.moyuduo.test.Emp@6659c656
}
6659c656转换成十进制也就是1717159510
哈希集合的使用
我们很多时候这样使用HashMap
@Test
public void test3() {
HashMap<String,Emp> map=new HashMap<>();
map.put(new String("zhangsan"), new Emp("01001","zhangsan",20));
map.put(new String("lisi"), new Emp("01002","lisi",22));
map.put(new String("zhangsan"), new Emp("01003","zhangsan",23));
Emp emp = map.get("zhangsan");
System.out.println(emp);//Emp [idCord=01003, name=zhangsan, age=23]
}
额?不对呀,编号为01001的张三呢?而且我们知道new出来的String的hashCode是地址一定是不相同的,那么为什么后一个张三还是把前一个张三覆盖了呢?
是因为String重写了hashCode方法和equals方法
public int hashCode() {
//默认为0
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
//一个一个遍历String的char[]
for (int i = 0; i < value.length; i++) {
//hash值等于当前字符前字符的hash*31+当前字符的Unicode
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
public boolean equals(Object anObject) {
//判断两个对象的地址是否相同
if (this == anObject) {
return true;
}
//判断传入的对象是不是String类型
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
//判断两个String的char[]的长度是否一致
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
//一个一个字符进行比较
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
那么当我们使用自己的对象作为键时
@Test
public void test4() {
HashMap<Emp,Integer> map=new HashMap<>();
map.put(new Emp("01001","zhangsan",20),6000);
map.put(new Emp("01002","lisi",22),8000);
Integer integer = map.get(new Emp("01001","zhangsan",20));
System.out.println(integer);//null
}
可以看到输出的是null,这是为什么呢,就是因为我们自定义的类没有重新写hashCode方法,get的时候新new出来的Emp对象的hashCode(也就是地址)肯定和存的时候的hashCode不一样,所以拿不到,所以当我们自定义的类要使用散列集合存储时,一定要重写equals方法和hashCode方法
HashMap的底层原理
为什么当我们要使用自定义对象作为key存放在HashMap中时,一定要重写equals和hashCode呢?
我们去看看HashMap底层是怎么存键值对和得到值的
HashMap的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//计算键的hash
static final int hash(Object key) {
int h;
//如果键为null那么hash为0,这也是为什么HashMap只能存放一个键为null的元素,否则hash为hashCode与上hashCode无符号右移16位
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果当前的Node数组还未初始化或长度为0
if ((tab = table) == null || (n = tab.length) == 0)
//进行扩容
n = (tab = resize()).length;
//节点存放的下标是数组长度-1与上键的hash
if ((p = tab[i = (n - 1) & hash]) == null)
//运算得到的下标的位置没有存放元素,那么直接保存
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果下标位置元素的hash和键的hash相等并且下标元素的key和键的地址相同或equals那么直接覆盖
e = p;
else if (p instanceof TreeNode)
//如果下标元素位置存放的元素本来就是红黑色节点,那么按照红黑树的规则插入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //下标位置有元素而且还没转化为红黑树,说明是链表存储
for (int binCount = 0; ; ++binCount) {
//让e指向链表下一个节点
if ((e = p.next) == null) {//当找到最后e等于null了说明链表中没有元素的key和当前插入的key相同
//直接把节点挂到链表尾
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果找到已存入元素的key和插入key的hash相同并且两key地址相等或equals,那么e就是要替换的元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//替换旧值
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
//插入后元素大小超过阈值进行扩容
resize();
afterNodeInsertion(evict);
return null;
}
HashMap的get方法
public V get(Object key) {
Node<K,V> e;
//如果通过key拿到的键值对节点为null就返回null,否则返回节点的value
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//Node[]是否已经初始化并且长度>0并且通过hash运算得到的下标已经有元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//判断下标第一个位置节点的hash和查询key的hash一致并且两key地址一样或equals
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//下标节点还有next
if ((e = first.next) != null) {
//节点是红黑树,那么按照红黑树的查找规则进行
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//是链表,那么依次遍历
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
可以看到存的时候是通过hashCode得到hash再用hash得到存放下标,然后存入键值对
取的时候是通过hashCode得到hash再得到下标元素,下标元素再根据hash&&(地址相等||equals)得到键值对
Object规范
说了这些再来说说Object规范
- 两对象equals那么hashCode值一定要相同
- 两对象hashCode值相等,对象不一定equals,这主要是因为hashCode是根据对象的特征值生成的,hashCode的算法是程序员自己实现的,在某些情况下可能两对象在逻辑上也不同也能生成相同的hashCode
equals和hashCode联系
- 当我们自定义类不需要充当key来在散列表中存储对象时,equals和hashCode根本没有关系,你也没必要重写hashCode方法
- 当我们会用自定义类充当key在散列表中存对象,这时候你一定要重写equals和hashCode
彻底明白equals和hashCode的更多相关文章
- Java equals 和 hashCode 的这几个问题可以说明白吗?
前言 上一篇文章 如何妙用 Spring 数据绑定? ,灵魂追问 环节留下了一个有关 equals 和 hashcode 问题 .基础面试经常会碰到与之相关的问题,这不是一个复杂的问题,但很多朋友都苦 ...
- Java中的equals和hashCode方法
本文转载自:Java中的equals和hashCode方法详解 Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要 ...
- Java提高篇——equals()与hashCode()方法详解
java.lang.Object类中有两个非常重要的方法: 1 2 public boolean equals(Object obj) public int hashCode() Object类是类继 ...
- Java实战equals()与hashCode()
一.equals()方法详解 equals()方法在object类中定义如下: 代码 public boolean equals(Object obj) { return (this == obj); ...
- java集合(3)- Java中的equals和hashCode方法详解
参考:http://blog.csdn.net/jiangwei0910410003/article/details/22739953 Java中的equals方法和hashCode方法是Object ...
- java基础(十六)----- equals()与hashCode()方法详解 —— 面试必问
本文将详解 equals()与hashCode()方法 概述 java.lang.Object类中有两个非常重要的方法: public boolean equals(Object obj) publi ...
- Java中的equals和hashCode方法详解
Java中的equals和hashCode方法详解 转自 https://www.cnblogs.com/crazylqy/category/655181.html 参考:http://blog.c ...
- 为什么要重写equals和hashcode方法
equals hashcode 当新建一个java类时,需要重写equals和hashcode方法,大家都知道!但是,为什么要重写呢? 需要保证对象调用equals方法为true时,hashcode ...
- java学习-- equals和hashCode的关系
hashcode的目的就是在hashset或者hashmap等中比较两个对象相等时,减少equals的使用次数来提高效率 以下为摘录 java中hashcode和equals的区别和联系 HashSe ...
随机推荐
- 系统分析与设计lesson6
| 分类 作业 | 1.用例建模 a. 阅读 Asg_RH 文档,绘制用例图. 按 Task1 要求,请使用工具 UMLet,截图格式务必是 png 并控制尺寸 b. 选择你熟悉的定旅馆在线服务系统 ...
- Angular总结
angular关键核心点进行总结 1 2 angular中有很多知识点需要学习,学习成本是很大的,我通过平常开发中把一些 很重要知识点总结下来,不管是以后拿来用,或者跳槽面试需要,我都感觉是很有帮助的 ...
- objectarx 得到有宽度的多段的轮廓
使用到的命令是:_.wmfout和_.import以及PEdit步骤:1.先通过_.wmfout和_.import得到轮廓线,得到的轮廓线是一个块.方法如下: //ssname:选择的有宽度的多段线 ...
- Scheme实现数字电路仿真(3)——模块
版权申明:本文为博主窗户(Colin Cai)原创,欢迎转帖.如要转贴,必须注明原文网址 http://www.cnblogs.com/Colin-Cai/p/12242650.html 作者:窗户 ...
- C++走向远洋——54(项目一2、分数类的重载、取倒数)
*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...
- .NET Core 获取主机运行资源的库
简介 CZGL.SystemInfo 是一个支持 Windows 和 Linux 的资源信息获取库,用于获取系统环境.机器资源信息.系统资源使用情况. Nuget 搜索 CZGL.SystemInfo ...
- 一起了解 .Net Foundation 项目 No.14
.Net 基金会中包含有很多优秀的项目,今天就和笔者一起了解一下其中的一些优秀作品吧. 中文介绍 中文介绍内容翻译自英文介绍,主要采用意译.如与原文存在出入,请以原文为准. .NET Core .NE ...
- [C++入门篇]了解C++
前言 我是杨某人,点击右下方"+"一键关注我.如果你喜欢我的文章,那么拒绝白嫖行为.然后..请多来做客鸭. 如果你是已经入门的大佬,请滑到下方点个推荐再走. 我个人认为,博客有两种 ...
- 爬虫使用中间代理人 fiddl...,charles,mitmproxy 设置
一般的设置在网上就能找到(端口,ip啥的) 但是难点是关于安卓手机证书 在网上找到的几种方法,一种是在app源码中添加设置让手机app同意你下载安装的证书,另一种则是root_adb 安装证书 但是太 ...
- JavaMail(一):利用JavaMail发送简单邮件
JavaMail,提供给开发者处理电子邮件相关的编程接口.它是Sun发布的用来处理email的API.它可以方便地执行一些常用的邮件传输.但它并没有包含在JDK中,要使用JavaMail首先要下载ja ...