专栏名称: 安卓开发精选
伯乐在线旗下账号,分享安卓应用相关内容,包括:安卓应用开发、设计和动态等。
目录
相关文章推荐
鸿洋  ·  Android | 扩大View点击区域的几种方式 ·  2 天前  
stormzhang  ·  双十一晚会不香了? ·  3 天前  
鸿洋  ·  鸿蒙中是如何实现UI自动刷新的? ·  1 周前  
51好读  ›  专栏  ›  安卓开发精选

重写equals()时为什么也得重写hashCode()之深度解读equals方法与hashCode方法渊源(下)

安卓开发精选  · 公众号  · android  · 2016-08-12 08:29

正文

(点击上方公众号,可快速关注)


来源:伯乐在线专栏作者 - shine_zejian

链接:http://android.jobbole.com/84152/

点击 → 了解如何加入专栏作者


接上文


5.为什么重写equals()的同时还得重写hashCode()


这个问题之前我也很好奇,不过最后还是在书上得到了比较明朗的解释,当然这个问题主要是针对映射相关的操作(Map接口)。学过数据结构的同学都知道Map接口的类会使用到键对象的哈希码,当我们调用put方法或者get方法对Map容器进行操作时,都是根据键对象的哈希码来计算存储位置的,因此如果我们对哈希码的获取没有相关保证,就可能会得不到预期的结果。在java中,我们可以使用hashCode()来获取对象的哈希码,其值就是对象的存储地址,这个方法在Object类中声明,因此所有的子类都含有该方法。那我们先来认识一下hashCode()这个方法吧。hashCode的意思就是散列码,也就是哈希码,是由对象导出的一个整型值,散列码是没有规律的,如果x与y是两个不同的对象,那么x.hashCode()与y.hashCode()基本是不会相同的,下面通过String类的hashCode()计算一组散列码:


package com.zejian.test;  

public class HashCodeTest {  

    public static void main(String[] args) {  

        int hash=0;  

        String s="ok";  

        StringBuilder sb =new StringBuilder(s);  

          

        System.out.println(s.hashCode()+"  "+sb.hashCode());  

          

        String t = new String("ok");  

        StringBuilder tb =new StringBuilder(s);  

        System.out.println(t.hashCode()+"  "+tb.hashCode());  

    }  

      

}


运行结果:


3548  1829164700


3548  2018699554


我们可以看出,字符串s与t拥有相同的散列码,这是因为字符串的散列码是由内容导出的。而字符串缓冲sb与tb却有着不同的散列码,这是因为StringBuilder没有重写hashCode方法,它的散列码是由Object类默认的hashCode方法计算出来的对象存储地址,所以散列码自然也就不同了。那么我们该如何重写出一个较好的hashCode方法呢,其实并不难,我们只要合理地组织对象的散列码,就能够让不同的对象产生比较均匀的散列码。例如下面的例子:


package com.zejian.test;  

public class Model {  

    private String name;  

    private double salary;  

    private int sex;  

      

    @Override  

    public int hashCode() {  

        return name.hashCode()+new Double(salary).hashCode()  

                + new Integer(sex).hashCode();  

    }  

}


上面的代码我们通过合理的利用各个属性对象的散列码进行组合,最终便能产生一个相对比较好的或者说更加均匀的散列码,当然上面仅仅是个参考例子而已,我们也可以通过其他方式去实现,只要能使散列码更加均匀(所谓的均匀就是每个对象产生的散列码最好都不冲突)就行了。不过这里有点要注意的就是java 7中对hashCode方法做了两个改进,首先java发布者希望我们使用更加安全的调用方式来返回散列码,也就是使用null安全的方法Objects.hashCode(注意不是Object而是java.util.Objects)方法,这个方法的优点是如果参数为null,就只返回0,否则返回对象参数调用的hashCode的结果。Objects.hashCode 源码如下:


public static int hashCode(Object o) {  

        return o != null ? o.hashCode() : 0;  

    }


因此我们修改后的代码如下:


package com.zejian.test;  

import java.util.Objects;  

public  class Model {  

    private   String name;  

    private double salary;  

    private int sex;  

    @Override  

    public int hashCode() {  

        return Objects.hashCode(name)+new Double(salary).hashCode()  

                + new Integer(sex).hashCode();  

    }  

}


java 7还提供了另外一个方法java.util.Objects.hash(Object… objects),当我们需要组合多个散列值时可以调用该方法。进一步简化上述的代码:


package com.zejian.test;  

import java.util.Objects;  

public  class Model {  

    private   String name;  

    private double salary;  

    private int sex;  

//  @Override  

//  public int hashCode() {  

//      return Objects.hashCode(name)+new Double(salary).hashCode()  

//              + new Integer(sex).hashCode();  

//  }  

      

