首页 > 编程语言 >Java基础(四)—— HashCode和Equals

Java基础(四)—— HashCode和Equals

时间:2022-09-18 21:36:28浏览次数:62  
标签:return 对象 equals hashCode Equals Java HashCode true public

正如标题所言,今天我们来讲讲hashCode和equals。或许有些人会奇怪了,这两个东西为什么要放在一起来讲呢?这是因为按照JDK规范:

如果两个对象根据equals方法比较是相等的,那么调用这两个对象中的hashCode方法都必须产生相同的整数结果。

所以为了遵守这个约定,就必须在重写equals时同样重写hashCode方法。如果不这样的话,就会违反该约定。

违反约定的后果

如果违反了这个约定,会出现什么后果呢?我们来一起探讨一下。最简单的一个分析,如果这种违反了约定的对象插入到HashSet中会怎么样呢?

 

首先,我们应该知道,HashSet的底层实现是使用的HashMap。代码如下:

public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;

private transient HashMap<E,Object> map;

private static final Object PRESENT = new Object();

public HashSet() {
map = new HashMap<>();
}

public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}

// 略
}

从源码中可以看出,在构造HashSet时会初始化内部的map对象,然后addremove等对set的操作其实就是对内部map的操作。add的对象会作为map的key,PRESENT这个Object的对象会作为map的value,这两者作为一个键值对put到map中。

 

那么问题来了,HashMap的put流程是怎么样的呢?这里我先简要说下:主要是根据key的hash值算到对应的槽,如果对应槽位有值,则比较槽位值的key与插入key是否相等(hashCode,==,equals都为true),如果为true的则槽位的值会被覆盖,否则遍历判断该槽位下的链表,如果都不相等则链表链接新值。主要流程图如下:

 

 

到这里其实我们就能了解到几点:

  1. HashSet去重用的是HashMap的key如果相等会覆盖value的特性,而相等首先是hash之后会进入同一个槽,然后再通过hashCode和equals等判断是否为true,这才保证是相等的。

  2. 如果HashSet的add的对象equals为true,但是hashCode不是相等的值,那么就可能会出现add第二个值时,导致第二个对象也被HashMap存储,以至于HashSet的去重特性被打破。

可以使用以下代码验证:

@AllArgsConstructor
public class HashEqualsTest {

private String name;

private Integer age;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof HashEqualsTest)) return false;
HashEqualsTest that = (HashEqualsTest) o;
return name.equals(that.name) && age.equals(that.age);
}

public static void main(String[] args) {

HashEqualsTest a = new HashEqualsTest("aischen", 3);
HashEqualsTest b = new HashEqualsTest("aischen", 3);
System.out.println(a.equals(b));

Set<HashEqualsTest> set = new HashSet<>();
set.add(a);
set.add(b);
System.out.println(set);
}
}

输出结果:

true

[org.aischen.HashEqualsTest@5b2133b1, org.aischen.HashEqualsTest@77459877]

 

可以看到,结果确实如我们所料,因为违反了hashCode和equals的约定,所以HashSet可以插入多个相互之间equals为true的对象,那这种对象的到底算不算重复对象,就见仁见智了。就实际业务上来说,这种对象是算重复对象的,毕竟相同名字,相同身份证号的两条数据,不能就算是两个人吧。

hashCode一些特性

我们已经知道hashCode一般是用于散列寻址和前置判断使用,那么hashCode可以随意生成吗?怎么生成比较好呢?

 

还是以HashMap举例,如果我们的hashCode生成算法不够优雅,生成的hashCode值碰撞概率高,以极端情况来看,hash之后所有的元素全部在一个hash槽中,那就完全成了一个链表或者红黑树了。所有的查询都得基于链表和红黑树来查。而纯以计算来说,逻辑越多,计算越多,那效率必然就越低,所以经过了那么多前置的计算和判断之后还是用链表的数据结构,那相对于单纯使用链表效率必然是比不上的。

 

我们来看这样一段代码:

public static int hashCode(Object a[]) {
if (a == null)
return 0;

int result = 1;

for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());

return result;
}

 

这其实是JDK中的Objects.hashCode方法,具体逻辑就不讲了,很直观。

 

