zoukankan      html  css  js  c++  java
  • 对于所有对象都通用方法的解读(Effective Java 第三章)

    这篇博文主要介绍覆盖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方法源码就知道了。

        public V get(Object key) {
            if (key == null)
                return getForNullKey();
            Entry<K,V> entry = getEntry(key);
    
            return null == entry ? null : entry.getValue();
        }
        final Entry<K,V> getEntry(Object key) {
            if (size == 0) {
                return null;
            }
    
            int hash = (key == null) ? 0 : hash(key);//获取对象的hash值
            for (Entry<K,V> e = table[indexFor(hash, table.length)];
                 e != null;
                 e = e.next) {
                Object k;
                //hash必须相等才有可能进行返回
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            }
            return null;
        }    

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

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

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

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

        @Override
        public int hashCode() {
            return 1;
        }

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

        @Override
        public int hashCode() {
            int result = 17;
            result = 31 * result + areaCode;
            result = 31 * result + prefix;
            result = 31 * result + lineNumer;
            return 1;
        }
  • 相关阅读:
    [Xcode 实际操作]四、常用控件-(4)UILabel文本标签的自动换行
    [Xcode 实际操作]四、常用控件-(3)UILabel文本标签的使用
    [Xcode 实际操作]四、常用控件-(2)UIButton图片按钮的使用
    [Xcode 实际操作]四、常用控件-(1)UIButton控件的使用
    [Xcode 实际操作]三、视图控制器-(12)在Storyboard中使用集合控件
    [Xcode 实际操作]三、视图控制器-(11)在Storyboard中使用表格控件
    [Xcode 实际操作]三、视图控制器-(10)在Storyboard中使用图像视图控件
    [Xcode 实际操作]三、视图控制器-(9)在Storyboard中使用标签和按钮控件
    NYOJ 119 士兵杀敌(三)(RMQ算法)
    解决[[NSFileManager defaultManager] contentsOfDirectoryAtPath 方法获取不到数据的bug
  • 原文地址:https://www.cnblogs.com/yuanfy008/p/7151891.html
Copyright © 2011-2022 走看看