最近我读了这篇文章 开发人员工作文件。
该文档是关于有效和正确地定义hashCode()和equals(),但我无法弄清楚为什么我们需要覆盖这两个方法。
我如何决定有效地实现这些方法?
最近我读了这篇文章 开发人员工作文件。
该文档是关于有效和正确地定义hashCode()和equals(),但我无法弄清楚为什么我们需要覆盖这两个方法。
我如何决定有效地实现这些方法?
当前回答
class A {
int i;
// Hashing Algorithm
if even number return 0 else return 1
// Equals Algorithm,
if i = this.i return true else false
}
put('key','value')将使用hashCode()计算哈希值来确定 桶,并使用equals()方法查找该值是否已经 出现在桶里。如果不是,它将被添加,否则它将被替换为当前值 get('key')将使用hashCode()首先找到条目(桶) equals()来查找Entry中的值
如果两者都被覆盖,
地图<A>
Map.Entry 1 --> 1,3,5,...
Map.Entry 2 --> 2,4,6,...
If =没有被覆盖
地图<A>
Map.Entry 1 --> 1,3,5,...,1,3,5,... // Duplicate values as equals not overridden
Map.Entry 2 --> 2,4,6,...,2,4,..
如果hashCode没有被覆盖
地图<A>
Map.Entry 1 --> 1
Map.Entry 2 --> 2
Map.Entry 3 --> 3
Map.Entry 4 --> 1
Map.Entry 5 --> 2
Map.Entry 6 --> 3 // Same values are Stored in different hasCodes violates Contract 1
So on...
HashCode等价契约
根据equal方法,两个相等的键应该生成相同的hashCode 生成相同hashCode的两个key不需要相等(在上面的例子中,所有偶数生成相同的hashCode)
其他回答
为什么重写equals()方法
在Java中,我们不能重载==、+=、-+等操作符的行为。他们的行为是特定的。让我们关注一下这里的运算符==。
operator ==如何工作。
它检查我们比较的两个引用是否指向内存中的同一个实例。只有当这两个引用表示内存中的同一个实例时,Operator ==才会解析为true。
现在让我们考虑下面的例子
public class Person {
private Integer age;
private String name;
..getters, setters, constructors
}
假设在你的程序中,你在不同的地方建立了2个Person对象,你希望比较它们。
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1 == person2 ); --> will print false!
这两个对象从商业角度看是一样的,对吧?对于JVM,它们是不一样的。因为它们都是用new关键字创建的,所以这些实例位于内存中的不同段中。因此运算符==将返回false
但是如果我们不能重写==操作符,我们怎么能对JVM说我们希望这两个对象被视为相同的。这里出现了.equals()方法。
您可以重写equals()来检查某些对象是否具有相同的值,以便将特定字段视为相等。
您可以选择要比较的字段。如果我们说2个Person对象当且仅当它们具有相同的年龄和相同的名称时是相同的,那么IDE将为自动生成equals()创建如下内容
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
name.equals(person.name);
}
让我们回到前面的例子
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1 == person2 ); --> will print false!
System.out.println ( person1.equals(person2) ); --> will print true!
所以我们不能重载==运算符来以我们想要的方式比较对象,但是Java给了我们另一种方法,equals()方法,我们可以随心所欲地重写它。
但是请记住,如果我们没有在我们的类中提供.equals()的自定义版本(也就是重写),那么Object类和==操作符中预定义的.equals()将表现完全相同。
从Object继承的默认equals()方法将检查两个比较实例在内存中是否相同!
为什么重写hashCode()方法
java中的一些数据结构(如HashSet, HashMap)基于应用于这些元素上的哈希函数来存储它们的元素。哈希函数是hashCode()
如果我们可以选择重写.equals()方法,那么我们也必须选择重写hashCode()方法。这是有原因的。
继承自Object的hashCode()的默认实现认为内存中的所有对象都是唯一的!
让我们回到哈希数据结构。对于这些数据结构有一个规则。
HashSet不能包含重复的值,HashMap不能包含重复的键
HashSet是在幕后使用HashMap实现的,HashSet的每个值都存储为HashMap中的一个键。
所以我们必须理解HashMap是如何工作的。
简单地说,HashMap是一个具有一些桶的原生数组。每个桶都有一个linkedList。在那个linkedList中存储了我们的键。HashMap通过应用hashCode()方法为每个键定位正确的linkedList,然后它遍历该linkedList中的所有元素,并对每个元素应用equals()方法,以检查该元素是否已经包含在其中。不允许重复密钥。
当我们在HashMap中放东西时,键就存储在其中一个linkedlist中。该键将存储在哪个linkedList中,由该键上的hashCode()方法的结果显示。因此,如果key1. hashcode()的结果是4,那么key1将存储在数组的第4个桶中,在那里存在的linkedList中。
默认情况下,hashCode()方法为每个不同的实例返回不同的结果。如果我们有默认的equals(),它的行为类似于==,它将内存中的所有实例视为不同的对象,我们就没有任何问题。
但在前面的示例中,我们说过,如果Person实例的年龄和名字匹配,则认为Person实例是相等的。
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1.equals(person2) ); --> will print true!
现在让我们创建一个映射,将这些实例存储为键,并使用一些字符串作为pair值
Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");
在Person类中,我们没有重写hashCode方法,但我们重写了equals方法。由于默认的hashCode为不同的java实例提供不同的结果,person1.hashCode()和person2.hashCode()很有可能得到不同的结果。
我们的映射可能以这些人在不同的链表中结束。
这违背了HashMap的逻辑
一个HashMap不允许有多个相等的键!
但是我们现在有了,原因是从对象类继承的默认hashCode()是不够的。在Person类上重写equals()方法之后就不会了。
这就是为什么我们必须在重写equals方法之后重写hashCode()方法的原因。
现在我们来解决这个问题。让我们重写hashCode()方法来考虑equals()考虑的相同字段,即年龄、姓名
public class Person {
private Integer age;
private String name;
..getters, setters, constructors
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
name.equals(person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
现在让我们再次尝试将这些键保存到HashMap中
Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");
person1.hashCode()和person2.hashCode()肯定是相同的。假设它是0。
HashMap将进入0桶,在LinkedList中将person1保存为值为“1”的键。对于第二次放置HashMap是足够智能的,当它再次到bucket 0保存值为“2”的person2 key时,它将看到另一个相等的key已经存在于那里。它会覆盖之前的键。最后HashMap中只有person2键。
现在我们与哈希映射规则保持一致,该规则说不允许有多个相等的键!
Bah -“你必须在每个重写equals()的类中重写hashCode()。”
[出自Joshua Bloch的《Effective Java》?]
Isn't this the wrong way round? Overriding hashCode likely implies you're writing a hash-key class, but overriding equals certainly does not. There are many classes that are not used as hash-keys, but do want a logical-equality-testing method for some other reason. If you choose "equals" for it, you may then be mandated to write a hashCode implementation by overzealous application of this rule. All that achieves is adding untested code in the codebase, an evil waiting to trip someone up in the future. Also writing code you don't need is anti-agile. It's just wrong (and an ide generated one will probably be incompatible with your hand-crafted equals).
他们肯定应该在被写来用作键的对象上强制设置一个接口吗?无论如何,Object永远不应该提供默认的hashCode()和equals() imho。它可能鼓励了许多破碎的散列集合。
但无论如何,我认为“规则”是前后颠倒的。与此同时,我将继续避免使用“等号”进行相等性测试方法:-(
在这个回答中没有提到测试equals/hashcode契约。
我发现EqualsVerifier库非常有用和全面。它也很容易使用。
另外,从头构建equals()和hashCode()方法涉及大量样板代码。Apache Commons Lang库提供了EqualsBuilder和HashCodeBuilder类。这些类极大地简化了复杂类的equals()和hashCode()方法的实现。
顺便说一句,值得考虑重写toString()方法以帮助调试。Apache Commons Lang库提供了ToStringBuilder类来帮助实现这一点。
public class Employee {
private int empId;
private String empName;
public Employee(int empId, String empName) {
super();
this.empId = empId;
this.empName = empName;
}
public int getEmpId() {
return empId;
}
public void setEmpId(int empId) {
this.empId = empId;
}
public String getEmpName() {
return empName;
}
public void setEmpName(String empName) {
this.empName = empName;
}
@Override
public String toString() {
return "Employee [empId=" + empId + ", empName=" + empName + "]";
}
@Override
public int hashCode() {
return empId + empName.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(this instanceof Employee)) {
return false;
}
Employee emp = (Employee) obj;
return this.getEmpId() == emp.getEmpId() && this.getEmpName().equals(emp.getEmpName());
}
}
测试类
public class Test {
public static void main(String[] args) {
Employee emp1 = new Employee(101,"Manash");
Employee emp2 = new Employee(101,"Manash");
Employee emp3 = new Employee(103,"Ranjan");
System.out.println(emp1.hashCode());
System.out.println(emp2.hashCode());
System.out.println(emp1.equals(emp2));
System.out.println(emp1.equals(emp3));
}
}
在对象类中,equals(Object obj)用于比较地址比较,这就是为什么在Test类中,如果你比较两个对象,则equals method给出false,但当我们重写hashcode()时,它可以比较内容并给出正确的结果。
String类和包装器类的equals()和hashCode()方法实现与Object类不同。Object类的equals()方法比较对象的引用,而不是内容。Object类的hashCode()方法为每个对象返回不同的hashCode,无论内容是否相同。
It leads problem when you use Map collection and the key is of Persistent type, StringBuffer/builder type. Since they don't override equals() and hashCode() unlike String class, equals() will return false when you compare two different objects even though both have same contents. It will make the hashMap storing same content keys. Storing same content keys means it is violating the rule of Map because Map doesnt allow duplicate keys at all. Therefore you override equals() as well as hashCode() methods in your class and provide the implementation(IDE can generate these methods) so that they work same as String's equals() and hashCode() and prevent same content keys.
你必须重写hashCode()方法和equals(),因为equals()根据hashCode工作。
此外,与equals()一起重写hashCode()方法有助于完好无损equals()-hashCode()契约:“如果两个对象相等,那么它们必须具有相同的哈希码。”
什么时候需要为hashCode()编写自定义实现?
正如我们所知,HashMap的内部工作是基于哈希原理的。条目集存储在特定的存储桶中。您可以根据自己的需求定制hashCode()实现,以便相同的类别对象可以存储在相同的索引中。 当你使用put(k,v)方法将值存储到Map集合中时,put()的内部实现是:
put(k, v){
hash(k);
index=hash & (n-1);
}
意思是,它生成索引,索引是基于特定键对象的hashcode生成的。所以让这个方法根据你的需求生成哈希码,因为相同的哈希码条目集将存储在相同的桶或索引中。
就是这样!