    @Override  

    public int hashCode() {  

        return Objects.hash(name,salary,sex);  

    }  

}


好了,到此hashCode()该介绍的我们都说了,还有一点要说的如果我们提供的是一个数值类型的变量的话,那么我们可以调用Arrays.hashCode()来计算它的散列码,这个散列码是由数组元素的散列码组成的。接下来我们回归到我们之前的问题,重写equals方法时也必须重写hashCode方法。在Java API文档中关于hashCode方法有以下几点规定(原文来自java深入解析一书)。


  • 在java应用程序执行期间,如果在equals方法比较中所用的信息没有被修改,那么在同一个对象上多次调用hashCode方法时必须一致地返回相同的整数。如果多次执行同一个应用时,不要求该整数必须相同。

  • 如果两个对象通过调用equals方法是相等的,那么这两个对象调用hashCode方法必须返回相同的整数。

  • 如果两个对象通过调用equals方法是不相等的,不要求这两个对象调用hashCode方法必须返回不同的整数。但是程序员应该意识到对不同的对象产生不同的hash值可以提供哈希表的性能。


通过前面的分析,我们知道在Object类中,hashCode方法是通过Object对象的地址计算出来的,因为Object对象只与自身相等,所以同一个对象的地址总是相等的,计算取得的哈希码也必然相等,对于不同的对象,由于地址不同,所获取的哈希码自然也不会相等。因此到这里我们就明白了,如果一个类重写了equals方法,但没有重写hashCode方法,将会直接违法了第2条规定,这样的话,如果我们通过映射表(Map接口)操作相关对象时,就无法达到我们预期想要的效果。如果大家不相信, 可以看看下面的例子(来自java深入解析一书)


package com.zejian.test;  

import java.util.HashMap;  

import java.util.Map;  

public class MapTest {  

    public static void main(String[] args) {  

        MapString,Value> map1 = new HashMapString,Value>();  

        String s1 = new String("key");  

        String s2 = new String("key");    

        Value value = new Value(2);  

        map1.put(s1, value);  

        System.out.println("s1.equals(s2):"+s1.equals(s2));  

        System.out.println("map1.get(s1):"+map1.get(s1));  

        System.out.println("map1.get(s2):"+map1.get(s2));  

          

          

        MapKey,Value> map2 = new HashMapKey,Value>();  

        Key k1 = new Key("A");  

        Key k2 = new Key("A");  

        map2.put(k1, value);  

        System.out.println("k1.equals(k2):"+s1.equals(s2));  

        System.out.println("map2.get(k1):"+map2.get(k1));  

        System.out.println("map2.get(k2):"+map2.get(k2));  

    }  

      

    /**

     * 键

     * @author zejian

     *

     */  

    static class Key{  

        private String k;  

        public Key(String key){  

            this.k=key;  

        }  

          

        @Override  

        public boolean equals(Object obj) {  

            if(obj instanceof Key){  

                Key key=(Key)obj;  

                return k.equals(key.k);  

            }  

            return false;  

        }  

    }  

      

    /**

     * 值

     * @author zejian

     *

     */  

    static class Value{  

        private int v;  

          

        public Value(int v){  

            this.v=v;  

        }  

          

        @Override  

        public String toString() {  

            return "类Value的值-->"+v;  

        }  

    }  

}


代码比较简单,我们就不过多解释了(注意Key类并没有重写hashCode方法),直接运行看结果


s1.equals(s2):true

map1.get(s1):Value的值-->2

map1.get(s2):Value的值-->2

k1.equals(k2):true

map2.get(k1):Value的值-->2

map2.get(k2):null


对于s1和s2的结果,我们并不惊讶,因为相同的内容的s1和s2获取相同内的value这个很正常,因为String类重写了equals方法和hashCode方法,使其比较的是内容和获取的是内容的哈希码。但是对于k1和k2的结果就不太尽人意了,k1获取到的值是2,k2获取到的是null,这是为什么呢?想必大家已经发现了,Key只重写了equals方法并没有重写hashCode方法,这样的话,equals比较的确实是内容,而hashCode方法呢?没重写,那就肯定调用超类Object的hashCode方法,这样返回的不就是地址了吗?k1与k2属于两个不同的对象,返回的地址肯定不一样,所以现在我们知道调用map2.get(k2)为什么返回null了吧?那么该如何修改呢?很简单,我们要做也重写一下hashCode方法即可(如果参与equals方法比较的成员变量是引用类型的,则可以递归调用hashCode方法来实现):


@Override  

public int hashCode() {  

     return k.hashCode();  

}


再次运行:


s1.equals(s2):true  

map1.get(s1):Value的值-->2  

