接下来继续讲第二章,第8-12条。
第8条:覆盖equals时请遵守通用约定
equals 时Object类的一个非final方法,一般是表示类的实例对象是否相同,也就是对象的地址是否相等。
但是某些时候却要重写Object.equals方法。即类需要有“逻辑相等”,也就是值类,这都需要重写equals方法。这样这个类的实例可以用做Map的key中。有一种值类就不需要重写equals,就是单例模式的类,至始至终也就一个对象。
在覆盖equals时需要遵守的几个约定:自反性、对称性、传递性、一致性、非null。
第9条:覆盖equals时总要覆盖hashode
每个重写类equals方法的类都必须要重写hashCode方法,不然在改类将不能和基于散列的集合【HashMap、HashSet】一起正常使用。对象的根据equals方法判断时相等的,其hashcode肯定也是相等的。即判断2个对象是否相等,先比较hashcode是否相等,若相等则在比较equals是否相等。
看下面的一个例子。
package com.example.demo2;
import java.util.Objects;
public class Student {
private int age;
private String name;
private int grade;
public Student(int age, String name, int grade) {
this.age = age;
this.name = name;
this.grade = grade;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student student = (Student) o;
return age == student.age &&
grade == student.grade &&
Objects.equals(name, student.name);
}
}
当把这个类和HashMap一起使用时
Map stu = new HashMap<>();
stu.put(new Student(12,"jack",99),"Jack");
期望的时stu.get(new Student(12,"jack",99)) 返回的时Jack,但是实际上返回的时null,主要的原因就是没有重写hashCode,从而导致2个对象实例有不同的hashcode值。一个好的哈希函数就是为每个不同的对象产出不同的散列码。
一般一个理想的散列函数设计方案:
1、把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中
2、对于对象只能够的每个关键字f,完成下面的步骤
a、为该域计算int类型的散列码C
b、按下面的计算公式,result = 31 * result + c
3、返回result
public int hashCode() {
int result = 17;
result = 31 * result + age;
result = 31 * result + Integer.valueOf(name);
result = 31 * result + grade;
return result;
}
但是现在的IDE已经非常的强大了,可以利用快捷键自动重写equals和hashcode方法了,但是其中的原理我们还是需要掌握的。
第10条:始终要覆盖toString
java.lang.Object也提供了toString方法的一个实现,但是它格式可读性不好,它包含类的名称,一个一个“@”符号,接着是haahcode的无符号的十六进制表示法。虽然遵守toString的约定不如遵守equals、hashcode的约定这么重要,但是提供好的toString方法可以让类的使用更加的舒适。
第11条:谨慎地覆盖clone
按照书中的话来讲,能不重写clone就不要去重写,因为它带来的问题太多了。我们暂且不讨论这里面的陷阱有多少,只从对Java基础知识的掌握程度来说明什么是clone,以及什么是“深拷贝”和“浅拷贝”。首先观察以下代码,并思考对象在内存中的分配以及引用的变化:
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class Main {
public static void main(String[] args) throws Exception{
Student stu = new Student("kevin", 23);
Student stu2 = stu;
stu2.setAge(0);
System.out.println(stu.getAge());
}
}
这是一段很简单的代码,Student对象实例stu、stu2在内存中的分配及引用分别如下图所示:
所以代码中出现修改stu2实例的age字段时,stu中的age字段也被修改了,原因很简单因为它们的引用指向的都是同一个对象实例。
那如果我们想在实例化一个name=”kevin”,age=23的Student实例怎么办呢?当然可以再写一段Student stu2 = new Student(“kevin”, 23);如果再重新构造一个对象实例很复杂,能不能直接复制呢?显然,使Student实现Cloneable接口并重写clone方法即可,注意此时的重写clone方法在里面仅有一句代码即是即调用父类的clone方法,而不是自定义实现:
上面我们仅仅是说明了什么是clone,接下来我们接着来讲解什么是“深拷贝”和“浅拷贝”。
在上面的例子Student类中,我们新增一个引用型变量Test类:
书中是不建议自定义重写clone方法的,如果非要重写书中总结为一句话:clone方法就是一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。
再说一个与本条目无关的点,查看Cloneable接口实际上可以发现里面什么方法都没有,clone方法却来自Object类,继承了Cloneable接口为什么就能重写clone方法了呢?原因在于clone方法在Object类中的修饰符是protected,而Cloneable接口和Object处于同一个包下,熟悉修饰符的都知道protected的权限限定在同一个包下或者其子类。Cloneable和Object同属于一个包,Cloneable自然能继承clone方法,继承了Cloneable接口的成为了它的子类同样也就继承了clone方法。
此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法。
实现此接口的对象列表(和数组)可以通过 Collections.sort
(和 Arrays.sort
)进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
package com.example.demo2;
import java.util.*;
public class Student implements Comparable{
private int age;
private String name;
private int grade;
@Override
public int compareTo(Student o) {
return this.grade - o.grade;
}
public Student(int age, String name, int grade) {
this.age = age;
this.name = name;
this.grade = grade;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
", grade=" + grade +
'}';
}
public static void main(String[] args) {
Student s1 = new Student(12,"kk",200);
Student s2 = new Student(15,"ff",100);
Student s3 = new Student(17,"zz",390);
List result = new ArrayList<>();
Collections.sort(result);
for(Student s : result) {
System.out.println(s.toString());
}
}
}
总结:我们可以使用Comparable接口中的compareTo方法使原本无法比较的对象通过某种自身条件来排序.
下面的方法也可以实现按grade字段排序
Collections.sort(result, new Comparator() { @Override public int compare(Student o1, Student o2) { return o1.grade - o2.grade; } });