Java中的equals和hashCode方法详解

来源:互联网 发布:手机短信群发的软件 编辑:程序博客网 时间:2024/05/16 10:39

散列集的概念

    链表和数组可以按照人们的医院排列元素的次序,但是,如果想要查看某个指定的元素,却又忘了它的位置,就需要去遍历访问所有元素,知道找到为止。如果集合中包含的元素很多,将会消耗很多的时候。有种数据结构,可以快速地查找所需要的对象,这就是散列表。    散列表为每一个对象计算一个证书,成为散列码(hash code)。散列码是由对象的实例产生的一个整数。具体怎么产生的,下面将会说明。    在java中,散列表由链表数组实现。每个列表被称为桶。要想查找表中的对象位置,就要先计算出它的散列码,然后与桶的总数取余,得到的结果就是保存这个元素的桶的索引。    例如,如果某个对象的散列码为76268,并且有128个桶,对象就应该保存在第108号桶中(76268除以128余108)。如果此时桶为空,直接插入便是。若桶被占满了,就会发生散列冲突(hash collision).    如果想更多地控制散列表的运行性能,就要指定一个初始的桶数。例如:在hashSet初始化方法中 HashSet(int initialCapacity),可以指定桶数大小。    通常将桶数设置为预计元素个数的75%-150%,如果散列表太满,就需要再散列。如果要对散列表再散列,就需要创建一个桶数更多的表,将所有元素插入新的表中,然后丢弃原来的表。装填因子(load factor)决定何时对散列表进行再散列。例如,如果装填因子为0.75(默认值),而表中超过75%的位置已经填入元素,之歌表就会用双倍的桶数进行再散列。

理解了散列集的概念之后,我们再来讨论hashCode和equals方法。
equals方法
Object类中的equals方法用于检测一个对象是否等于另一个对象。在Object类中,这个方法是判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,那么他们一定是相等的。但是,这种比较大多数没有太大意义。比如:两个PrintStream对象是是否相等就没有什么意义。因此,我们在大多数情况都是要比较两个对象的状态的相等性。比如:在自定义对象中两个员工的名字,薪水,入职时间一致,我们就会认为是相等的。
hashCode方法
hashCode方法会返回由对象的存储地址经过计算得到的一个整型值。因为是Object类的方法,因此每一个对象都会有一个散列码。

   在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。        以下情况不是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。        实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 Java 编程语言不需要这种实现技巧。)            当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

测试一:覆盖equals(Object obj)但不覆盖hashCode(),导致数据不唯一性

