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联系

  1. 当我们自定义类不需要充当key来在散列表中存储对象时,equals和hashCode根本没有关系,你也没必要重写hashCode方法
  2. 当我们会用自定义类充当key在散列表中存对象,这时候你一定要重写equals和hashCode

彻底明白equals和hashCode的更多相关文章

  1. Java equals 和 hashCode 的这几个问题可以说明白吗?

    前言 上一篇文章 如何妙用 Spring 数据绑定? ,灵魂追问 环节留下了一个有关 equals 和 hashcode 问题 .基础面试经常会碰到与之相关的问题,这不是一个复杂的问题,但很多朋友都苦 ...

  2. Java中的equals和hashCode方法

    本文转载自:Java中的equals和hashCode方法详解 Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要 ...

  3. Java提高篇——equals()与hashCode()方法详解

    java.lang.Object类中有两个非常重要的方法: 1 2 public boolean equals(Object obj) public int hashCode() Object类是类继 ...

  4. Java实战equals()与hashCode()

    一.equals()方法详解 equals()方法在object类中定义如下: 代码 public boolean equals(Object obj) { return (this == obj); ...

  5. java集合(3)- Java中的equals和hashCode方法详解

    参考:http://blog.csdn.net/jiangwei0910410003/article/details/22739953 Java中的equals方法和hashCode方法是Object ...

  6. java基础(十六)----- equals()与hashCode()方法详解 —— 面试必问

    本文将详解 equals()与hashCode()方法 概述 java.lang.Object类中有两个非常重要的方法: public boolean equals(Object obj) publi ...

  7. Java中的equals和hashCode方法详解

    Java中的equals和hashCode方法详解  转自 https://www.cnblogs.com/crazylqy/category/655181.html 参考:http://blog.c ...

  8. 为什么要重写equals和hashcode方法

    equals hashcode  当新建一个java类时,需要重写equals和hashcode方法,大家都知道!但是,为什么要重写呢? 需要保证对象调用equals方法为true时,hashcode ...

  9. java学习-- equals和hashCode的关系

    hashcode的目的就是在hashset或者hashmap等中比较两个对象相等时,减少equals的使用次数来提高效率 以下为摘录 java中hashcode和equals的区别和联系 HashSe ...

随机推荐

  1. Redis:slave flush old data造成实例不可用

    一.问题描述 2019-02-22凌晨02:42分前后,收到集群中 [10.32.52.8:6500] 实例不可用告警,登陆管理界面查看此实例在正常运行状态,期间未出现机器宕机或实例直接挂掉的现象. ...

  2. 从 ListView 到 RecyclerView 的用法浅析

    文章目录 要走好明天的路,必须记住昨天走过的路,思索今天正在走着的路. ListView,一种在垂直滚动列表中显示条目的视图:RecyclerView,一种在局限的窗口呈现大数据集合的灵活视图.Rec ...

  3. HTML5全屏背景视频与 CSS 和 JS(插件或库)

    译文原链接:http://codetheory.in/html5-fullscreen-background-video/ 前言: 当网页载入时,自动播放的全屏背景视频已经成为当前颇受欢迎的趋势. 就 ...

  4. Zookeeper的核心概念以及java客户端使用

    一.Zookeeper的核心概念 分布式配置中心(存储):disconf(zk).diamond(mysql+http) 1)znode ZooKeeper操作和维护的是一个个数据节点,称为 znod ...

  5. Oracle密码验证函数与Create Profile

    今天看到了一个oracle密码函数的东西,就在网上找文档自己做测试,刚开始看不懂,最后做完记录一下 密码函数的作用就是要将用户密码进行限制,比如申请一个网站的账号的时候,密码会要求你不少于8位,必须要 ...

  6. Markdown中插入复杂的合并表格方法

    由于Markdown自身的语法限制,不能直接插入有合并单元格的复杂表格. 姓名 学号 专业 张三 2018123456 计算机 赵四 2018222356 自动化 李六 2018666666 信息工程 ...

  7. MATLAB神经网络(2) BP神经网络的非线性系统建模——非线性函数拟合

    2.1 案例背景 在工程应用中经常会遇到一些复杂的非线性系统,这些系统状态方程复杂,难以用数学方法准确建模.在这种情况下,可以建立BP神经网络表达这些非线性系统.该方法把未知系统看成是一个黑箱,首先用 ...

  8. Python 装饰器(无参,有参、多重))

    Python装饰器介绍 在Python中,装饰器(decorator)是在闭包的基础上发展起来的. 装饰器的实质是一个高阶函数,其参数是要装饰的函数名,其返回值是完成装饰的函数名,其作用是为已经存在的 ...

  9. 关于动态路由中路由之间的跳转(页面a跳转到页面b)

    由addRouters方法获取到后台的动态路由,要实现路由之间的跳转,不可直接用path: '***',而是将动态路由存储到vuex中,再从vuex中取得,如:this.$store.menu.nav ...

  10. 3.后台配置、环境变量、日志、异常处理、二次封装Response、路由组件

    目录 环境变量 封装logger 封装项目异常处理 二次封装Response模块 路由组件配置 环境变量 dev.py # 环境变量操作:小luffyapiBASE_DIR与apps文件夹都要添加到环 ...