HashSet储存对象时重写equals和hashcode方法时的情景分析
Set 接口的特点:
-
1.它不允许出现重复元素-----------无重复
-
2.不保证集合中元素的顺序---------无序
-
3.允许包含值为null的元素,但最多只能有一个null元素。
hashCode() 与 equals()
HashSet集合,采用哈希表结构存储数据,保证元素唯一性的方式依赖于:hashCode()与equals()方法。
- hashCode() 方法:
Returns a hash code value for the object. This method issupported for the benefit of hash tables such as those provided by java.util.HashMap.
当向hash集合(诸如HashSet、HashMap等)中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的hashCode值,然后根据 hashCode值 决定该对象在 HashSet 中的存储位置。
- equals() 方法:
true if this object is the same as the objargument; false otherwise
两个元素通过equals()方法比较内容1是否相同。
hash集合判断标准
两个对象通过equals() 方法比较相等,并且两个对象的hashCode() 方法返回值也相等
hashset不能为一样的,放入一个值首先判断hashcode(内存中的位置)是否已经存在,然后用equals判断是否有一样的值。
关于hash集合:如果两个元素通过equals()方法比较返回true,但是这两个元素的hashcode()返回方法不等,hashset会把他们存储在不同的位置,依然可以添加成功。这也就是为什么“当把对象存储在集合中时,需要重写该类的equals()方法与hashCode()方法的原因”
两种情形
情形一:当我们往HashSet集合中添加 8大基本类型和String类型的时候,不需要重写hashCode()和equals()方法。因为任何对象都是Object类的子类,所以任何对象都拥有这个方法。
情形二:当我们往HashSet集合添加自定义对象的时候,就需要重写 hashCode() 和 equals() 方法。建立自己的比较方式,才能保证HashSet集合中的对象唯一。
情形一示例:
HashSet 集合中添加 8大基本类型和String类型的时候,不需要重写hashCode() 和equals() 方法
package Packger;
import java.util.ArrayList;
public class TestHashSet {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(12);
arrayList.add(10);
arrayList.add(35);
arrayList.add(100);
System.out.println("元素为:");
System.out.println(arrayList);
}
}
输出结果为:
元素为:
[12, 10, 35, 100]
情形二示例:
情形二: HashSet集合添加自定义对象的时候,就需要重写 hashCode() 和 equals() 方法。
- 自定义对象(不进行重写)
Student01.java
package container;
/**
* Created by Haoweixl on 2021/1/31.
*/
public class Student01 {
private String number;//学号
private String name; //姓名
private Integer age; //年龄
public Student01(String number, String name, Integer age) {
super();
this.number = number;
this.name = name;
this.age = age;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student01 [number=" + number + ", name=" + name + ", age=" + age + "]";
}
}
- 测试一:测试自定义对象(不进行重写):
Test01.java
package container;
/**
* Created by Haoweixl on 2021/1/31.
*/
import java.util.HashSet;
public class Test01 {
public static void main(String[] args) {
// TODO Auto-generated method stub
HashSet<Student01> set = new HashSet<>();
Student01 student1 = new Student01("123", "name1", 11);
Student01 student2 = new Student01("123", "name1", 11);
set.add(student1);
set.add(student2);
System.out.println("Hashset 元素:"+set);// 默认调用 toString()方法
System.out.println("集合元素总数:"+set.size());
}
}
输出结果:
Hashset 元素:[Student01 [number=123, name=name1, age=11], Student01 [number=123, name=name1, age=11]]
集合元素总数:2
- 重写 hashCode() 与 equals() 的自定义对象 (使用eclipse自动重写)
Set 集合有去重的功能,但是在向 Set 集合中添加自定义的对象时无法去重,我们重写一下 Student 类的 equals() 和 hashCode() 方法(此处的 equals() 和 hashCode() 使用 eclipse 自动生成的)。
Student02.java
package container;
public class Student02 {
private String number;//学号
private String name; //姓名
private Integer age; //年龄
public Student02(String number, String name, Integer age) {
super();
this.number = number;
this.name = name;
this.age = age;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public int hashCode() {
final int prime = 31;//获取学号、姓名、年龄的 hashCode(即所在hash表的索引) 分别乘以 31
int result = 1;
result = prime * result + ((age == null) ? 0 : age.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((number == null) ? 0 : number.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) //判断传入的是否为同一个对象
return true;
if (obj == null)//判断传入的对象是否为空
return false;
if (getClass() != obj.getClass())//判断传入的对象是不是相同的类
return false;
Student02 other = (Student02) obj;//强转
if (age == null) {//判断年龄是否相同
if (other.age != null)
return false;
} else if (!age.equals(other.age))
return false;
if (name == null) { //判断姓名是否相同
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (number == null) {//判断学号是否相同
if (other.number != null)
return false;
} else if (!number.equals(other.number))
return false;
return true;
}
@Override
public String toString() {
return "Student02 [number=" + number + ", name=" + name + ", age=" + age + "]";
}
}
- 测试二:测试重写后的自定义对象
Test02.java:
package container;
/**
* Created by Haoweixl on 2021/1/31.
*/
import java.util.HashSet;
public class Test02 {
public static void main(String[] args) {
// TODO Auto-generated method stub
HashSet<Student02> set2 = new HashSet<>();
Student02 student3 = new Student02("123", "name1", 11);
Student02 student4 = new Student02("123", "name1", 11);
set2.add(student3);
set2.add(student4);
System.out.println("Hashset 元素:"+set2);// 默认调用 toString()方法
System.out.println("集合元素总数:"+set2.size());
}
}
显示结果:
Hashset 元素:[Student02 [number=123, name=name1, age=11]]
集合元素总数:1
HashSet 源码分析
HashSet的 add 方法实际上调用的是 HashMap 的put 方法,我们来看看 HashMap 的 put 方法。
HashMap put 方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
HashMap hash方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
调用 put 方法时,会调用相应的自定义对象的 hashCode 方法,来获取该对象在 Node 数组中的坐标。因此我们在 User 对象中重写 hashCode 方法是确保相同的对象在同一个 Node 数组中会有相同的坐标。
在储存对象时当发生冲突的时候会经过下面一段代码:
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
这段代码首先比较两个对象是不是同一个对象,然后通过 equals 方法判断对象的属性值是否相同,来确认是否为同一个对象。
看看下面的测试用例:
Test03:
import java.util.HashSet;
/**
* Created by MGL on 2017/5/4.
*/
public class Test03 {
public static void main(String[] args) {
HashSet<User> set = new HashSet<>();
User user1 = new User("123", "zhangsan", 11);
set.add(user1);
set.add(user1);
System.out.println(set.size());
}
}
其实这两段代码的运行结果是一样的,但是它们的内部运行原理是不相同的,在 Test02 中向 Set 集合中添加了两个不同对象,两个对象的属性值相同,在 Test03 中也是向 Set 集合中添加了两个对象,但是是同一个对象。到底有什么不同,我们在 User 类的 equals 方法出打个断点,调试一下,我们可以发现一个有趣的现象,Test02 可以调用到 equals 而 Test03 不会调用到 equals 这个方法,这到底发生了什么???
其实问题的关键还是在于这一行代码:
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
当我们储存同一个对象的时候 (k = p.key) == key 计算结果为 true ,运用双或 的特点,直接就返回了 true ,而 Test03 不是传入的同一个对象,因此会调用 key.equals(k) 这段代码,调用自定义类的 equals 方法。
总结
1.我们在使用 HashSet 对自定义类进行去重的时候,一定要覆盖自定义类的 equals 和 hashCode 方法,hashCode 方法是找到当前对象在 Node 数组重的位置,而 equals 是比较当前对象与对应坐标链表中的对象是否相同。
2.在使用Set对象储存自定义对象的时候,每次都会调用自定义对象的 hashCode 方法,但是 equals 方法并不是每次都会被调用到,不会被调用到的情形有下:
- 传入同一个对象;
- 当前坐标为空;
3.要根据自己要实现的功能,合理的重写 hashCode 和 equals 方法来达到去重的目的。
参考博客: