1、覆盖equals时请遵守通用约定
1.1、不覆盖equals方法的情况:(类的每个实例都只与它自身相等)
1.1.1、类的每个实例本质上都是唯一的。例如Thread类 每个实体都有不同的编号,每个实体都是唯一的。
1.1.2、如果从object继承而来的equals方法够用,没有必要提供逻辑相等的测试功能
1.1.3、父类已经覆盖了equals方法,并且同时适用于子类
1.1.4、类是私有的或者包是私有的,可以确定它的equals方法永不会被调用
1.1.5、值类中的枚举类(实例受控类),他逻辑相同与对象等同是一个意思
1.2、equals通用规定:
1.2.1、自反性:对于任何非Null的引用值x,x.equals(x)返回必须为true;
1.2.2、对称性:对于任何非Null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true;
1.2.3、传递性:对于任何非Null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)也必须返回true;
1.2.4、一致性:对于任何非Null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致的返回true或者一致的返回false;
1.2.5、非空性:对于任何非Null的引用值x,x.equals(null)必须返回false。
1.3、使用equals的诀窍(一步步走):
1.3.1、使用==操作符检查参数是否为该对象的引用。如果是,则返回true,如果比较操作代价大,就值得这么做。
1.3.2、使用instanceof操作符检查参数是否为正确的类型。如果不是,则返回false。
1.3.3、把参数转换成正确的类型。
1.3.4、对于该类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配。
对于既不是float也不是double类型的基本类型域,可以使用==操作符进行比较;
对于对象引用域,可以递归的调用equals方法;
对于float域,可以使用静态Float.compare(float,float)方法;Float.equals每次比较会自动装箱,影响性能。
对于double域,可以使用Double.compare(double,double);Double.equals每次比较会自动装箱,影响性能。
对于数组域,如果每个元素都很重要,则采用Arrays.equals方法。
对于域的比较比等同性测试复杂的情况,保存该域的一个范式,这样就可以根据范式进行低开销的精确比较
1.4、使用equals的告诫:
1.4.1、覆盖equals时总要覆盖hashCode
1.4.2、不要企图让equals方法过去智能
1.4.3、不要将equals声明中的Object对象替换为其他的类型
1.4.4、可以使用Google开源的AutoValue框架生成代码,也可以使用IDE生成代码,但是稍微冗长一些,可读性差点,这两种方法都比手动编写好,不会犯错
2、覆盖equals时总要覆盖hashCode
2.1、原因:
在每个覆盖equals方法的类中,都必须覆盖hashCode方法,如果不就会违反hashCode约定,从而导致该类无法结合所有基于散列的集合一起正常运行(HashMap、HashSet)。相等的对象必须具有相等的散列码。
public class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
//覆盖equals方法
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof PhoneNumber))
return false;
//必须满足如下条件,才能说明为同一个对象
PhoneNumber pn = (PhoneNumber) obj;
return pn.areaCode == areaCode && pn.prefix == prefix && pn.lineNumber == lineNumber;
}
public static void main(String[] args){
Map<PhoneNumber, String> m = new HashMap<>();
//创建两个相同的对象
PhoneNumber p1 = new PhoneNumber(707, 867, 5309);
PhoneNumber p2 = new PhoneNumber(707, 867, 5309);
//添加到hashmap中
m.put(p1, "Jenny");
//比较对象p1和p2
System.out.println("p1.equals(p2): " + p1.equals(p2));
System.out.println("p2.equals(p1): " + p2.equals(p1));
//从hashmap中去获取对象p1和p2
System.out.println("get p1 from hashmap: " + m.get(p1));
System.out.println("get p2 from hashmap: " + m.get(p2));
}
}
输出结果:
p1.equals(p2): true
p2.equals(p1): true
get p1 from hashmap: Jenny
get p2 from hashmap: null
因为PhoneNumber类没有覆盖hashcode方法,从而导致两个相等的实例具有不相等的散列码。
2.2、覆盖hashCode的两种方法:
2.2.1、重写一个hashCode方法,直接返回一个固定值。可行但是性能差,因为虽然保证了每个对象都具有同样的散列码,但每个对象都映射到同一个散列桶中,使得散列表退化为链表。
@Override
public int hashCode() {
return 42;
}
2.2.2、比较好的散列函数算法:
①、为对象计算int类型的散列码c:
对于boolean类型,计算(f?1:0)
对于byte,char,short,int类型,则计算(int)f
对于long类型,计算(int)(f^(f>>>32))
对于float类型,计算Float.floatToIntBits(f)
对于Double类型,计算Double.doubleToLongBits(f),然后再按照long类型处理
对于对象引用,equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashcode
对于数组,则把每一个元素当作单独的域来处理。
②、将获取到的c合并:result = 31 * result +c;
③、返回result
private volatile static int hashcode;
@Override
public int hashCode() {
int result = hashcode;
if (result == 0){
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
hashcode = result;
}
return result;
}
2.3、覆盖hashCode的忠告
2.3.1、不要试图从散列码计算中排除掉一个对象的关键域来提高性能,因为可能导致散列表漫道无法使用
2.3.2、不要对hashCode方法的返回值做出具体规定,因此客户端无法理所当然的依赖它,可以为修改提供灵活性
3、始终要覆盖toString
3.1、原因
3.1.1、提供好的toString实现可使类用起来更加舒适,使用了这个类的系统更易于调数。建议所有的子类都覆盖这个方法,来使信息更加简洁并易于阅读易于调试。
3.2、覆盖toString规范
3.2.1、toString方法应该返回对象中包含的所有值得关注的信息
3.2.2、无论是否决定指定格式,都应该在文档中明确的表明你的意图
3.2.3、无论是否指定格式,都为toString返回值中包含的所有信息提供一种可以通过编程访问之的途径
4、谨慎的覆盖clone
4.1、clone概述
一个类要想实现克隆,需要实现Cloneable接口,表明这个类的对象具有克隆的功能。Cloneable接口是一个mixin接口,它里面并没有任何的抽象方法,类似的接口有Serializable接口,表明该类的对象可以序列化。首先应该明确通过一个对象克隆出另一个对象的概念:通过一个对象克隆出另一个对象,简称对象1和对象2,这两个对象就成为两个独立的对象,那么对对象1做任何的修改,对象2应该不受影响;对对象2做任何的修改的,对象1就不应该受影响。这样就要求对象1与对象2的实例域各自是独立的。
4.2、覆盖clone忠告
所有实现了Cloneable接口的类都应该有一个公有的方法覆盖从Object类继承而来的clone()方法,此公有方法先调用super.clone(),得到一个“浅拷贝”的对象,然后根据原对象的实例域的情况,修正任何需要修正的域。一般情况下,这意味这要拷贝任何包含内部“深层结构”的可变对象。并将新对象的引用代替原来指向这些对象的引用。虽然,这些内部拷贝操作往往可以通过层层递进的调用clone()来完成,但这通常不是最佳的方法。如果该类只包含基本类型的域,和不可变类型的引用域,那么多半情况下是没有域需要修正的。
5、考虑实现Comparable接口(唯一的方法compareTo)
5.1、使用情景:
编写一个值类,它具有非常明显的内在排序关系,比如按字母顺序、按数值排序或者按年代排序,那就应该考虑实现Comparable接口
5.2、实现Comparable忠告:
5.2.1、compareTo不能跨越不同类型的对象进行比较,否则报ClassCastException异常
5.2.2、无法在用新的值组件扩展可实例化的类时,同时保持compareTo约定,除非愿意放弃面向对象的抽象优势
5.2.3、如果想成为一个实现了Comparable接口的类增加值组件,请不要扩展这个类,而是要编写一个不相关的类,其中包含第一个类的一个实例,然后提供一个视图方法返回这个实例,这样即可以让你自由的在第二个类上实现compareTo方法,同时也允许它的客户端在必要的时候把第二类的实例视为第一个类的实例
5.2.4、在java7,java的所有装箱基本类型的类中增加了静态的compare方法,在compareTo方法中使用关系操作符<和>是非常繁琐的,并且容易出错,因此不建议使用。
5.2.5、每当实现一个对排序敏感的类时,都应该让这个类实现Comparable接口,以便其实例可以轻松的被分类、搜索,以及用在基于比较的集合中。每当在compareTo方法的实现中比较域值时,都要避免使用<和>操作符,而应该在装箱基本类型的类中使用静态compare方法,或者在Comparable接口中使用比较器构造方法。