这篇博文主要介绍覆盖Object中的方法要注意的事项以及Comparable.compareTo()方法。

一、谨慎覆盖equals()方法

  其实平时很少要用到覆盖equals方法的情况,没有什么特殊情况最好是使用原有提供的equlas方法。因为覆盖equals()方法时要遵循一些通用的约定之外,在与hash相关的集合类使用时,就必须要覆盖hashCode()方法了(第二点会强调)。

  我们先说说覆盖equlas()方法要遵循哪些通用约定:

  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)的结果依然一致。;

  5、非空性:对于任何非null的引用值,x.equals(null)必须返回false。 且x必须不能为空,否则会抛出空指针异常。

  其实上面这些约定看起来都看简单但也不能大意,很有可能你只满足了其中一点或者几点没有满足全部。如果没有满足全部的话,那么你覆盖equals()方法是不合理的,总会有那么中情况会超乎你的意料之外。

  下面我们举例说明,其中自反性是很难违反的(我也想象不出怎么举反例测试,如有高手明白,请举例评论下),下面我举例验证对称性:

  1. package test.effective;
  2. /**
  3. * @Description: 普通坐标类
  4. * @author yuanfy
  5. * @date 2017年7月13日 上午10:30:06
  6. */
  7. class Point {
  8. private int x;
  9. private int y;
  10.  
  11. public Point(int x, int y) {
  12. this.x = x;
  13. this.y = y;
  14. }
  15.  
  16. @Override
  17. public boolean equals(Object obj) {
  18. if (!(obj instanceof Point)) {
  19. return false;
  20. }
  21. Point p = (Point) obj;
  22. return this.x == p.x && this.y == p.y;
  23. }
  24. }
  25. /**
  26. * @Description: 带有颜色的坐标
  27. * @author yuanfy
  28. * @date 2017年7月13日 上午10:35:19
  29. */
  30. class ColorPoint extends Point {
  31.  
  32. private String color;
  33.  
  34. public ColorPoint(int x, int y, String color) {
  35. super(x, y);
  36. this.color = color;
  37. }
  38. /**
  39. * 与point不满足对称性示范案例
  40. */
  41. @Override
  42. public boolean equals(Object obj) {
  43. if (!(obj instanceof ColorPoint)) {
  44. return false;//与point实例比较直接返回false
  45. }
  46. ColorPoint p = (ColorPoint) obj;
  47. return super.equals(p) && this.color.equals(p.color);
  48. }
  49. }
  50.  
  51. public class EqualsTest {
  52. public static void main(String[] args) {
  53. Point p = new Point(1,2);
  54. ColorPoint cp = new ColorPoint(1, 2, "red");
  55.  
  56. System.out.println(p.equals(cp));//输出结果:true
  57. System.out.println(cp.equals(p));//输出结果:false
  58. }
  59. }

  从上面的例子可以看出是违反了对称性规定的。问题原因在于:在比较普通点和有色点时,忽略了颜色的比较,而有色点跟普通点比较时,普通点不属于ColorPoint的实例,就直接返回了false。所以ColorPoint类中覆盖equlas()方法是有问题的,修改equals()方法后的代码如下:

  1. @Override
  2. public boolean equals(Object obj) {
  3. //不属于Point实例对象
  4. if (!(obj instanceof Point)) {
  5. return false;
  6. }
  7. //不是ColorPoint实例对象,可能是Point实例对象或者其他类型对象
  8. if (!(obj instanceof ColorPoint)) {
  9. return super.equals(obj);
  10. }
  11. //ColorPoint实例对象
  12. ColorPoint p = (ColorPoint) obj;
  13. return super.equals(p) && this.color.equals(p.color);
  14. }

  测试代码如下:

  1. ColorPoint cp1 = new ColorPoint(1, 2, "red");
  2. Point p = new Point(1,2);
  3. ColorPoint cp2 = new ColorPoint(1, 2, "blue");
  4.  
  5. System.out.println(cp1.equals(p));//输出结果:true
  6. System.out.println(p.equals(cp1));//输出结果:true
  7.  
  8. System.out.println(p.equals(cp2));//输出结果:true
  9. System.out.println(cp2.equals(p));//输出结果:true
  10.  
  11. System.out.println(cp1.equals(p));//输出结果:true
  12. System.out.println(p.equals(cp2));//输出结果:true
  13. System.out.println(cp1.equals(cp2));//输出结果:false

  从修改后的例子中可以看出,这种方法确实满足了对称性,但是却不满足传递性。其实我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象带来的优势。当然我们可以不扩展Point的,所谓“复合优先于继承”。在ColorPoint假如一个私有的Point域,代码如下:

  1. class ColorPoint{
  2.  
  3. private String color;
  4.  
  5. private Point point;
  6.  
  7. public ColorPoint(int x, int y, String color) {
  8. point = new Point(x, y);
  9. this.color = color;
  10. }
  11.  
  12. public Point asPoint(){
  13. return point;
  14. }
  15.  
  16. @Override
  17. public boolean equals(Object obj) {
  18. if (!(obj instanceof ColorPoint)) {//满足非空性验证
  19. return false;
  20. }
  21. //ColorPoint实例对象
  22. ColorPoint p = (ColorPoint) obj;
  23. return this.point.equals(p.point) && this.color.equals(p.color);
  24. }
  25. }
  26.  
  27. public class EqualsTest1 {
  28. public static void main(String[] args) {
  29. ColorPoint cp1 = new ColorPoint(1, 2, "red");
  30. Point p = new Point(1,2);
  31. ColorPoint cp2 = new ColorPoint(1, 2, "red");
  32. ColorPoint cp3 = new ColorPoint(1, 2, "red");
  33.  
  34. System.out.println(cp1.equals(p));//输出结果:false
  35. System.out.println(p.equals(cp1));//输出结果:false
  36.  
  37. System.out.println(cp1.equals(cp2));//输出结果:true
  38. System.out.println(cp2.equals(cp1));//输出结果:true
  39.  
  40. System.out.println(cp1.equals(cp2));//输出结果:true
  41. System.out.println(cp2.equals(cp3));//输出结果:true
  42. System.out.println(cp1.equals(cp3));//输出结果:true
  43. }
  44. }

  上面的例子就满足对称性、传递性同时满足非空性。

  接下来验证一致性:如果两个对象相等,它们就必须始终保持相等,除非他们中有一个对象或者两个都被修改了。换句话说,可变的对象在不同的时候可以与不同的对象相等, 而不可变对象则不会这样。比如说时间对象一个指定了时间,另外一个对象没有指定时间。当在某一个刻时候,他们既满足对称性和传递性,但是它不满足一致性。因为没有指定时间的对象的时间是一直在改变的。

  所以当你编写完成了equlas之后,应该测试验证是否满足这个几个特性。

