深度解读equal方法与hashCode方法渊源

深度解读equal方法与hashCode方法渊源

大部分内容参考自重写equal()时为什么也得重写hashCode()之深度解读equal方法与hashCode方法渊源

1. equals()的所属以及内部原理(即Object中equals方法的实现原理)

说起equals方法,我们都知道是超类Object中的一个基本方法,用于检测一个对象是否与另外一个对象相等。而在Object类中这个方法实际上是判断两个对象是否具有相同的引用,如果有,它们就一定相等。其源码如下:

public boolean equals(Object obj){
    return (this == obj);
}

实际上我们知道所有的对象都拥有标识(内存地址)和状态(数据),同时“==”比较两个对象的的内存地址,所以说 Object 的 equals() 方法是比较两个对象的内存地址是否相等,即若 object1.equals(object2) 为 true,则表示 equals1 和 equals2 实际上是引用同一个对象。

2. equals()和’=='的区别

常常我们会说:

  • equals比较的是对象的内容
  • ==比较的是对象的地址

但是从前面我们可以知道equals方法在Object中的实现也是间接使用了‘==’运算符进行比较的,所以从严格意义上来说,不正确。

public class Car{
    private int batch;
    public Car(int batch){
        this.batch = batch;
    }
    public static void main(String[] args){
        Car c1 = new Car(1);
        Car c2 = new Car(1);
        System.out.println(c1.equals(c2));
        System.out.println(c1 == c2);
    }
}

结果:

false

false

分析

  1. 对于’=='返回false
    • 比较的是内存地址,而c1与c2是两个不同的对象,所以c1与c2的内存地址自然也不一样
  2. 对于equals返回false
    • 并没有重写equals()方法,调用的是Object超类的原始equals方法,其内部实现使用的是’=='运算符

但我们想让equals返回true,该如何实现呢?

为了达到我们的期望值,我们必须重写Car的equal方法,让其比较的是对象的批次(即对象的内容),而不是比较内存地址,于是修改如下:

@Override
	public boolean equals(Object obj) {
		if (obj instanceof Car) {
			Car c = (Car) obj;
			return batch == c.batch;
		}
		return false;
    }

使用instanceof来判断引用obj所指向的对象的类型,如果obj是Car类对象,就可以将其强制转为Car对象

结果:

true

false

总结

默认情况下也就是从超类Object继承而来的equals方法与‘==’是完全等价的,比较的都是对象的内存地址,但我们可以重写equals方法,使其按照我们的需求的方式进行比较,如String类重写了equals方法,使其比较的是字符的序列,而不再是内存地址。

3. equals()的重写规则

在重写equals方法时,还是需要注意如下几点规则:

  • 自反性:对于任何非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

在同一个类的两个对象间的比较这里就略过了,还是相当容易理解的。但是如果是子类与父类混合比较,那么情况就不太简单了。下面我们来看看另一个例子,首先,我们先创建一个新类BigCar,继承于Car,然后进行子类与父类间的比较。

public class BigCar extends Car {
	int count;
	public BigCar(int batch, int count) {
		super(batch);
		this.count = count;
	}
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof BigCar) {
			BigCar bc = (BigCar) obj;
			return super.equals(bc) && count == bc.count;
		}
		return false;
	}
	public static void main(String[] args) {
		Car c = new Car(1);
		BigCar bc = new BigCar(1, 20);
		System.out.println(c.equals(bc));
		System.out.println(bc.equals(c));
	}
}

运行结果:

true

false

分析

  • BigCar类型肯定是属于Car类型,所以c.equals(bc)肯定为true
  • Car类型并不一定是BigCar类型(Car类还可以有其他子类),所以bc.equals©返回false

​ 但如果有这样一个需求,只要BigCar和Car的生产批次一样,我们就认为它们两个是相当的,在这样一种需求的情况下,父类(Car)与子类(BigCar)的混合比较就不符合equals方法对称性特性了。

​ 很明显一个返回true,一个返回了false,根据对称性的特性,此时两次比较都应该返回true才对。那么该如何修改才能符合对称性呢?

​ 其实造成不符合对称性特性的原因很明显,那就是因为Car类型并不一定是BigCar类型(Car类还可以有其他子类),在这样的情况下(Car instanceof BigCar)永远返回false,因此,我们不应该直接返回false,而应该继续使用父类的equals方法进行比较才行(因为我们的需求是批次相同,两个对象就相等,父类equals方法比较的就是batch是否相同)。

 @Override
	public boolean equals(Object obj) {
		if (obj instanceof BigCar) {
			BigCar bc = (BigCar) obj;
			return super.equals(bc) && count == bc.count;
		}
		return super.equals(obj);
	}

此时符合了对称性,但还没有符合传递性

public class BigCar extends Car {
	int count;
	public BigCar(int batch, int count) {
		super(batch);
		this.count = count;
	}
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof BigCar) {
			BigCar bc = (BigCar) obj;
			return super.equals(bc) && count == bc.count;
		}
		return super.equals(obj);
	}
	public static void main(String[] args) {
		Car c = new Car(1);
		BigCar bc = new BigCar(1, 20);
		BigCar bc2 = new BigCar(1, 22);
		System.out.println(bc.equals(c));
		System.out.println(c.equals(bc2));
		System.out.println(bc.equals(bc2));
	}
}

结果:

true

true

false

bc,bc2,c的批次都是相同的,按我们之前的需求应该是相等,而且也应该符合equals的传递性才对。但是事实上运行结果却不是这样,违背了传递性。

出现这种情况根本原因在于:

  • 父类与子类进行混合比较。
  • 子类中声明了新变量,并且在子类equals方法使用了新增的成员变量作为判断对象是否相等的条件。