map1.get(s2):Value的值-->2  

k1.equals(k2):true  

map2.get(k1):Value的值-->2  

map2.get(k2):Value的值-->2


6.重写equals()中getClass与instanceof的区别


虽然前面我们都在使用instanceof(当然前面我们是根据需求(批次相同即相等)而使用instanceof的),但是在重写equals() 方法时,一般都是推荐使用 getClass 来进行类型判断(除非所有的子类有统一的语义才使用instanceof),不是使用 instanceof。我们都知道 instanceof 的作用是判断其左边对象是否为其右边类的实例,返回 boolean 类型的数据。可以用来判断继承中的子类的实例是否为父类的实现。下来我们来看一个例子:父类Person


public class Person {  

        protected String name;  

        public String getName() {  

            return name;  

        }  

        public void setName(String name) {  

            this.name = name;  

        }  

        public Person(String name){  

            this.name = name;  

        }  

        public boolean equals(Object object){  

            if(object instanceof Person){  

                Person p = (Person) object;  

                if(p.getName() == null || name == null){  

                    return false;  

                }  

                else{  

                    return name.equalsIgnoreCase(p.getName ());  

                }  

            }  

            return false;  

       }  

    }


子类 Employee


public class Employee extends Person{  

        private int id;  

        public int getId() {  

            return id;  

        }  

        public void setId(int id) {  

            this.id = id;  

        }  

        public Employee(String name,int id){  

            super(name);  

            this.id = id;  

        }  

        /**

         * 重写equals()方法

         */  

        public boolean equals(Object object){  

            if(object instanceof Employee){  

                Employee e = (Employee) object;  

                return super.equals(object) && e.getId() == id;  

            }  

            return false;  

        }  

    }


上面父类 Person 和子类 Employee 都重写了 equals(),不过 Employee 比父类多了一个id属性,而且这里我们并没有统一语义。测试代码如下:


public class Test {  

        public static void main(String[] args) {  

            Employee e1 = new Employee("chenssy", 23);  

            Employee e2 = new Employee("chenssy", 24);  

            Person p1 = new Person("chenssy");  

            System.out.println(p1.equals(e1));  

            System.out.println(p1.equals(e2));  

            System.out.println(e1.equals(e2));  

        }  

    }


上面代码我们定义了两个员工和一个普通人,虽然他们同名,但是他们肯定不是同一人,所以按理来说结果应该全部是 false,但是事与愿违,结果是:true、true、false。对于那 e1!=e2 我们非常容易理解,因为他们不仅需要比较 name,还需要比较 ID。但是 p1 即等于 e1 也等于 e2,这是非常奇怪的,因为 e1、e2 明明是两个不同的类,但为什么会出现这个情况?首先 p1.equals(e1),是调用 p1 的 equals 方法,该方法使用 instanceof 关键字来检查 e1 是否为 Person 类,这里我们再看看 instanceof:判断其左边对象是否为其右边类的实例,也可以用来判断继承中的子类的实例是否为父类的实现。他们两者存在继承关系,肯定会返回 true 了,而两者 name 又相同,所以结果肯定是 true。所以出现上面的情况就是使用了关键字 instanceof,这是非常容易导致我们“钻牛角尖”。故在覆写 equals 时推荐使用 getClass 进行类型判断。而不是使用 instanceof(除非子类拥有统一的语义)。


7.编写一个完美equals()的几点建议


下面给出编写一个完美的equals方法的建议(出自Java核心技术 第一卷:基础知识):


1)显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量(参数名命名,强制转换请参考建议5)


2)检测this与otherObject是否引用同一个对象 :if(this == otherObject) return true;(存储地址相同,肯定是同个对象,直接返回true)


3) 检测otherObject是否为null ,如果为null,返回false.if(otherObject == null) return false;


4) 比较this与otherObject是否属于同一个类 (视需求而选择)


  • 如果equals的语义在每个子类中有所改变,就使用getClass检测 :if(getClass()!=otherObject.getClass()) return false; (参考前面分析的第6点)

  • 如果所有的子类都拥有统一的语义,就使用instanceof检测 :if(!(otherObject instanceof ClassName)) return false;(即前面我们所分析的父类car与子类bigCar混合比,我们统一了批次相同即相等)


5) 将otherObject转换为相应的类类型变量:ClassName other = (ClassName) otherObject;


6) 现在开始对所有需要比较的域进行比较 。使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true,否则就返回flase。


  • 如果在子类中重新定义equals,就要在其中包含调用super.equals(other)

  • 当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明 相等对象必须具有相等的哈希码 。


参考资料:


Java核心技术 第一卷:基础知识


Java深入分析


http://wiki.jikexueyuan.com/project/java-enhancement/java-thirteen.html