对于所有对象都通用的方法(二)

时间:2021-05-04 16:03:54
以下为我在《Effective Java》中留下的读书笔记,对一些重要的知识点提取出了摘要.
8、覆盖equals时请遵守通用约定
无需覆盖equals的几种情况:    1.类的每个实例本质上都是唯一的.换句话说,就是Object提供的equals实现对于这些类来说是正确的行为.    2.不关心类是否提供了”逻辑相等“的测试功能.换句话说,就是客户端不期望调用equals方法.    3.已在超类中覆盖equals方法.换句话说,就是超类中的equals方法已经满足子类逻辑相等判断的需求.    4.equals方法永远不会调用(在equals方法中覆盖抛出AssertionError)
JavaSE6通用约定规范:    1.自反性.对于任何非null的引用值x,x.equals(x)必须返回true。    2.对称性.对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。    3.传递性.对于任何非null的引用值x、y、z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。    4.一致性.对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false.    5.x.equals(null)必须返回false

对称性问题通常出现在没有继承关系的不同类型上. 传递性问题通常出现在有继承关系的类型且增加新的值组件之上.权宜之计可以用复合代替继承,从而解决超类对子类的equals方法返回true的问题. 一致性问题,不要使equals方法依赖于不可靠的资源.可能会导致不一致性问题.
equals正确用法:    1.使用==操作符检查     2.使用instanceof检查参数是否为正确的类型(同时保证了非空性)    3.把参数转换成正确的类型    4.对于该类中的每个”关键“域,检查参数中的域是否与该对象中对应的域相匹配       一些原则:         a.对于不可变类,可能存在用于比较的保留范式         b.先比较最有可能不一致的域和开销最小的域         c.不要比较不属于对象逻辑状态的域         d.一些情况下可以选择先比较逻辑冗余域(如果冗余域代表了整个对象的综合描述)      一些方法:         a.对于既不是float也不是double类型的基本类型域,可以用==操作符进行比较;         b.对于对象引用域,可以递归地调用equals方法;         c.对于float域使用Float.compare方法,对于double域使用Double.compare方法;         d.对于数组域,可把以上方法应用到每个元素上,如果数组域中每个元素都作为比较域,那么可以使用Arrays.equals方法.    5.检查对称性、传递性、一致性(编写能生成检测equals方法对称性、传递性、一致性代码的单元测试代码)
补充:    AtomicInteger作为static final属性 记录创建了多少实例,在构造器中AtomicInteger.incrementAndGet();    不要把java.sql.Timestamp和java.util.Date混合使用在同一集合中,违反了对称性.    o instanceof XXX时,如果o为null,返回false.    Arrays.equals方法,用于比较数组域中的每个元素    习惯用法比较:       (field == null ? o.field == null : field.equals(o.field))        (field == o.field || (field != null && field.equals(o.field)))
疑惑:getClass测试与instanceof测试的区别 : instanceof 相比getClass还有超类兼容性,比较不同如下  
class A { }  

class B extends A { }

Object o1 = new A();
Object o2 = new B();

o1 instanceof A => true
o1 instanceof B => false
o2 instanceof A => true // <================ HERE
o2 instanceof B => true

o1.getClass().equals(A.class) => true
o1.getClass().equals(B.class) => false
o2.getClass().equals(A.class) => false // <===============HERE
o2.getClass().equals(B.class) => true
          实践:编写能生成检测equals方法对称性、传递性、一致性代码的单元测试代码.
