Java的equals和hashCode方法深度漫游

2018-09-1423:05:37后端程序开发Comments2,051 views字数 10739阅读模式

1、谈谈equals方法

相信大家对这个这个方法一定不陌生。该方法是Object基类里的非final方法(被设计成可覆盖的),下面我们来看看Object中是如何实现该方法的。源代码如下:
public boolean equals(Object obj) { return (this == obj); }
从源代码我们知道,Object类默认为我们实现了通过比较两个对象的内存地址是来判断对象之间是否相等。即若 object1.equals(object2) 为 true,则表示 object1 和 object2 实际上是引用同一个对象。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

2、equals方法和==操作符的异同

我们知道对于引用类型,==操作符比较的都是对象间的内存地址(对象的引用)。通过看Object类源码知道,equals方法默认实现间接使用了==操作符。细心的同学可能会问,那我们想比较对象之间的内容呢?这时候我们可以通过覆盖(override)equals方法来自定义比较逻辑。
简单总结如下:
默认情况下也就是从超类Object继承而来的equals方法与‘==’是完全等价的,比较的都是对象的内存地址,但我们可以重写equals方法,使其按照我们需要的方式进行比较,如String类重写了equals方法,比较的是字符的序列,而不再是内存地址。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

3、equals方法的重写规则

我们知道通过重写equals方法可以自定义比较逻辑,重写Equal方法是需要有相关规则的,下面我们来一一介绍。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

  • 自反性。对于任何非null的引用值x,x.equals(x)应返回true。
  • 对称性。对于任何非null的引用值x与y,当且仅当:y.equals(x)返回true时,x.equals(y)才返回true。
  • 传递性。对于任何非null的引用值x、y与z,如果y.equals(x)返回true,y.equals(z)返回true,那么x.equals(z)也应返回true。
  • 一致性。对于任何非null的引用值x与y,假设对象上equals比较中的信息没有被修改,则多次调用x.equals(y)始终返回true或者始终返回false。
  • 对于任何非空引用值x,x.equal(null)应返回false。 一般情况下,对于同一个类的两个实例进行比较,都可以满足这5个规则,我们来看下面这个例子:
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 obj) {
        if(!(obj instanceof Point)) {
    	    return false;
        }
        Point point=(Point)obj;
        return (this.x==point.x && this.y==point.y);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(this.x,this.y);
    }
}
复制代码

接下来我们来看验证这5条规则的代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

public static void main(String[] args) {
	Point point1=new Point(1, 1);
	Point point2=new Point(1, 1);
	Point point3=new Point(1, 1);
	//equals方法规则测试
	System.out.println("equals方法自反性:"+point1.equals(point1));
	System.out.println("equals方法对称性:");
	System.out.println(point1.equals(point2));
	System.out.println(point2.equals(point1));
	System.out.println("equals方法传递性:");
	System.out.println(point1.equals(point2));
	System.out.println(point2.equals(point3));
	System.out.println(point1.equals(point3));
	System.out.println("equals方法一致性:");
	for(int i=0;i<5;i++) {
	    System.out.println(point1.equals(point2));
	}
	System.out.println("null比较:");
	System.out.println(point1.equals(null));
	System.out.println(point2.equals(null));
	System.out.println(point3.equals(null));
}
复制代码

运行结果如下:
equals方法自反性:true文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

equals方法对称性: true true文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

equals方法传递性: true true true文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

equals方法一致性: true true true true true文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

null比较: false false false文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

我们看到同一个类进行比较,理解起来不难。如果重写equals方法存在继承情况呢?下面我们就来详细讨论这个情况。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

类继承层次下的equals方法重写的要点

我们还是以刚才PointClass为例,现在我们添加一个color字段,原来的Point类代码保持不变,ColorPoint代码如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

class ColorPoint extends Point{
    private String color;
    public ColorPoint(int x,int y,String color) {
    	super(x, y);
    	this.color=color;
    }
    
    @Override
    public boolean equals(Object obj) {
    	if(!(obj instanceof ColorPoint)) {
    		return false;
    	}
    	ColorPoint colorPoint=(ColorPoint)obj;
    	return (super.equals(colorPoint) 
    			&& this.color.equals(colorPoint.color));
    }
    
    @Override
    public int hashCode() {
    	return Objects.hash(super.hashCode(),this.color);
    }
}
复制代码

我们在Main方法写代码测试下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

ColorPoint colorPoint=new ColorPoint(1, 1, "#ffffff");
    Point point=new Point(1, 1);
    System.out.println("对称性:");
    System.out.println(colorPoint.equals(point));	//false
    System.out.println(point.equals(colorPoint));	//true
