Java中==、equals()、hashcode()三者的理解

转载请注明出处:http://blog.csdn.net/li0978/article/details/53519268
以前看了有关==、equals()、hashcode()这三者的区别和使用,当时感觉就这一点知识能够记住,最近用到这块内容脑海中只有一丝印象却不知具体如何,好记性不如赖笔头,遂再重新总结一下,以便日后信手拈来。

==

对于引用对象而言,比较两个对象引用的是否是同一个对象。比较是的两个引用对象的存储地址是否一样。
对于基本数据类型而言,比较的就是两个数据的值是否相等。

String a = "aaa";
String b = "aaa";
String c = new String("aaa");
System.out.println(a==b);    //比较1
System.out.println(a==c);     //比较2

结果是:
true
false

解释:

  1. String a = “aaa”这种方式的时候java首先在内存中寻找”aaa”字符串,如果有,就把aaa的地址给它,如果没有则创建。因此比较1结果返回true;
  2. String c = new String(“aaa”)这种方式不论内存中是否存在“aaa”都会开辟一个新的空间来存储c对象,比较2两个引用的地址是不一样的,因此返回false;

equals()

针对对象而言,equals()和==效果是一样的。但是有一个这样的需求:发新书的时候,两个同学都发了一本语文书,这时候根据书的外观和内容我们可以说这两本语文书是一样的。这里的equals()比较的是两个对象的某些属性值是否相等从而判断两个对象(内容)是否相等。

String c = new string("aaa");
String d = new String("aaa");
System.out.println(c.equals(d));

结果是:
true
深入源码能够发现String类中equals()方法已经进行重写过了。另外JDK中基本数据类型的包装类中equals()方法也已经重写过了,比较的时候都是比较的两个对象的值。

为什么重写equals()

就像上边的需求,有些时候仅仅是两个对象的某些属性相同就认为两个对象一样,这个时候我们一般要重写对象的equals()方法,重写的目的就是根据某些特定的属性来判断两个对象的是否相等。

在重写equals方法时,要注意满足离散数学上的特性:

  • 自反性 :对任意引用值X,x.equals(x)的返回值一定为true;
  • 对称性: 对于任何引用值x,y,当且仅当y.equals(x)返回值为true时,x.equals(y)的返回值一定为true;
  • 传递性:如果x.equals(y)=true, y.equals(z)=true,则x.equals(z)=true;
  • 一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变;
  • 非空性:任何非空的引用值X,x.equals(null)的返回值一定为false;

hashCode()

hashCode()返回的是一个hashCode(hash码),hash码主要用于散列对象作为KEY来标示这个对象的存储位置,功能类似索引一样能够快速查找提取对象。由此可知每个对象的hash码是唯一的(在对象的内存地址基础上经过特定算法返回一个hash码)。

为什么重写hashcode()

一般情况下我们是不需要重写hashcode()的,Object.hashCode生成规则也有通用约定:

  1. 在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,那么,对该对象调用hashCode方法多次,它必须始终如一地返回 同一个整数。在同一个应用程序的多次执行过程中,这个整数可以不同,即这个应用程序这次执行返回的整数与下一次执行返回的整数可以不一致。
  2. 如果两个对象根据equals(Object)方法是相等的,那么调用这两个对象中任一个对象的hashCode方法必须产生同样的整数结果。
  3. 如果两个对象根据equals(Object)方法是不相等的,那么调用这两个对象中任一个对象的hashCode方法,不要求必须产生不同的整数结果。然而,程序员应该意识到这样的事实,对于不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。

上边可用一句话说明:我们认为两个对象相同,则两个对象的hashcode必定一样。我们重写equals()方法仅仅是判定若干个属性是否相等(这两个对象默认不重写情况下有可能是不一样的,hashcode默认也是不一样),所以要重写hashcode令其在若干属性相等下也一样。

一般我们在集合(例如list)中存对象时hashcode所扮演的角色并不重要,但是当对象需要放在HashTable、HashMap、HashSet等hash结构的集合时候,我们如果重写equals()方法必须就重写hashcode()方法,因为在hash结构的集合中存储对象就是通过hash算法来散列对象的。假如这里有一些箱子(bucket,一个bucket可存放多个元对象,参考HashMap原理),hash码可以看成每一个箱子指定的编码,每一个元对象就是根据箱子的编码存入每一个箱子的,所有的箱子加起来就是一个HashSet,HashMap,或 Hashtable对象。当我们想找某一个元对象的时候,我们必须先指定他所在那个箱子的hash码,才能找到那个箱子,然后根据对象的值从那个箱子中拿到这个对象。所以得出的结论是:

在java集合中判断两个对象是否相同,先判断两个对象的hashcode是否相等,再采用equals()判断两个对象的值是否相等。即:

  • 如果x.equals(y)返回“true”,那么x和y的hashCode()必须相等。
  • 如果x.equals(y)返回“false”,那么x和y的hashCode()有可能相等,也有可能不等。

equals()和hashcode()重写

由上可知我们equals()和hashcode()的方法重写是成对的,重写equals()方法必须重写hashcode()方法。

equals()方法的重写规则:

  1. 使用instanceof操作符检查“实参是否为正确的类型”。
  2. 对于类中的每一个“关键域”,检查实参中的域与当前对象中对应的域值。
    [2.1]对于非float和double类型的原语类型域,使用==比较;
    [2.2]对于对象引用域,递归调用equals方法;
    [2.3]对于float域,使用Float.floatToIntBits(afloat)转换为int,再使用==比较;
    [2.4]对于double域,使用Double.doubleToLongBits(adouble) 转换为int,再使用==比较;
    [2.5]对于数组域,调用Arrays.equals方法。

hashcode()方法的重写规则:

  1. 把某个非零常数值,例如17,保存在int变量result中;
  2. 对于对象中每一个关键域f(指equals方法中考虑的每一个域):
    [2.1]boolean型,计算(f ? 0 : 1);
    [2.2]byte,char,short型,计算(int);
    [2.3]long型,计算(int) (f ^ (f>>>32));
    [2.4]float型,计算Float.floatToIntBits(afloat);
    [2.5]double型,计算Double.doubleToLongBits(adouble)得到一个long,再执行[2.3];
    [2.6]对象引用,递归调用它的hashCode方法;
    [2.7]数组域,对其中每个元素调用它的hashCode方法。
  3. 将上面计算得到的散列码保存到int变量c,然后执行 result=37*result+c;
  4. 返回result。
public class TestBean {
    private int mInt;
    private short mShort;
    private char mChar;
    private byte mByte;
    private boolean mBoolean;
    private long mLong;
    private float mFloat;
    private double mDouble;
    private Object mObject;
    private TestBean[] mArray;

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof TestBean))
            return false;
        TestBean tb = (TestBean) o;
        return tb.mInt == mInt
                &&tb.mShort == mShort
                && tb.mChar == mChar
                && tb.mByte == mByte
                && tb.mBoolean == mBoolean
                && tb.mLong == mLong
                && Float.floatToIntBits(tb.mFloat) == Float.floatToIntBits(mFloat)
                && Double.doubleToLongBits(tb.mDouble) == Double.doubleToLongBits(mDouble)
                && tb.mObject.equals(mObject)
                && Arrays.equals(mArray, tb.mArray);
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + mInt;
        result = 31 * result + (int) mShort;
        result = 31 * result + (int) mChar;
        result = 31 * result + (int) mByte;
        result = 31 * result + (mBoolean ? 0 : 1);
        result = 31 * result + (int) (mLong ^ (mLong >>> 32));
        result = 31 * result + Float.floatToIntBits(mFloat);
        long doubleToLong = Double.doubleToLongBits(mDouble);
        result = 31 * result + (int) (doubleToLong ^ (doubleToLong >>> 32));
        result = 31 * result + mObject.hashCode();
        for (int i = 0; i < mArray.length; i++) {
            result = 31 * result + mArray[i].hashCode();
        }
        return result;
    }
}

这里的31和17仅仅作为一个因子,其实取什么值都可以的,hashcode是一个对象存储位置的标示,因子取值的标准就是最终的得到的结果要尽可能的分散。至于为什么都是取的31,这里大概有这几个原因:

  • 31是一个奇素数,奇素数作用就是如果我用一个数字来乘以这个素数,那么最终的出来的结果只能被素数本身和被乘数还有1来整除!。(减少冲突)
  • 31可以由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化.(提高算法效率)
  • 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
  • 并且31只占用5bits,相乘造成数据溢出的概率较小。

参考

猜你喜欢

转载自blog.csdn.net/li0978/article/details/53519268