转载

第3章-对于所有对象都通用的方法

前言

Object的设定是为了扩展,它的所有非final方法(equals hashCode toString clone finalize)都有明确的通用约定,因为 它们被设计是要被覆盖 (override)的

而在覆盖这些方法时,都 有责任遵守这些通用的约定 ,否则,其他依赖这些约定的类(如HashMap&HashSet)就无法结合该类一起正常运作.

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

不覆盖equals

不覆盖equals的情况下,类的每个实例都与它自身相等,如果满足以下任何一个条件,就是所期望的结果:

  • 类的每个实例本质上都是唯一的
  • 不关心类是否提供了”逻辑相等”的测试功能
  • 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的(要小心)
  • 类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用 (不懂为什么)

讲得怪怪的

PS: 逻辑相等,就是逻辑上是相等的,比如id一样,判定它们相等,即使它们是两个不同的对象

什么时候应该覆盖equals

当类需要逻辑相等这个概念的时候就应该覆盖equals

比如要判断两个 student 是否是同一个人,这个时候我们就需要按需重写equals

通用约定

重写equals的时候就必须要遵守它的通用约定equals方法实现了等价关系(equivalence relation):

  • 自反性(reflexive) 对于任何非null的引用值x,x.equals(x)必须返回true
  • 对称性(symmetric) 对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true
  • 传递性(transitive) 对于任何非null的引用值,x,y,z,如果x.equals(y)为true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true
  • 一致性(consistent) 对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者false
  • 对于任何非null的引用值,x,x.equals(null)必须返回false

感觉又回到了学数学交换律什么的的时候了~

有些类(如集合,HashMap)与 equals 方法息息相关,所以重写的时候要仔细小心

高质量的equals

ej对equals提了几点建议:

  1. 使用 == 操作符检查”参数是否为这个对象的引用” 如果是,则返回true. 这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做 (平时没有用过,怎么样的比较操作算是昂贵的呢?)
  2. 使用 instanceof 操作符检查”参数是否为正确的类型” 如果不是,则返回false。
  3. 把参数装换成正确的类型。(这个比较好理解,instanceof检测后,一般都会强转成所需类型)
  4. 对于该类中的每个『关键』域,检查参数中的域是否与对象中对应的域相配。(比如学生类有学号,班级,姓名这些重要的属性,我们都需要去比对)
  5. 当你编写完成了equals方法之后,应该问自己是哪个问题:它是否是对称的、传递的、一致的?

另外EJ还告诫我们 覆盖equals的时候总要覆盖hashCode (见第9条)

小结

最后按照上诉建议,用一个 Student 类来总结一下equals的写法:

public class Student {
public String name;
public String className;
@Override
public boolean equals(Object obj) {
//对于一个null的对象 我们总是返回false
if (null == obj) {
return false;
}
// 利用instanceof检查类型后,强转
if (obj instanceof Student){
Student other = (Student) obj;
//再对关键的属性做比较 得出结论
if (name.equals(other.name) && className.equals(other.className)) {
return true;
}
}
return false;
}
}

equals 是一个看上去简单,实则是个比较容易犯错的方法,需要小心仔细

第9条 覆盖equals时总要覆盖hashCode

覆盖了equals方法,也必须覆盖hashCode方法,if not,就违反了hashCode的通用约定,会导致无法跟基于散列的集合正常运作.

Object通用约定(在Object类中的注释即是):

  • 在应用程序的执行期间,只要对象的 equals 方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次, hashCode 方法都必须始终如一地返回同一个整数.在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致.
  • 如果两个对象根据 equals 方法比较是相等的,那么调用这两个对象中任意一个对象的 hashCode 方法都必须产生同样的整数结果.( 即equals相等,那么hashCode一定相等,需要注意的是,反过来不一定成立,即hashCode相等不代表equals相等 )
  • 如果两个对象根据 equals 方法比较是不相等的,那么调用这两个对象中任意一个对象的 hashCode 方法,则不一定要产生不同的整数结果.但是程序员应该知道,给不相等的对象产生截然不同的证书结果,有可能提高散列表(hash table)的性能.

不重写 hashCode 带来的问题

正如之前提到的,hashCode其实主要用于跟基于散列的集合合作

如HashMap会把 相同的hashCode的对象放在同一个散列桶 (hash bucket)中,那么即使equals相同而hashCode不相等,那么跟HashMap一起使用,则 会得到与预期不相同的结果 .

具体是怎么样的不同的效果?来看一段代码:

PS: Student 类是第8条里的类,重写了 equals

public static void main(String[]args) {
Student lilei = new Student("lilei","class1");
HashMap<Student, String> hashMap = new HashMap<>();
hashMap.put(lilei, lilei.className);
String className = hashMap.get(new Student("lilei","class1"));//值与之前的lilei相同,即equals会为true

System.out.println(className);
}

className 的值为多少呢?

class1 ?

NO!是 null !!!!(诶?)

