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

以下为我在《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);

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值