复制代码

我们看到第2个条件对称性现在就无法满足了。我们稍稍分析下,第一个比较方法返回false,很好理解,因为point实例不是ClorPoint类型,所以在ColorPoint类equals方法里面if(!(obj instanceof ColorPoint))成立,返回false。第二个比较方法执行的是Point类的equals方法,会正常返回true。如果现在我们有特殊需求,两个对象只要是x,y一致,我们就认为是一样的对象。显然,上面的这种重写方式是不对的。 这种比较方法问题在于,colorPoint.equals(point)总是会返回false。那如何解决呢?聪明的同学想到了这种方法:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

@Override
public boolean equals(Object obj) {
    if(!(obj instanceof Point)) {
    	return false;
    }
    if(!(obj instanceof ColorPoint)) {
    	return obj.equals(this);
    }
    ColorPoint colorPoint=(ColorPoint)obj;
    return (super.equals(colorPoint) 
	    && this.color.equals(colorPoint.color));
}
复制代码

这个方法似乎符合要求,这个方法首先判断obj是否是Point类及其子类,如果不是,那么直接返回false。否则的话将比较访问缩小,看是否是ColorPoint及其子类,如果不是的话,说明至少是ColorPoint的基类,就可以使用它equals方法进行比较,否则使用ColorPoint类的equals方法比较。
但是这个写法不符合传递性规则,我们举例来看:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

ColorPoint colorPoint1=new ColorPoint(1, 1, "#ffffff");
ColorPoint colorPoint2=new ColorPoint(1, 1, "#000000");
Point point=new Point(1, 1);
System.out.println("传递性:");
System.out.println(colorPoint1.equals(point));		//true
System.out.println(point.equals(colorPoint2));		//true	
System.out.println(colorPoint2.equals(colorPoint1));//false
复制代码

出现这种情况的根本原因在于:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

  • 父类与子类进行混合比较。
  • 子类中声明了新变量,并且在子类equals方法使用了新增的成员变量作为判断对象是否相等的条件。
    只要满足上面两个条件,传递性就会失效。Effective Java中第8条对这个有一个总结:这是面向对象语言中关于等价关系的一个基本问题,我们无法再扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象带来的优势。 没有直接的方法解决这个问题,间接的方法还是有的,不要使用继承结构,使用组合的方式。我们将上面的ColorPoint类进行改写。代码如下:
class ColorPoint{
    private String color;
    private Point point;
    
    public ColorPoint(int x,int y,String color) {
    	point=new Point(x, y);
    	this.color=color;
    }
    
    @Override
    public boolean equals(Object obj) {
    	if(!(obj instanceof ColorPoint)) {
    		return false;
    	}
    	ColorPoint colorPoint=(ColorPoint)obj;
    	return (colorPoint.point.equals(point) 
    			&& this.color.equals(colorPoint.color));
    }
    
    @Override
    public int hashCode() {
    	return Objects.hash(this.point.hashCode(),this.color);
    }
}
复制代码

重写equals是getClass和instanceof关键字的区别

我们前面在重写equals方法时,使用的都是instanceof方法,但是重写equals时推荐的还是使用getClass来进行类型判断。什么情况下可以使用instanceof关键字,除非父类和子类有统一的语义。下面举一个例子给大家看下:
首先是:父类Person文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

public class Person {
    protected String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Person(String name){
        this.name = name;
    }
    public boolean equals(Object object){
        if(object instanceof Person){
            Person p = (Person) object;
            if(p.getName() == null || name == null){
                return false;
            }
            else{
                return name.equalsIgnoreCase(p.getName ());
            }
        }
        return false;
    }
}
复制代码

子类Employee文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

public class Employee extends Person{
    private int id;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public Employee(String name,int id){
        super(name);
        this.id = id;
    }
    public boolean equals(Object object){
        if(object instanceof Employee){
            Employee e = (Employee) object;
            return super.equals(object) && e.getId() == id;
        }
        return false;
    }
}
复制代码

我们来看测试代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

public static void main(String[] args) {
    Employee e1 = new Employee("chenssy", 23);
    Employee e2 = new Employee("chenssy", 24);
    Person p1 = new Person("chenssy");
    System.out.println(p1.equals(e1));  //true
    System.out.println(p1.equals(e2));  //true
    System.out.println(e1.equals(e2));  //false
}
复制代码

