Set接口
Set集合Java集合框架中的非常重要的组成部分。Set集合不能存重复的元素。这里的重复是根据该对象所属的类的equals方法和hashCode方法来判断的。
只有当两个对象的hashcode相同,且equals方法返回true时,才断定两个对象相等。
代码验证:
import java.util.Objects;
/**
* @auther plg
* @date 2019/4/8 21:08
*/
public class Person {
private static int num = 0;
private String name;
private Integer age;
public Person(){}
public Person(String name,Integer age){
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name) &&
Objects.equals(age, person.age);
}
@Override
public int hashCode() {
return num++;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
这样覆写的hashCode方法,每个对象的hashcode都是在它的前一个对象的基础上加一,因此这样创建的多个对象的hashcode是不会相等的。
测试代码:
public static void testHashSet(){
HashSet<Person> set = new HashSet<>();
set.add(new Person("Javk",18));
set.add(new Person("Javk",18));
set.add(new Person("Javk",18));
System.out.println(set);
}
运行结果:
可以看到三个元素都存进了Set集合中。
这便是因为虽然这三个对象的值相等,即调用equals方法返回的结果是true,但hashcode不相同,因此认为它们三个是不同的对象,所有可以存进HashSet中。
覆写equals和hashCode方法的要求
1.equals方法判断相等,则hashCode必须相等。
2.equals方法判断不相等,hashCode尽量不相等(否则造成hash碰撞,但确实存在相等的情况,这是由散列函数的性质而产生的)。
3.同一个对象调用多次hashCode方法都应产生同样的结果。
经典面试题1:覆写equals方法为什么一定要覆写hashCode方法
首先说一下为什么要覆写equals方法?equals方法和hashCode方法都是Object类的方法,而equals方法默认的比较的是两个对象的地址是否相等。
方法源码:
public boolean equals(Object obj) {
return (this == obj);
}
但在实际开发中,通常对对象的地址是不太关心的,而更关心的是对象的内容。因此这才有必要覆写equals方法。
如果只重写equals方法而没有覆写hashCode方法,则默认使用的是Object类中的hashCode方法,该方法是一个本地方法,是根据对象的地址来算hash值的。两个new的对象即使它们的内容相同,它们的地址也必然是不同的。
这便违反了上述的第一条规则:equals相同,hashCode也必须相同。
因此当两个对象的内容相同而地址不同,仍被认为是两个不同得对象。因此在这种情况下,去HashSet或者HashMap中查找一个对象是否存在时,即使该集合中已经存放着该元素,但还是会返回false,因为这两个对象的地址是不相等的。这便是覆写equals方法为什么一定要覆写hashCode方法的原因。
HashSet与TreeSet的异同
相同点:
- 都是Set接口的实现类,都不能存放重复的值。
- HashSet和TreeSet都是异步的非线程安全的,在遍历时,若改变元素的值会产生
fail-fast(并发修改异常)。
不同点:
- HashSet底层是HashMap,而TreeSet底层是TreeMap。
- HashSet中可以存放null值,而TreeSet中不能存放null值。
- HashSet中元素无序存储(但非随机存放的,由hash值而定),而TreeSet中的元素是有序存放的。
TreeSet排序分析
对于TreeSet可以实现有序存储,也没有那么神秘。只是与两个接口有关系而已,而对于String,Integer等类,都实现了Comparable接口,并覆写了compareTo方法。
- Comparable接口:实现内部排序。
- Comparator接口:实现外部排序。
详情请参考
Java 学习总结—比较器。
而对于实现了比较器的TreeSet而言,就与equals方法和hashCode方法没有关系,覆写或者不覆写都无关紧要,因为此时判断两个对象相等是根据比较器中的方法是否返回0而定的,返回0,则表示两个对象相等,否则就是不想等。
代码演示:
自定义类,采用外部排序,因此不必实现Comparable接口
public class Person {
private static int num = 0;
private String name;
private Integer age;
public Person(){}
public Person(String name,Integer age){
this.age = age;
this.name = name;
}
public static int getNum() {
return num;
}
public static void setNum(int num) {
Person.num = num;
}
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 "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试代码:构造方法中通过Lambda表达式传入比较器
public static void testTreeSet(){
TreeSet<Person> set = new TreeSet<>((p1,p2)->{return p1.getAge() - p2.getAge();});
set.add(new Person("Jack",20));
set.add(new Person("Lucy",18));
set.add(new Person("Tom",22));
set.add(new Person("Danny",20));
System.out.println(set);
System.out.println(set.contains(new Person("Jack",20)));
}
运行结果:
此时出现很滑稽的一幕,只要两个Person对象的年龄相同,就当作是同一个对象,这是由自定义的比较器而定的。也没有啥奇怪的。
总结
TreeSet的元素相同判断是根据比较器中自定义的方法而定的。而不再是equals和hashCode方法。