首先说明下为什么需要使用乘法。有乘法的话,就使得散列值依赖于传入参数的顺序,如果一个类包含了多个相似的域,这样的乘法运算就会产生一个更好区分的散列值。

 

其次为什么要选择31这样的数。引用自Effective Java:

之所以选择31,是因为它是一个奇素数。如果乘法是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。使用素数的好处并不是很明显,但是习惯上都使用素数来计算散列结果。31有个很好的特性,即使用移位和减法来代替乘法,可以得到更好的性能:31×i==(i<<5) - 1。现代的虚拟机可以自动完成这种优化。

 

如此,我们知道,选择31的原因主要还是性能。不过实际上我们使用时,也只需要调用Objects.hashCode的方法即可,无需再重复造轮子。

 

平常对象或者String的hashCode都是一些比较长的数字。但是Integer或者Short等这种整型的类,他们的hashCode就等于他们的value,这点在实际使用时可以注意下。

equals一些特性

我们已经知道,equals是用来比较对象是否相等的一个函数,如果没有重写的话,那默认是使用"=="。而我们常用的一些类其实JDK的开发人员已经帮我们重写过了,比如String,比如Integer。

 

在重写equals方法时,必须要遵守它的通用约定。下面是约定的内容,来自Object的规范:

  1. 自反性:对于任何非null的引用值x,x.equals(x)必须返回true。

  2. 对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。

  3. 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。

  4. 一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致的返回true或者一致的返回false。

  5. 对于任何非null的引用值x,x.equals(null)时必须返回false。

 

约定看起来很多,也好像很复杂的样子,但是却是我们必须遵守的一些约定,否则就可能会导致系统出现非常严重的后果,甚至崩溃,而且你很难找到根源。John Donne说过:没有哪个类是孤立的,一个类的实例通常会被频繁的传递给另一个类的实例。

 

不过这些约定虽然看起来比较复杂,但实际上并非如此,一旦理解了,遵守它们也并不困难。

 

自反性

很难想象会怎么无意识违反这一约定。假如违背这一条的话,那么把类加到集合实例中,再调用集合的contains方法时将返回false,告诉你该集合不包含刚刚添加的实例。

 

对称性

这个要求是说,任何两个对象对于"它们是否相等"的问题都必须保持一致。这种违反的情况其实不难想象。一般是用于equals不同的类导致的。比如有段代码如下:

public class IgnoreCaseString {

private final String s;

public IgnoreCaseString(String s) {
this.s = Objects.requireNonNull(s);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof IgnoreCaseString)
return s.equalsIgnoreCase(((IgnoreCaseString) o).s);
if (o instanceof String)
return s.equalsIgnoreCase((String) o);
return false;
}
}

这个意图很明显,是想能和普通的字符串进行互操作。 但是这样会明显的违反对称性:

    public static void main(String[] args) {
String s = "aTad";
IgnoreCaseString ics = new IgnoreCaseString("atad") ;
System.out.println(ics.equals(s));
System.out.println(s.equals(ics));
}

结果不出所料:第一个返回true,第二个返回false

true

false

 

传递性

这点是要求我们在第一个对象等于第二个对象,第二个对象等于第三个对象时,第一个对象一定等于第三个对象。这个要求无意识违反的话也是不难想象的,比如类继承的的情况下,扩展了属性,那么需不需要将扩展属性也加入到比较中呢?

 

如果不加的话那么显然是不会违反equals约定的,但是新加的信息被比较时忽略掉也是无法接受的。那么就需要将扩展信息加入到比较中。此时就会出现对称性问题。

 

例如这两个类:

public class Point {

private int x;

private int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
}

class Point3D extends Point {

private int z;

public Point3D(int x, int y, int z) {
super(x, y);
this.z = z;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point3D)) return o.equals(this);
if (!super.equals(o)) return false;
Point3D point3D = (Point3D) o;
return z == point3D.z;
}
}

 

这两个类的equals都是很常见的通过ide生成的。但是很明显它们违反了对称性。如果要解决该问题也很简单,将代码if (!(o instanceof Point3D)) return false;调整为if (!(o instanceof Point3D)) return o.equals(this); 即可。但是这样虽然可以保证对称性,但是确违反了传递性。

 

事实上,这种解法还有可能导致无限递归问题,假设Point有两个子类,且它们各自都带有一个equals方法,那么两个子类对象之间的equals将导致它们互相调用对方的equals方法,然后将抛出StackOverflowError

 

事实上,这种问题是无解的。我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象所带来的优势。

 

一致性

这个要求是,如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象发生了变更。

 

无论类是否可变,都不要使equals依赖于不可靠的资源。如果违反了这一原则,要想满足一致性就十分困难了,例如JDK中的URL类,因为它的equals是依赖于IP地址的比较,而IP又是需要访问网络的,随着时间的推移,就不能确保会产生相同的结果。遗憾的是,因为兼容性要求,这一行为无法被改变。为了避免这种问题,equals方法应该对驻留在内存中的对象执行确定性的计算。

 

最后再留下几个告诫:

  1. 重写equals时需要重写hashCode

  2. 不要企图让equals方法过于智能。

  3. 不要将equals声明中的Object替换成别的类,否则就不是重写equals方法而是重载了。

 

写在最后

到此我们的分享就告一段落了,虽然篇幅不算短,但是总感觉还有很多东西其实是没有说清楚了,只是说的很粗略,很大概。可能也是因为我最近实在是太忙了,每天都要到凌晨到家,只有周天的这一点点时间可以挪出来写点东西,但是又因为太累需要补血,以至于都不能保证一整天的时间有输出。另外就是也忙得连书都没法看了,希望过了这段忙碌期能够好起来,上周又欠了一片文章,到现在算下来的话已经欠了两篇的。只能在后面找时间给补上了。

 

最后,希望大家能好好学习,天天向上~

标签:return,对象,equals,hashCode,Equals,Java,HashCode,true,public
From: https://www.cnblogs.com/aischen/p/16705861.html

相关文章

  • java初步学习(基于黑马的课进行自学,初学者,不喜勿喷)7
    初步学习循环for“for”循环格式如下for(初始化语句;条件判断语句;条件控制语句){循环体语句;}执行流程:1.执行初始化语句2.执行条件判断语句,判定其结果为“true”......
  • java第二周
    static声明的成员变量为静态成员变量,其的生命周期和类相同,在整个应用程序执行期间都有效。静态方法不能调用非静态成员。static可以用来修饰类的成员方法、类的成员变量......
  • javase基础
    1.类与对象*类是方法与属性的集合,是一种抽象的概念*对象是对该类事物的具体体现形式,具体存在的个体studentstu1=newstudent();student为类名stu1为对象名=new......
  • Java8新特性
    1.lambda表达式即允许将函数作为参数传递进方法中。可以替代匿名内部类的编写新手一开始不能直接写出lambda表达式,我们可以先用Idea的提示写出匿名内部类,匿名内部类比较......
  • k8s 的java程序内存设置多大合适 怎么设置
     主要参考的三个博客参考1:https://www.cnblogs.com/xiaoqi/p/container-jvm.html参考2:https://www.imooc.com/article/292785?block_id=tuijian_wz参考3:https://blog.csd......
  • java: Flyweight Patterns
     /***版权所有2022涂聚文有限公司*许可信息查看:*描述:*享元模式FlyweightPatterns*历史版本:JDK14.02*2022-09-12创建者geovindu*2022-09-1......
  • javascript中的一些细节,undefined和null的区别,什么情况下是false,函数赋值,等等
    如果不赋值,就使用默认值,page=1,size=10如果赋值按位置赋值,如果要跨越位置赋值size,则page定义为undefined则使用的是默认值如下图:javascript什么情况下是false,什么情况......
  • Java8/18
    Java流程控制1.用户交互ScannerScanner是Java5提供的一个工具类(java.until.Scanner),用来获取用户的输入,实现人机交互。基本语法:Scanners=newScanner(System.in);......
  • Java8/18
    Java方法1.方法概念eg:System.out.println();----System是一个系统类,out是System类的一个输出对象,println()就是一个方法什么是方法:Java方法是语句的集合,他们在一起执行......
  • Java static关键字
    在类中,使用static修饰符修饰的属性(成员变量)称为静态变量,也可以称为类变量,常量称为静态常量,方法称为静态方法或类方法,它们统称为静态成员,归整个类所有。注意:static......