上面代码我们定义了两个员工和一个普通人,虽然他们同名,但是他们肯定不是同一人,所以按理来说结果应该全部是 false,但结果是:true、true、false。对于那 e1!=e2 我们非常容易理解,因为他们不仅需要比较 name,还需要比较 ID。但是 p1 即等于 e1 也等于 e2,这是非常奇怪的,因为 e1、e2 明明是两个不同的实例,但为什么会出现这个情况?首先 p1.equals(e1),是调用 p1 的 equals 方法,该方法使用 instanceof 关键字来检查 e1 是否为 Person 类,这里我们再看看 instanceof:判断其左边对象是否为其右边类的实例,也可以用来判断继承中的子类的实例是否为父类的实现。他们两者存在继承关系,肯定会返回 true 了,而两者 name 又相同,所以结果肯定是 true。所以出现上面的情况就是使用了关键字 instanceof,这是非常容易导致我们“钻牛角尖”。故在覆写 equals 时推荐使用 getClass 进行类型判断。而不是使用 instanceof(除非子类equals方法拥有统一的语义,例如继承Point类的子类中,我们认为只需要x,y相等的两个实例就认为是相等的)。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

编写一个高质量equals方法的几点建议

  • 使用==操作符检查“参数是否为这个对象的引用”,如果是,返回true,这只是一种性能优化的写法。
  • 如果equals方法的语义在每一个子类中有所改变,就使用getClass来判断类型。如果所有的子类都拥有统一的语义(例如继承Point类的子类中,我们认为只需要x,y相等的两个实例就认为是相等的)我们可以使用instanceof来判断类型。
  • 对于该类中的每个“关键”域,检查参数中的域是否与该对象中的对应的域相匹配。如果测试全部成功,返回true,否则返回false。
  • 当你编写完equals返回之后,应该确认它是否是对称的、可传递的、一致的。
  • 覆盖equals时总要覆盖hashCode

4、不符合规则的equals方法带来的影响

前面说的都是如何写出符合规范的equals方法,那么我们写的equals方法如果不符合5条规范,到底会有什么影响呢?我们通过例子来看一下文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

class A {
    @Override
    public boolean equals(Object obj) {
    	return obj instanceof A;
    }
}

class B extends A {
    @Override
    public boolean equals(Object obj) {
    	return obj instanceof B;
    }
}

public class RunMain {
    public static void main(String[] args) {
    	List<A> list=new ArrayList<>();
    	A a=new A();
    	B b=new B();
    	list.add(a);
    	System.out.println("list.contains(a)"+list.contains(a));	//true
    	System.out.println("list.contains(b)"+list.contains(b));	//false
    	
    	list.clear();
    	
    	list.add(b);
    	System.out.println("list.contains(a)"+list.contains(a));	//true
    	System.out.println("list.contains(b)"+list.contains(b));	//true
    }
}
复制代码

我们看上面的A,B两个类equals方法是不符合对称性的。我们看到我们将其放入a的时候list.contains(b)返回false。当我们放入b的时候list.contains(a)又返回true了,那我们来看看contains返回到底执行了什么。代码如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

/**
 * Returns the index of the first occurrence of the specified element
 * in this list, or -1 if this list does not contain the element.
 * More formally, returns the lowest index <tt>i</tt> such that
 * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
 * or -1 if there is no such index.
 */
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}
复制代码

我们先来看第一组比较过程,list里面现在有a对象,根据contains源码(o.equals(elementData[i])),我们知道它会执行这两种代码a.equals(a)和b.equals(a)。结果当然是true和false。接下来我们来看第二组比较过程,这时候list里面保存的是b对象。两次的比较过程分别是b.equals(b)和a.equals(b)。结果当然会是true和true。 很显然,上面的重写equals方法是存在问题的。它没有遵循对称性原则。我们只需要修改B类的equals方法即可。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

 class B extends A{
    @Override
    public boolean equals(Object obj) {
    	if(obj instanceof B){
    		return true;
    	}
    	return super.equals(obj);
    }
}
复制代码

简单的总结:只要是java集合类或者java类库中的其他方法,重写equals不遵守5点原则的话,都可能出现意想不到的结果。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

5、重写equals方法的时候必须也重写hashCode方法