public class HashCodeTest {    public static void main(String[] args) {        Collection set = new HashSet();        Point p1 = new Point(1, 1);        Point p2 = new Point(1, 1);        System.out.println(p1.equals(p2));        set.add(p1);   //(1)        set.add(p2);   //(2)        set.add(p1);   //(3)        Iterator iterator = set.iterator();        while (iterator.hasNext()) {            Object object = iterator.next();            System.out.println(object);        }    }}class Point {    private int x;    private int y;    public Point(int x, int y) {        super();        this.x = x;        this.y = y;    }    @Override    public boolean equals(Object obj) {        if (this == obj)            return true;        if (obj == null)            return false;        if (getClass() != obj.getClass())            return false;        Point other = (Point) obj;        if (x != other.x)            return false;        if (y != other.y)            return false;        return true;    }    @Override    public String toString() {        return "x:" + x + ",y:" + y;    }}

输出结果:
true
x:1,y:1
x:1,y:1
原因分析:
(1)当执行set.add(p1)时(1),集合为空,直接存入集合;
(2)当执行set.add(p2)时(2),首先判断该对象(p2)的hashCode值所在的存储区域是否有相同的hashCode,因为没有覆盖hashCode方法,所以jdk使用默认Object的hashCode方法,返回内存地址转换后的整数,因为不同对象的地址值不同,所以这里不存在与p2相同hashCode值的对象,因此jdk默认不同hashCode值,equals一定返回false,所以直接存入集合。
(3)当执行set.add(p1)时(3),时,因为p1已经存入集合,同一对象返回的hashCode值是一样的,继续判断equals是否返回true,因为是同一对象所以返回true。此时jdk认为该对象已经存在于集合中,所以舍弃。

测试二:覆盖hashCode方法,但不覆盖equals方法,仍然会导致数据的不唯一性
修改Point类:

public class Point {    private int x;    private int y;    public Point(int x, int y) {        super();        this.x = x;        this.y = y;    }    @Override    public int hashCode() {        final int prime = 31;        int result = 1;        result = prime * result + x;        result = prime * result + y;        return result;    }    @Override    public String toString() {        return "x:" + x + ",y:" + y;    }}

输出结果:
false
x:1,y:1
x:1,y:1
原因分析:
(1)当执行set.add(p1)时(1),集合为空,直接存入集合;
(2)当执行set.add(p2)时(2),首先判断该对象(p2)的hashCode值所在的存储区域是否有相同的hashCode,这里覆盖了hashCode方法,p1和p2的hashCode相等,所以继续判断equals是否相等,因为这里没有覆盖equals,默认使用’==’来判断,所以这里equals返回false,jdk认为是不同的对象,所以将p2存入集合。
(3)当执行set.add(p1)时(3),时,因为p1已经存入集合,同一对象返回的hashCode值是一样的,并且equals返回true。此时jdk认为该对象已经存在于集合中,所以舍弃。

综合上述两个测试,要想保证元素的唯一性,必须同时覆盖hashCode和equals才行。
(注意:在HashSet中插入同一个元素(hashCode和equals均相等)时,会被舍弃,而在HashMap中插入同一个Key(Value 不同)时,原来的元素会被覆盖。)

测试三:在内存泄露问题

public class HashCodeTest {    public static void main(String[] args) {        Collection set = new HashSet();        Point p1 = new Point(1, 1);        Point p2 = new Point(1, 2);        set.add(p1);        set.add(p2);        p2.setX(10);        p2.setY(10);        set.remove(p2);        Iterator iterator = set.iterator();        while (iterator.hasNext()) {            Object object = iterator.next();            System.out.println(object);        }    }}class Point {    private int x;    private int y;    public Point(int x, int y) {        super();        this.x = x;        this.y = y;    }    public int getX() {        return x;    }    public void setX(int x) {        this.x = x;    }    public int getY() {        return y;    }    public void setY(int y) {        this.y = y;    }    @Override    public int hashCode() {        final int prime = 31;        int result = 1;        result = prime * result + x;        result = prime * result + y;        return result;    }    @Override    public boolean equals(Object obj) {        if (this == obj)            return true;        if (obj == null)            return false;        if (getClass() != obj.getClass())            return false;        Point other = (Point) obj;        if (x != other.x)            return false;        if (y != other.y)            return false;        return true;    }    @Override    public String toString() {        return "x:" + x + ",y:" + y;    }}

原因分析:
假设p1的hashCode为1,p2的hashCode为2,在存储时p1被分配在1号桶中,p2被分配在2号筒中。这时修改了p2中与计算hashCode有关的信息(x和y),当调用remove(Object obj)时,首先会查找该hashCode值得对象是否在集合中。假设修改后的hashCode值为10(仍存在2号桶中),这时查找结果空,jdk认为该对象不在集合中,所以不会进行删除操作。然而用户以为该对象已经被删除,导致该对象长时间不能被释放,造成内存泄露。解决该问题的办法是不要在执行期间修改与hashCode值有关的对象信息,如果非要修改,则必须先从集合中删除,更新信息后再加入集合中。

总结:
1.hashCode是为了提高在散列结构存储中查找的效率,在线性表中没有作用。
2.equals和hashCode需要同时覆盖。
3.若两个对象equals返回true,则hashCode有必要也返回相同的int数。
4.若两个对象equals返回false,则hashCode不一定返回不同的int数,但为不相等的对象生成不同hashCode值可以提高 哈希表的性能。
5.若两个对象hashCode返回相同int数,则equals不一定返回true。
6.若两个对象hashCode返回不同int数,则equals一定返回false。
7.同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。

下面是一些小补充:

什么是内存泄露?
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏的定义:对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。
HashMap,有时候,我们一不小心把可变hashcode的对象作为HashMap的key,当你一不小心改变了对象的hashcode的时候,key就无法取出对应的value,此value对于你便不可达了。但是value的引用还是存在与HashMap框架内部的数组和链表结构中。而你又无可奈何,内存便”泄漏“了

为什么在定义hashcode时要使用31这个数呢?

public int hashCode() {          int h = hash;          int len = count;          if (h == 0 && len > 0) {              int off = offset;              char val[] = value;              for (int i = 0; i < len; i++) {                  h = 31*h + val[off++];              }              hash = h;          }          return h;      }  

注意上面的for循环,有点搞吧?我来举个例子,让你很容易明白它在搞什么名堂。比如有一个字符串“abcde”,采用31进制的计算方法来计算这个字符串的总和,你会写出下面的计算式子:
a*31^4+b*31^3+c*31^2+d*31^1+e*31^0.注意,这里的a,b,c,d或者e指的是它们的ASCII值。很有趣的循环,居然可以用来算N进制。
那么为什么选用31作为基数呢?先要明白为什么需要HashCode.每个对象根据值计算HashCode,这个code大小虽然不奢求必须唯一(因为这样通常计算会非常慢),但是要尽可能的不要重复,因此基数要尽量的大。另外,31*N可以被编译器优化为
左移5位后减1,有较高的性能。其实选用31还是有争议,反对者(参考http://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier)
认为这个东西还是会导致较多的重复,应该用更大的数字。所以,或许将来Java的实现中会有所变化。下面这篇文章介绍了两个结论:
1.基数要用质数
质数的特性(只有1和自己是因子)能够使得它和其他数相乘后得到的结果比其他方式更容易产成唯一性,也就是hash code值的冲突概率最小。
2.选择31是观测分布结果后的一个选择,不清楚原因,但的确有利。
http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/
另外,String.hashCode内部会缓存第一次计算的值,因为这是一个final(不可变)类,也就是String对象的内容是不会变的。这能够在多次put到HashMap的场合提高性能,不过似乎用处不多。

原创粉丝点击