二、覆盖equals时总要覆盖hashCode

  书中提出:在每个覆盖了equlas方法的类,也必须覆盖hashCode方法。如果不这样做的话, 就会违反Object.hashCode()的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和HashTable。

  其中约定简洁如下(详细请参考书中):

  1、相等的对象必须具有相等的散列码(hash code)

  2、不相等的对象未必是不一样的散列码。也就是说相同散列码的两个对象,两个对象未必相等。但是相等的两个对象,一定就有相等的散列码。

  下面根据例子来说明:

  1. package test.effective;
  2.  
  3. import java.util.HashMap;
  4. import java.util.Map;
  5.  
  6. public class PhoneNumber {
  7. private int areaCode;
  8.  
  9. private int prefix;
  10.  
  11. private int lineNumer;
  12.  
  13. public PhoneNumber(int areaCode, int prefix, int lineNumer) {
  14. this.areaCode = areaCode;
  15. this.prefix = prefix;
  16. this.lineNumer = lineNumer;
  17. }
  18.  
  19. @Override
  20. public boolean equals(Object obj) {
  21. if (obj == this) {
  22. return true;
  23. }
  24. if (!(obj instanceof PhoneNumber)) {
  25. return false;
  26. }
  27. PhoneNumber pn = (PhoneNumber) obj;
  28. return this.areaCode == pn.areaCode
  29. && this.prefix == pn.prefix
  30. && this.lineNumer == this.lineNumer;
  31. }
  32.  
  33. public static void main(String[] args) {
  34.  
  35. Map<PhoneNumber, String> map = new HashMap<PhoneNumber, String>();
  36. PhoneNumber pn1 = new PhoneNumber(408, 867, 5309);
  37. PhoneNumber pn2 = new PhoneNumber(408, 867, 5309);
  38.  
  39. System.out.println(pn1.equals(pn2));//输出结果:true
  40.  
  41. map.put(pn1, "Jany");
  42.  
  43. System.out.println(map.get(pn1));
  44. System.out.println(map.get(pn2));
  45. }
  46. }

  我们将覆盖了equals方法的类结合基于散列码的集合使用。从上面例子39行知道pn1实例和pn2实例是相等,行43代码输出的结果可想而知是Jany,但是行44输出什么呢,估计没有仔细考虑的话,可能第一感觉也会输出null。我们看下HashMap中get方法源码就知道了。

  1. public V get(Object key) {
  2. if (key == null)
  3. return getForNullKey();
  4. Entry<K,V> entry = getEntry(key);
  5.  
  6. return null == entry ? null : entry.getValue();
  7. }
  8. final Entry<K,V> getEntry(Object key) {
  9. if (size == 0) {
  10. return null;
  11. }
  12.  
  13. int hash = (key == null) ? 0 : hash(key);//获取对象的hash值
  14. for (Entry<K,V> e = table[indexFor(hash, table.length)];
  15. e != null;
  16. e = e.next) {
  17. Object k;
  18. //hash必须相等才有可能进行返回
  19. if (e.hash == hash &&
  20. ((k = e.key) == key || (key != null && key.equals(k))))
  21. return e;
  22. }
  23. return null;
  24. }

  从源码中得知get()方法是要根据hash值获取的。然后我们在看看pn1和pn2对象的hash码。

  1. System.out.println(pn1.hashCode());//
  2. System.out.println(pn2.hashCode());//2031122075

  所以显而易见见,上述map.get(pn2) 的返回值是为null的。

  修正这个问题很简单,在PhoneNumber类中覆盖hashCode()方法即可,先提供简单的案例:

  1. @Override
  2. public int hashCode() {
  3. return 1;
  4. }

  这样的能满足上面那个例子map.get(pn2) 返回“Jany”,但是这样 所有实例都是返回一样的hashCode是不可取的。参考书中最正确的方式:

  1. @Override
  2. public int hashCode() {
  3. int result = 17;
  4. result = 31 * result + areaCode;
  5. result = 31 * result + prefix;
  6. result = 31 * result + lineNumer;
  7. return 1;
  8. }

