第十条:覆盖equals时请遵守通用的约定
类具有特有的逻辑相等的概念,且超类没有覆盖equals方法时应该覆盖equals方法,例如integer、String这种“值类”。
但是有一种值值类无需覆盖equals,即实例受控,每个值最多只存在一个对象的类,比如枚举类,这种类逻辑相同和对象相同是同一回事,所以Object的equals方法等她与逻辑的equals。
equals必须满足四种等价关系:自反性、对称性、传递性、一致性,并且对于非null的x,x.equals(null)永远返回false.
对称性:
CaseString类equals方法比较不区分大小写的字符串,问题在与CaseString的equals方法知道普通类型的字符串,但是String的equals方法并不知道CaseString,比较时违背了对称性。
public class EqualsDemo { public static void main(String[] args) { CaseString caseString = new CaseString("Case"); String string = "case"; System.out.println(caseString.equals(string)); System.out.println(string.equals(caseString)); } } final class CaseString { private final String s; public CaseString(String s) { this.s = Objects.requireNonNull(s); } @Override public boolean equals(Object obj) { if (obj instanceof CaseString) { return s.equalsIgnoreCase(((CaseString) obj).s); } if (obj instanceof String) { return s.equalsIgnoreCase((String) obj); } return false; } }
解决方式:equals方法中去除与String类型比较的部分。
@Override public boolean equals(Object obj) { return obj instanceof CaseString && s.equalsIgnoreCase(((CaseString) obj).s); }
传递性:
子类增加的信息会影响equals的比较结果,我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定。但是可以在抽象类的子类增加新的组件且不违反equals的规定。
一致性:
如果两个对象相等,他们就要一直保持相等,所以不要使equals方法依赖不可靠的资源。
非空性:
指所有的对象都不能等于null,大多数情况下一个非空对象equals null,都会返回false(返回true的情况我还没有想到),很多类的equals方法中为了避免抛出NPE,会先判读入参是否为null,其实这是没有必要的,因为equals方法在比较之前,必须是呀instanceof判断Object类型的入参是否为比较类型子类的对象,如果instanceof的第一个操作数是null,那么不管第二个操作数是什么类型,都会返回false,所以不需要显式的null检查。
ps:
1.对于非float和double类型的基本数据类型,可以使用==比较,对于float和double类型,可以使用Float.compare(x,y)和Double.compare(x,y),因为存在Float.NAN,-0.0f之类的常量,如果使用Float.equals()或Double.equals()会对基本类型进行装箱操作,降低性能.
2.不要将equals方法参数中Object对象替换成其他类型的对象,如果替换的话,它将不会重写Object.equals()而是重载。
第11条:覆盖equals时总要覆盖hashCode
在每个覆盖了equals方法的类中都要覆盖hashCode方法,否则这种类对象元素的散列集合(如HashMap、HashSet)将无法正常运行。
例:
public class EqualsDemo {
public static void main(String[] args) {
CaseString caseString = new CaseString("Case");
String string = "case";
CaseString caseString1 = new CaseString("Case");
System.out.println(caseString.equals(caseString1));
HashMap<CaseString, Integer> map = new HashMap<>();
map.put(caseString, 1);
map.put(caseString1, 2);
System.out.println(map);
CaseString caseString3 = new CaseString("test");
CaseString caseString4 = new CaseString("test");
map.put(caseString3, 3);
System.out.println(map.get(caseString4));
}
}
final class CaseString {
private final String s;
public CaseString(String s) {
this.s = Objects.requireNonNull(s);
}
@Override
public boolean equals(Object obj) {
return obj instanceof CaseString && s.equalsIgnoreCase(((CaseString) obj).s);
}
}
输出:
equals和HashCode比较规则:
equals相等->hashCode一定相等
hashCode相等->equals不一定相等
hashCode不等->equals一定不相等
所以在比较时,会优先比较hashCode,hashCode相等后再比较equals,所以不同的对象生成不同的hash值,会提高比较的性能。
第12条:始终要覆盖toString方法
返回值的关注的信息,易于调试。
第13条:谨慎地覆盖clone
Cloneable接口是一个标记接口,表示实现了这个接口的类可以被克隆,object的clone方法返回该对象的逐级拷贝,否则抛出CloneNotSupportedException异常。实现cloneable接口的类是为了提供一个功能适当的公有clone方法,clone方法无需调用构造器就可以创建对象。
clone方法约定:
1. x.clone != x
2. x.clone.getClass == x.getClass
3. x.clone.equals(x) == true
对于不可变的类,永远不应该提供clone方法,因为会激发不必要的克隆。
clone方法相当于另一个构造器;必须确保原始的对象和克隆的对象不会互相伤害。如果对象包含的域中,引用了可变的对象,并且只实现简单的clone,会导致修改原始的实例会破坏克隆对象中的约束条件。
public class CloneDemo { public static void main(String[] args) throws CloneNotSupportedException { Clo clo = new Clo(); System.out.println(Arrays.toString(clo.getArray())); Clo cloCopy = clo.clone(); System.out.println(Arrays.toString(cloCopy.getArray())); clo.getArray()[0] = 9; System.out.println(Arrays.toString(cloCopy.getArray())); } } class Clo implements Cloneable { private int[] array; public Clo() { this.array = new int[]{1, 2, 3}; } public int[] getArray() { return array; } @Override protected Clo clone() throws CloneNotSupportedException { return (Clo) super.clone(); } }
输出:
可以看到修改原始对象数组的值,同时影响了克隆后对象的数组的值,为了使clone方法正常工作,应递归的调用clone,clone方法修改为如下:
@Override protected Clo clone() throws CloneNotSupportedException { Clo clo = (Clo) super.clone(); clo.array = array.clone(); return clo; }
输出:
ps:要注意,如果array的类型是final的上述方法无法正常工作,因为clone方法是禁止给final域赋新值的。
克隆负责对象的最后方法是,先调用super.clone,然后爸结果对象中的所有域设为初始状态,再调用高层方法重新赋值,同时要注意,不要在clone方法的构造过程中,调用被子类覆盖的方法,如果调用了在子类中覆盖的方法,那么在该方法所在的子类有机会修正它在克隆对象中的状态之前,该方法就会先被执行,可能会导致克隆对象和原始对象的不一致。
线程安全的类的clone方法也必须做到同步。
对象拷贝的更好的方法是实现一个拷贝构造器或拷贝工厂:
public Yum(Yum yum){ ... }
public static Yum newInstance (Yum yum){ ... }
第14条:考虑实现Comparable接口
对排序敏感的类可以kaol实现Compareable接口
compareTo约定:
- 确保所有的x,y都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
- 比较关心可传递:x.compareTo(y) > 0,y.compareTo(z) > 0 --> x.compareTo(z) > 0
- 确保x.compareTo(y) == 0 --> sgn(x.compareTo(z)) == sgn(y.compareTo(z))
- 强烈建议 (x.compareTo(y) == 0) == (x.equals(y)),如果遵守这一条,那么compareTo方法所施加的顺序关系就被认为与equlas一致。