只要满足上面两个条件,equals方法的传递性便失效了。而且目前并没有直接的方法可以解决这个问题。因此我们在重写equals方法时这一点需要特别注意。虽然没有直接的解决方法,但是间接的解决方案还说有滴,那就是通过组合的方式来代替继承,还有一点要注意的是组合的方式并非真正意义上的解决问题(只是让它们间的比较都返回了false,从而不违背传递性,然而并没有实现我们上面batch相同对象就相等的需求),而是让equals方法满足各种特性的前提下,让代码看起来更加合情合理,代码如下:

public class Combination4BigCar {
	private Car c;
	private int count;
	public Combination4BigCar(int batch, int count) {
		c = new Car(batch);
		this.count = count;
	}
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Combination4BigCar) {
			Combination4BigCar bc = (Combination4BigCar) obj;
			return c.equals(bc.c) && count == bc.count;
		}
		return false;
	}
}

从代码来看即使batch相同,Combination4BigCar类的对象与Car类的对象间的比较也永远都是false,但是这样看起来也就合情合理了,毕竟Combination4BigCar也不是Car的子类,因此equals方法也就没必要提供任何对Car的比较支持,同时也不会违背了equals方法的传递性。

4. 为什么重写equals()的同时还得重写hashCode()

这个问题主要针对Map接口。当我们调用put或者get方法对Map容器操作时,是根据键对象的哈希码来计算存储位置的。在java中,我们可以使用hasCode()来获取对象的哈希码,其值就是对象的存储地址。

hashCode的意思是散列码,也就是哈希码,是由对象到处的一个整型值,散列码是没有规律的,如果x与y是两个不同的对象,那么x.hashCode()与y.hashCode()基本是不会相同的,下面通过String类的hashCode()计算一组散列码:

public class HashCodeTest {
	public static void main(String[] args) {
		int hash=0;
		String s="ok";
		StringBuilder sb =new StringBuilder(s);
		
		System.out.println(s.hashCode()+"  "+sb.hashCode());
		
		String t = new String("ok");
		StringBuilder tb =new StringBuilder(s);
		System.out.println(t.hashCode()+"  "+tb.hashCode());
	}
}

结果:

3548 1829164700

3548 2018699554

分析

  • 字符串s与t拥有相同的散列码,这是因为字符串的散列码是由内容导出的
  • 符串缓冲sb与tb却有着不同的散列码,这是因为StringBuilder没有重写hashCode方法,它的散列码是由Object类默认的hashCode方法计算出来的对象存储地址

重写equals方法时也必须重写hashCode方法

在Java API文档中关于hashCode方法有以下几点规定:

  • 在java应用程序执行期间,如果在equals方法比较中所用的信息没有被修改,那么在同一个对象上多次调用hashCode方法时必须一致地返回相同的整数。如果多次执行同一个应用时,不要求该整数必须相同。
  • 如果两个对象通过调用equals方法是相等的,那么这两个对象调用hashCode方法必须返回相同的整数。
  • 如果两个对象通过调用equals方法是不相等的,不要求这两个对象调用hashCode方法必须返回不同的整数。但是程序员应该意识到对不同的对象产生不同的hash值可以提供哈希表的性能。

如果一个类重写了equals方法,但没有重写hashCode方法,将会直接违法了第2条规定,这样的话,如果我们通过映射表(Map接口)操作相关对象时,就无法达到我们预期想要的效果。

5. 重写equals()中getClass与instanceof的区别

在重写equals() 方法时,一般都是推荐使用 getClass 来进行类型判断(除非所有的子类有统一的语义才使用instanceof),不是使用 instanceof。

  • instanceof 的作用:判断其左边对象是否为其右边类的实例,返回 boolean 类型的数据

父类Person:


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:


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;
        }
        /**
         * 重写equals()方法
         */
        public boolean equals(Object object){
            if(object instanceof Employee){
                Employee e = (Employee) object;
                return super.equals(object) && e.getId() == id;
            }
            return false;
        }
    }

上面父类 Person 和子类 Employee 都重写了 equals(),不过 Employee 比父类多了一个id属性,而且这里我们并没有统一语义。测试代码如下:


public class Test {
        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));
            System.out.println(p1.equals(e2));
            System.out.println(e1.equals(e2));
        }
}

上面代码我们定义了两个员工和一个普通人,虽然他们同名,但是他们肯定不是同一人,所以按理来说结果应该全部是 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(除非子类拥有统一的语义)。

6. 编写一个完美equals()的几点建议

出自Java核心技术 第一卷:基础知识:

  1. 显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量(参数名命名,强制转换请参考建议5)
  2. 检测this与otherObject是否引用同一个对象 :if(this == otherObject) return true;(存储地址相同,肯定是同个对象,直接返回true)
  3. 检测otherObject是否为null ,如果为null,返回false.if(otherObject == null) return false;
  4. 比较this与otherObject是否属于同一个类 (视需求而选择)
    • 如果equals的语义在每个子类中有所改变,就使用getClass检测 :if(getClass()!=otherObject.getClass()) return false; (参考前面分析的第5点)
    • 如果所有的子类都拥有统一的语义,就使用instanceof检测 :if(!(otherObject instanceof ClassName)) return false;(即前面我们所分析的父类car与子类bigCar混合比,我们统一了批次相同即相等)
  5. 将otherObject转换为相应的类类型变量:ClassName other = (ClassName) otherObject;
  6. 现在开始对所有需要比较的域进行比较 。使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true,否则就返回flase。
    • 如果在子类中重新定义equals,就要在其中包含调用super.equals(other)
    • 当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明 相等对象必须具有相等的哈希码

参考资料:

Java核心技术 第一卷:基础知识

Java深入分析

equals()方法总结

猜你喜欢

转载自blog.csdn.net/yisany_Q/article/details/83512964