前面介绍正确重写equals方法有很大一部分原因是为了配合JDK集合类使用的时候不出现问题(集合类的很多操作都是依赖于正确实现equals方法的)。重写hashCode方法是为了类实例在Map集合中使用的时候可以产生正确的行为。
学过数据结构的同学都知道Map接口的类会使用到键对象的哈希码,当我们调用put方法或者get方法对Map容器进行操作时,都是根据键对象的哈希码来计算存储位置的,因此如果我们对哈希码的获取没有相关保证,就可能会得不到预期的结果。在java中,我们可以使用hashCode()来获取对象的哈希码,其值就是对象的存储地址(默认实现),这个方法在Object类中声明,因此所有的子类都含有该方法。hashCode的意思就是散列码,也就是哈希码,是由对象导出的一个整型值,散列码是没有规律的,如果x与y是两个不同的对象,那么x.hashCode()与y.hashCode()基本是不会相同的。那么equals方法和hashCode方法到底有什么关联呢?我们来详细的看下,相关规则如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

  • 在java应用程序执行期间,如果在equals方法比较中所用的信息没有被修改,那么在同一个对象上多次调用hashCode方法时必须一致地返回相同的整数。如果多次执行同一个应用时,不要求该整数必须相同。
  • 如果两个对象通过调用equals方法是相等的,那么这两个对象调用hashCode方法必须返回相同的整数。
  • 如果两个对象通过调用equals方法是不相等的,不要求这两个对象调用hashCode方法必须返回不同的整数。但是程序员应该意识到对不同的对象产生不同的hash值可以提供哈希表的性能。
    通过前面的分析,我们知道在Object类中,hashCode方法是通过Object对象的地址计算出来的,因为Object对象只与自身相等,所以同一个对象的地址总是相等的,计算取得的哈希码也必然相等,对于不同的对象,由于地址不同,所获取的哈希码自然也不会相等。因此到这里我们就明白了,如果一个类重写了equals方法,但没有重写hashCode方法,将会直接违法了第2条规定,这样的话,如果我们通过映射表(Map接口)操作相关对象时,就无法达到我们预期想要的效果,下面我们通过一个例子来看一下:
public static void main(String[] args) {
    	Map<String,Value> map1 = new HashMap<String,Value>();
    	String s1 = new String("key");
    	String s2 = new String("key");	
    	Value value = new Value(2);
    	map1.put(s1, value);
    	System.out.println("s1.equals(s2):"+s1.equals(s2));
    	System.out.println("map1.get(s1):"+map1.get(s1));
    	System.out.println("map1.get(s2):"+map1.get(s2));
    	
    	Map<Key,Value> map2 = new HashMap<Key,Value>();
    	Key k1 = new Key("A");
    	Key k2 = new Key("A");
    	map2.put(k1, value);
    	System.out.println("k1.equals(k2):"+s1.equals(s2));
    	System.out.println("map2.get(k1):"+map2.get(k1));
    	System.out.println("map2.get(k2):"+map2.get(k2));
    }
	
    /**
     * 键
     */
    class Key{
    	private String k;
    	public Key(String key){
    		this.k=key;
    	}
    	
    	@Override
    	public boolean equals(Object obj) {
    		if(obj instanceof Key){
    			Key key=(Key)obj;
    			return k.equals(key.k);
    		}
    		return false;
    	}
    }
	
    /**
     * 值
     */
    static class Value{
    	private int v;
    	
    	public Value(int v){
    		this.v=v;
    	}
    	
    	@Override
    	public String toString() {
    		return "类Value的值-->"+v;
    	}
    }
复制代码

运行结果如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

s1.equals(s2):true
map1.get(s1):类Value的值-->2
map1.get(s2):类Value的值-->2
k1.equals(k2):true
map2.get(k1):类Value的值-->2
map2.get(k2):null
复制代码

对于s1和s2的结果,我们并不惊讶,因为相同的内容的s1和s2获取相同内的value这个很正常,因为String类重写了equals方法和hashCode方法,使其比较的是内容和获取的是内容的哈希码。但是对于k1和k2的结果就不太尽人意了,k1获取到的值是2,k2获取到的是null,这是为什么呢?想必大家已经发现了,Key只重写了equals方法并没有重写hashCode方法,这样的话,equals比较的确实是内容,而hashCode方法呢?没重写,那就肯定调用超类Object的hashCode方法,这样返回的不就是地址了吗?k1与k2属于两个不同的对象,返回的地址肯定不一样,所以现在我们知道调用map2.get(k2)为什么返回null了吧?那么该如何修改呢?很简单,我们要做也重写一下hashCode方法即可(如果参与equals方法比较的成员变量是引用类型的,则可以递归调用hashCode方法来实现):文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

@Override
public int hashCode() {
     return k.hashCode();
}
复制代码

作者:dreamGong
链接:https://juejin.im/post/5b4f4e0451882519ee7fc3d8
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4637.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/bc/4637.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定