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

第8条 覆盖equals时请遵守通用约定

以下是,属于合理的不覆盖equals的情况:

  1. 类实例是本质上是唯一的,代表的是一个实体或是对象,一个实例不可能与另一个实例相等。也可理解为非值类的类:如Thread,Apple,Desk等,Object提供的默认实现已经足够了。
  2. 不关心类是否提供了“相等”的方法,如一些纯工具类。一个工具类的实例被创建,主要目的是为了调用工具方法,而不关心实例是否与其他实例相等,如Random类,就是生成随机数用的。
  3. 父类已经实现了equals方法,派生出的子类天然已具了判断与其他实例是否“相等”的方法。如AbstractList,AbstractSet等。
  4. 类是私有的,或是包级私有的,包内逻辑不会用到此类的判断相等的方法,包外也访问不了,可以确定equals永远用不到也没有必要实现。如果特殊情况,甚至不希望使用Object的默认实现,也可以覆盖equals方法,在方法体中单纯抛出一个异常。
@Override
public boolean equals(Object o){
    throw new AssertionError();
}

什么时候应该覆盖此方法呢?有判断逻辑相等的需求时,且父类没有提供默认实现(不含Object的)。一般也属于类似于值类的,如Integer、Date。

有一种值类是实例受控的,不需要覆盖此方法,如枚举类,这样的对象逻辑相等与对象相等是一样的。

覆盖equals时,遵守约定:

自反性、对称性、传递性、一致性、对于非null的值x,x.equals(null)必须返回false

复合优先于继承

表示同一时间的Date对象、它的子类java.sql.Timestamp对象,对它们执行 equals 方法,将返回false。在实现equals方法上来讲,这是一个不好的例子。

原因是: Timestamp 使用了继承Date的方式实现,并添加了nanos域,所以equals很难实现支持Date对象。

实现equals 的 一致性:无论类是否可变,都不要使equals依赖于不可靠的资源。java.net.URL是一个反面例子,但是由于兼容性要求,不能修改它了。

实现高质量的equals方法的诀窍

  1. 使用==检查 参数是否是当前对象的引用
  2. 使用instanceof 操作符,检查 参数是否是正确的类型。一般为本身的类,或是实现的接口,如List
  3. 把参数转化成正确的类型。因为上一步使用过instanceof,确保了此操作可以成功
  4. 对类中的关键域,逐一比较与参数对象中的域是否匹配,全部匹配return true,否则false

实现equals的告诫

  • 总要覆盖hashCode()
  • 不要让equals过于智能,File类是一个正向的例子
  • 不要将equals的参数改成自己的类,而要覆盖Object的方法

第9条 覆盖equals时,也一定要覆盖hashCode方法

Object实现hashCode的主要目的是,保证与所有基于散列的集合一起工作时,能够运行正常。

Object规范关于hashCode的约定内容:

  1. 在程序运行期间,如果equals方法中用到的域没有发生改变,则多次调用hashCode须返回同一个值
  2. 如果两个对象equals返回true,则这两个对象的hashCode方法返回值也要相同
  3. 如果两个对象equals返回false,则不一定要返回不同的hashCode。(但返回不同的hashCode可以提高散列表的性能)

问题:两个逻辑上相等的对象a1 a2,如果HashMap.put(a1, "love"),想通过HashMap.get(a2)取出"love"将返回null

原因:a1,a2虽然逻辑上相等,但在Object的默认实现中,它俩的hashCode值是不同的,是完全不同的对象。HashMap在put时,缓存了a1的hashCode值,在get时找不到a2的hashCode,导致查找不到。

设计一个类,如果它可能或一定会与基于散列的集合一起工作时,就一定要实现HashCode方法。

HashCode方法的高效实现:以下是String类的实现,可以作为一个高质量的实现模板,背下来:

public int hashCode() {
	int h = hash;
	final int len = length();
	if (h == 0 && len > 0) {
		for (int i = 0; i < len; i++) {
			h = 31 * h + charAt(i);
		}
		hash = h;
	}
	return h;
}

第10条 始终要覆盖toString

这个没啥好说的,优点是打印信息的时候,比较方便。

注意的是:toString()返回的信息,应该是简洁、包含有意义的信息、易读的。toString()中涉及的字段,最好在类中提供公有的访问方法,当然不是必须的。

第11条 谨慎地使用(最好不要用)覆盖clone方法

如何实现一个行为良好的clone方法?

行为良好的clone方法可以调用构造器来创建对象,构造之后再复制内部数据。

  1. 声明Cloneable接口
  2. 以公有访问方式,覆盖clone方法,返回类型为目标类型(子类->Object 用到了协变,从Java1.5开始支持的)。
  3. 继承链上的所有类,覆盖clone方法 且
  4. 每个类的每个域是基本类型,或是不可变对象的引用(不可变对象:基本类型、基本类型的装箱类型、final域,可变对象:数组、一般自定义对象)

此时clone出的对象正是需要的对象,不需要其他处理。


注意,如果类中的域 有的是可变对象的引用,上面的简单clone方法将导致clone出的新对象与原对象的可变对象引用是同一个。

clone架构与类中的可变对象引用final域是不兼容的,如果一定要clone这个类,可能需要去掉它的域中的final修饰符。否则在clone方法中,final域不能创建新对象,无法复制。

clone方法实现细节:

  • 基本类型的数组,直接clone就安全;
  • 可变对象引用,不安全,需要clone;
  • 可变对象引用的数组,clone数组后也是不安全的,还要循环元素clone对象引用

其他的可替代的方法?

copy构造器,或copy工厂

// Copy constructor
public Yum(Yum yum) { ... };

// Copy factory
public static Yum newInstance(Yum yum) { ... };

clone有什么缺点?

  1. 规范约定弱,由于Cloneable接口在Object中有一个保护的默认实现了,implement Cloneable接口也不一定有真的实现,缺少强制约束!
  2. 继承树的每一个类结点是否完整支持clone无法预知
  3. 实现clone方法需要考虑的域很多,容易出错。
  4. 需要处理clone方法抛出的异常
  5. 不支持类型转换:而使用copy工厂可以方便实现HashSet copy成TreeSet。

何时适合这样做?

如果一个内部实现复杂的类,实现了Cloneable接口,要实现它的clone方法将非常复杂与麻烦。大部分时间没必要这么做。有copy需求,请使用copy构造器,或copy工厂。

总结:

clone方法的缺点太多,一些专家级程序员从来不实现这个方法,也不调用这个方法。只有在copy一个基本类型数组的时候,可以当一个便捷方法使用。

第12条 考虑实现Comparable接口

为实现了Comparable接口的对象数组进行排序:

Arrays.sort(arr)

Java平台类库所有值类都实现了Comparable接口,如果你正在设计一个值类,它具有非常明显的内在排序关系业务场景很可能用到排序,就一定要考虑实现Comparable

就好像违反了HashCode的约定会破坏其他依赖于散列做算法的类一样,违反了Comparable约定会破坏其他依赖于比较关系的类:如有序集合TreeSet、TreeMap,工具类Collections、Arrays。

实现Comparable需要注意的点:

  1. 比较整型类型的域可以使用关系操作符> 和 <
  2. 比较浮点域使用 Float.compare 或 Double.compare 
  3. 对于数组域则应把上面的原则应用到每个元素上
  4. 类中有多个域需要比较时,按域的重要程度从高到低的顺序进行比较

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洛克Lee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值