对于所有对象都通用方法的解读(Effective Java 第三章)的更多相关文章

  1. 对于所有对象都通用方法的解读(Effective Java 第二章)

    这篇博文主要介绍覆盖Object中的方法要注意的事项以及Comparable.compareTo()方法. 一.谨慎覆盖equals()方法 其实平时很少要用到覆盖equals方法的情况,没有什么特殊 ...

  2. [Effective Java]第三章 对所有对象都通用的方法

    声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...

  3. Java高效编程之二【对所有对象都通用的方法】

    对于所有对象都通用的方法,即Object类的所有非final方法(equals.hashCode.toString.clone和finalize)都有明确的通用约定,都是为了要被改写(override ...

  4. Effective Java:对于全部对象都通用的方法

    前言: 读这本书第1条规则的时候就感觉到这是一本非常好的书.可以把我们的Java功底提升一个档次,我还是比較推荐的.这里我主要就关于覆盖equals.hashCode和toString方法来做一个笔记 ...

  5. [Effective Java 读书笔记] 第三章 对所有对象都通用的方法 第八 ---- 九条

    这一章主要讲解Object类中的方法, Object类是所有类的父类,所以它的方法也称得上是所有对象都通用的方法 第八条 覆盖equals时需要遵守的约定 Object中的equals实现,就是直接对 ...

  6. Effective java -- 2 对于所有对象都通用到方法

    第八条:覆盖equals时请遵守通用约定 什么时候需要覆盖equals方法?类具有自己的逻辑相等概念,并且父类的equals方法不能满足需要.重写equals时需要遵循一下约定: 自反性:非null ...

  7. 《Effective Java》第3章 对于所有对象都通用的方法

    第8条:覆盖equals时请遵守通用约定 覆盖equals方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重.最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每 ...

  8. Effective Java读书笔记——第三章 对于全部对象都通用的方法

    第8条:覆盖equals时请遵守通用的约定 设计Object类的目的就是用来覆盖的,它全部的非final方法都是用来被覆盖的(equals.hashcode.clone.finalize)都有通用约定 ...

  9. 《Effective Java》第2章 对所有对象都通用的方法

    第10条:覆盖equals时,请遵守通用约定 1.使用==来比较两个对象的时候,比较的是两个对象在内存中的地址是否相同(两个引用指向的是否为同一个对象):Object中定义的equals方法也是这样比 ...

随机推荐

  1. PAT L1-039 古风排版

    https://pintia.cn/problem-sets/994805046380707840/problems/994805091888906240 中国的古人写文字,是从右向左竖向排版的.本题 ...

  2. IntelliJ IDEA Maven引入

  3. ServiceMessage

    <?php class ServiceMessage { private $errorCode = array( '1000' => "系统错误", '1001' =& ...

  4. ajax 请求 后台返回的文件流

    download(url) { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); // 也可以使用POST方式,根据接口 xhr. ...

  5. SQL Server的全局变量

    SQL Service中的全部变量不需要用户参与定义,在任何程序均可随时调用.并且全部变量是以@@开头 全局变量名称 描述 @@CONNECTIONS 返回 SQL Server 自上次启动以来尝试的 ...

  6. bzoj3168-钙铁锌硒维生素

    题目 这道题的题意理解很重要,直接写原题了. 小林把人体需要的营养分成了\(n\)种,他准备了2套厨师机器人,一套厨师机器人有\(n\)个,每个厨师机器人只会做一道菜,这道菜一斤能提供第\(i\)种营 ...

  7. 【题解】Atcoder AGC#01 E-BBQ Hard

    计数题萌萌哒~ 这道题其实就是统计 \(\sum_{i=1}^{n}\sum_{j=i+1}^{n}C\binom{a[i] + a[j]}{a[i] + a[j] + b[i] + b[j]}\) ...

  8. [洛谷P4721]【模板】分治 FFT_求逆

    题目大意:给定长度为$n-1$的数组$g_{[1,n)}$,求$f_{[0,n)}$,要求: $$f_i=\sum_{j=1}^if_{i-j}g_j\\f_0=1$$ 题解:分治$FFT$博客,发现 ...

  9. ubuntu简易教程(如何使用noi linux)

    目录 linux环境下的基础操作 命令行操作 编辑器 程序编译 程序调试 gdb的使用 对拍 在提高组的考试中要求使用noi linux,因此了解一下如何在linux环境下编程是很有必要的. linu ...

  10. 洛谷3800:Power收集——题解

    https://www.luogu.org/problemnew/show/P3800 可以把游戏界面理解成一个N行M列的棋盘,有K个格子上有P点,其价值为val(i,j) 初始灵梦可以选择在第一行的 ...