9、覆盖equals时总要覆盖hashCode
如果不这么做,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常工作,这样的集合包括HashMap、HashSet和HashTable
HashMap有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也不必检验对象的等同性. 散列码计算开销较大时,可以考虑将散列码缓存在对象内部.同时,还可以根据散列调度频率决定是否延迟初始化散列码.
计算hashCode值的一个相当好的散列函数:    1.把某个非零的常数值,比如说17,作为初始值,保存在名为result的int类型变量中.    2.对于对象中每个关键域f(equals方法中涉及的每个域),完成以下步骤:       a.为该域计算int类型的散列码c:         i.如果该域是boolean类型,则计算(f?1:0).         ii.如果该域是byte、char、short或者int类型,则计算(int)f.         iii.如果该域是long类型,则计算(int)(f^f(>>>32)).         iv.如果该域是float类型,则计算Float.floatToIntBits(f).         v.如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2.a.iii,为得到的long类型值计算散列值.         vi.如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode.如果需要更复杂的比较,则为这个域计算一个"范式",然后针对这个范式调用hashCode.如果这个域的值为null,则返回0(或者其他某个常数,通常是0).         vii.如果该域是一个数组,就要把每一个元素当做单独的域来处理.也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的做法把这些散列值组合起来.如果数组域的每个元素都很重要(都是用来进行逻辑比较的关键域),可以利用Arrays.hashCode方法.      b.按照下面公式,把步骤2.a中计算得到的散列码c合并到result中:
result = 31 * result +c;
   3.返回result.
补充:   散列码与散列桶(hash bucket)   利用移位和减法来代替乘法
实践:写一个HashCode码生成的工具类
10、始终要覆盖toString
若在文档中指定返回值的格式,可以提供一个相匹配的静态工厂或者构造器,以便对象与它的字符串表示法之间来回转换.
补充:   java1.5增加printf,还提提供了包括String.format的相关方法   矩阵类
11、谨慎地覆盖clone
Object的clone方法是受保护的.换一句话说,一个没有实现Cloneable接口的类在超出protected修饰符的访问域外是访问不到clone方法的.如果一个类实现了Cloneable,Cloneable接口就改变了Object中受保护的方法行为,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常.
clone时需要返回所clone的类型,而不是超类.协变返回类型作为泛型,即目前覆盖方法的返回类型可以是被覆盖方法的返回类型的子类.这样有助于覆盖方法提供更多关于被返回对象的信息,并且在客户端中不必进行类型转换.
在数组上调用clone返回的数组,其编译时类型与被克隆数组的类型相同.因此数组调用clone时不需要进行手动转型.
clone架构与引用可变对象的final域的正常用法是不相兼容的.
clone复杂对象的做法:   1.调用高层的方法来重新产生对象的状态 (put(key,value))   2.或者直接操作对象及其克隆对象的内部状态的clone方法(性能更好,更快)(操作Entry)      步骤:              a.实现Cloneable接口,并且提供一个public的clone方法         b.先调用父类的clone方法得到对象,再clone它的引用类型域(可能会遇到final域问题)
对于一个专门为了继承而设计的类,需要像Object.clone一样提供行为良好的受保护的clone方法,为保证子类有可实现或不实现Cloneable接口的*.                                             另一个实现对象拷贝的好办法是提供一个 拷贝构造器 或 拷贝工厂
public Yum(Yum yum); //copy constructor
public static Yum newInstance(Yum yum); //copy factory

优点:   1.不依赖于某一种很有风险的、语言之外的创建机制   2.不会与final域的正常使用发生冲突   3.不会抛出不必要的受检查异常   4.不需要进行类型转换   5.可以带参数,参数类型是该类实现的接口(如TreeSet实现Collection),以此控制返回类型(HashSet拷贝成TreeSet)
12、考虑实现Comparable接口
一旦类实现了Comparable接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作.例如,TreeSet通过comparable方法将两个项进行比较. 这一点与重写equals方法协作集合的操作类似.
强烈建议compareTo方法与equals方法的一致性:(x.compareTo(y) == 0) == ( x.equals(y) )
比较整数型基本类型的域可以用>和< ,浮点域基本类型用Double.compare、或者Float.compare
另外比较对象引用域可以通过递归地调用compareTo方法来实现.如果一个域没有实现Comparable接口,或者需要使用一个非标准的排序关系,就可以使用一个显式的Comparator来代替.例子代码如下:
package compareble;
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString>{
private final String s;
public CaseInsensitiveString(String str) {
this.s = str;
}
@Override
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s); //String.CASE_INSENSITIVE_ORDER is a Comparator
}

}

补充: 为实现Comparable接口的对象数组进行排序: Arrays.sort(a);