为什么呢?因为我们并没有重写 hashcode ,所以即使我们去 get 的时候传入的 Student 的name以及classname与 put 的时候的对象值是一样的,也即 它们是 equals (我重写了 equals !),但是要注意, 它们的 hashcode 是不一样的 ,这样就违反了上面所说的 equals相等,hashCode也要相等 的原则,所以当我们期望 get 到的是 class1 的时候,我们需要重写 hashCode 方法,让它们的hashcode相同!

那么问题来了,如何去重写 hashCode 呢?返回一个固定值?比如1?NO!!!

So,how?

如何重写 hashCode

EJ给出的解决办法:

  1. 把某个 非零 的常数值,比如17,保存在一个名为result的int类型的变量中。
  2. 对于对象中每个关键域f( 指equals方法中涉及的每个域 ),完成以下步骤:
    • 步骤(a) 为该域计算 int 类型的散列码c:
      • 如果f是boolean,则计算 f?1:0
      • 如果是byte,char,short或int,则计算 (int)f
      • 如果是long,则 计算(int)(f^(f>>>32))
      • 如果是float,则 Float.floatToIntBits(s)
      • 如果是double,则计算 Double.doubleToLongBits(f) ,再按long类型计算一遍
      • 如果是f是个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归调用hashCode。如果需要更复杂的比较,则为这个域计算一个‘范式’,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(或者其他某个常数,但通常是0)。
      • 如果是个数组,则需要把每个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤b中的做法把这些散列值组合起来。 如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个 Arrays.hashCode 方法。
      • 步骤(b) 按照下面公式,把(a)步骤中计算得到的散列码c合并到result中: result = 31*result+c (为什么是31呢?)
  3. 返回result
  4. 测试,是否符合『相等的实例是否都具有相等的散列码』

OK,知道怎么写之后,我们重写Student类的hashCode方法:

@Override
public int hashCode() {
int result = 17;//非0 任选
result = 31*result + name.hashCode();
result = 31*result + className.hashCode();
return result;
}

这下之前的代码输出的结果为 class1 了!!!~

为什么要选31

因为它是个 奇素数 ,另外它还有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能:31*i == (i<<5)-i

小结

终于学会如何写 hashCode 了!

老实说,我并没有做到这条要求!

因为一般来说我不会把 Student 这样的类当做一个 Key 去处理

PS:书中讲到的知识点很多,光看这个笔记是不够的,如果可以,自己去阅读书籍吧!

其他资料

dim提供: 浅谈Java中的hashcode方法

第10条 始终要覆盖toString

Object类默认toString的实现方法是这样的:

public String toString() {
return getClass().getName() + '@' + Integer.toHexString(hashCode());
}

它只有类名+’@’+散列值, toString 的通用约定指出,被返回的字符串应该是一个『简洁的,但信息丰富,并且易于阅读的表达形式』

虽然够简单,但是信息并不丰富,而且更多时候我们更希望 toString 返回对象中包含的所有值得关注的信息 ,当属性多了,只显示信息重要的即可

toString 倒没有特别大的约束

第11条 谨慎地覆盖clone

clone 说到 clone (protected)就必须提及一下 Cloneable 接口,这个接口很奇怪,没有方法:

public interface Cloneable {
}

Object的 clone 方法 当我们尝试调用一个没有实现 Cloneable 接口的类的clone方法数时,clone会抛出 CloneNotSupportedException ,是不是很坑爹?

protected Object clone() throws CloneNotSupportedException {
if (!(this instanceof Cloneable)) {
throw new CloneNotSupportedException("Class " + getClass().getName() +
" doesn't implement Cloneable");
}
return internalClone();
}

为什么不把clone方法放Cloneable接口里面去却偏偏塞给了Object?这个设计我真的想不明白!!!!!

clone 方法自己没怎么用过,不过可以看看其他优秀的库的设计,比如 Retrofit 中的 OkHttpCall :

@Override public OkHttpCall<T> clone() {
return new OkHttpCall<>(serviceMethod, args);
}

PS:在使用优秀的开源库的时候,如果可以,多看看它的源码,你会学到很多!相信我!

第12条 考虑实现Comparable接口

注意 compareTo 不是Object的方法,而是 Comparable 接口的方法:

public interface Comparable<T>{
int compareTo(T t);
}

compareTo的约定跟equals类似:

PS:符合 sgn (表达式)表示数学中的signum函数,它根据表达式(expression)的值为负值、零、和正直,分别返回-1、0或1

  • 确保sgn(x.compareTo(y))== -sgn (y.compareTo(x))
  • 可传递:x.compareTo(y)> 0 && y.compareTo(z) 暗示 x.compareTo(z)> 0
  • 确保x.compareTo(y)==0暗示所有z都满足sgn(x.compareTo(z))== sgn(y.compareTo(z))
  • 强烈建议(x.compareTo(y)==0),但这并非绝对重要
    (个人觉得还是遵守更好一些!)

如果不想写compareTo或者类并没有实现Comparable接口的可以自定义一个 Comparator 类来进行比较。

需要注意,排序是不允许出现逻辑漏洞的,否则会crash!

本章完结

题外话:Object一共有 12 个方法,其中 7个是native 方法

原文  http://yifeiyuan.me/2016/05/18/第3章-对于所有对象都通用的方法/
正文到此结束
Loading...