一次性搞清楚equals和hashCode

news/2024/7/3 13:51:49

  在程序设计中,有很多的“公约”,遵守约定去实现你的代码,会让你避开很多坑,这些公约是前人总结出来的设计规范。

  Object类是Java中的万类之祖,其中,equals和hashCode是2个非常重要的方法。

  这2个方法总是被人放在一起讨论。最近在看集合框架,为了打基础,就决定把一些细枝末节清理掉。一次性搞清楚!

下面开始剖析。

 

 

public boolean equals(Object obj)

 

  Object类中默认的实现方式是  :   return this == obj  。那就是说,只有this 和 obj引用同一个对象,才会返回true。(如果我们没有重写equals方法,equals和==一样比较的都是内存地址,只是equals可以让我们重写。)

  而我们往往需要用equals来判断 2个对象是否等价,而非验证他们的唯一性。这样我们在实现自己的类时,就要重写equals.

 

按照约定,equals要满足以下规则。

 

自反性:  x.equals(x) 一定是true

对null:  x.equals(null) 一定是false

对称性:  x.equals(y)  和  y.equals(x)结果一致

传递性:  a 和 b equals , b 和 c  equals,那么 a 和 c也一定equals。

一致性:  在某个运行时期间,2个对象的状态的改变不会不影响equals的决策结果,那么,在这个运行时期间,无论调用多少次equals,都返回相同的结果。

 

 一个例子

复制代码
 1 class Test
 2 {
 3     private int num;
 4     private String data;
 5 
 6     public boolean equals(Object obj)
 7     {
 8         if (this == obj)
 9             return true;
10 
11         if ((obj == null) || (obj.getClass() != this.getClass()))
12             return false;
13 
//能执行到这里,说明obj和this同类且非null。 14 Test test = (Test) obj; 15 return num == test.num&& (data == test.data || (data != null && data.equals(test.data))); 16 } 17 18 public int hashCode() 19 { 20 //重写equals,也必须重写hashCode。具体后面介绍。
24 } 25 26 }
复制代码

 

 

 

equals编写指导

Test类对象有2个字段,num和data,这2个字段代表了对象的状态,他们也用在equals方法中作为评判的依据。

在第8行,传入的比较对象的引用和this做比较,这样做是为了 save time ,节约执行时间,如果this 和 obj是 对同一个堆对象的引用,那么,他们一定是qeuals 的。

接着,判断obj是不是为null,如果为null,一定不equals,因为既然当前对象this能调用equals方法,那么它一定不是null,非null 和 null当然不等价。

然后,比较2个对象的运行时类,是否为同一个类。不是同一个类,则不equals。getClass返回的是 this 和obj的运行时类的引用。如果他们属于同一个类,则返回的是同一个运行时类的引用。注意,一个类也是一个对象。



1、有些程序员使用下面的第二种写法替代第一种比较运行时类的写法。应该避免这样做。

 

复制代码
if((obj == null) || (obj.getClass() != this.getClass())) 

return false; if(!(obj instanceof Test))

return false; // avoid 避免!
复制代码

 

 

它违反了公约中的对称原则。


例如:假设Dog扩展了Aminal类。

dog instanceof Animal      得到true

animal instanceof Dog      得到false

 

这就会导致

animal.equls(dog) 返回true
dog.equals(animal) 返回false

仅当Test类没有子类的时候,这样做才能保证是正确的。

 


2、按照第一种方法实现,那么equals只能比较同一个类的对象,不同类对象永远是false。但这并不是强制要求的。一般我们也很少需要在不同的类之间使用equals。

3、在具体比较对象的字段的时候,对于基本值类型的字段,直接用 == 来比较(注意浮点数的比较,这是一个坑)对于引用类型的字段,你可以调用他们的equals,当然,你也需要处理字段为null 的情况。对于浮点数的比较,我在看Arrays.binarySearch的源代码时,发现了如下对于浮点数的比较的技巧: 

if ( Double.doubleToLongBits(d1) == Double.doubleToLongBits(d2) ) //d1 和 d2 是double类型

if(  Float.floatToIntBits(f1) == Float.floatToIntBits(f2)  )      //f1 和 f2 是d2是float类型

 

 

4、并不总是要将对象的所有字段来作为equals 的评判依据,那取决于你的业务要求。比如你要做一个家电功率统计系统,如果2个家电的功率一样,那就有足够的依据认为这2个家电对象等价了,至少在你这个业务逻辑背景下是等价的,并不关心他们的价钱啊,品牌啊,大小等其他参数。

5、最后需要注意的是,equals 方法的参数类型是Object,不要写错!

 

 

 

 

public int hashCode()


这个方法返回对象的散列码,返回值是int类型的散列码。
对象的散列码是为了更好的支持基于哈希机制的Java集合类,例如 Hashtable, HashMap, HashSet 等。


关于hashCode方法,一致的约定是:

重写了euqls方法的对象必须同时重写hashCode()方法。

如果2个对象通过equals调用后返回是true,那么这个2个对象的hashCode方法也必须返回同样的int型散列码

如果2个对象通过equals返回false,他们的hashCode返回的值允许相同。(然而,程序员必须意识到,hashCode返回独一无二的散列码,会让存储这个对象的hashtables更好地工作。)

 

在上面的例子中,Test类对象有2个字段,num和data,这2个字段代表了对象的状态,他们也用在equals方法中作为评判的依据。那么, 在hashCode方法中,这2个字段也要参与hash值的运算,作为hash运算的中间参数。这点很关键,这是为了遵守:2个对象equals,那么 hashCode一定相同规则。

也是说,参与equals函数的字段,也必须都参与hashCode 的计算。

 

 
合乎情理的是:同一个类中的不同对象返回不同的散列码。典型的方式就是根据对象的地址来转换为此对象的散列码,但是这种方式对于Java来说并不是唯一的要求的
的实现方式。通常也不是最好的实现方式。

相比 于 equals公认实现约定,hashCode的公约要求是很容易理解的。有2个重点是hashCode方法必须遵守的。约定的第3点,其实就是第2点的
细化,下面我们就来看看对hashCode方法的一致约定要求。


第一:在某个运行时期间,只要对象的(字段的)变化不会影响equals方法的决策结果,那么,在这个期间,无论调用多少次hashCode,都必须返回同一个散列码。


第二:通过equals调用返回true 的2个对象的hashCode一定一样。


第三:通过equasl返回false 的2个对象的散列码不需要不同,也就是他们的hashCode方法的返回值允许出现相同的情况。

总结一句话:等价的(调用equals返回true)对象必须产生相同的散列码。不等价的对象,不要求产生的散列码不相同。

 

 

 

hashCode编写指导

 

在编写hashCode时,你需要考虑的是,最终的hash是个int值,而不能溢出。不同的对象的hash码应该尽量不同,避免hash冲突。

那么如果做到呢?下面是解决方案。

 

1、定义一个int类型的变量 hash,初始化为 7。

接下来让你认为重要的字段(equals中衡量相等的字段)参入散列运,算每一个重要字段都会产生一个hash分量,为最终的hash值做出贡献(影响)

 

运算方法参考表
重要字段var的类型他生成的hash分量
byte, char, short , int(int)var
long (int)(var ^ (var >>> 32))
booleanvar?1:0
float Float.floatToIntBits(var)
 double long bits = Double.doubleToLongBits(var);
分量 = (int)(bits ^ (bits >>> 32));
 引用类型  (null == var ? 0 : var.hashCode())

 

 

 

最后把所有的分量都总和起来,注意并不是简单的相加。选择一个倍乘的数字31,参与计算。然后不断地递归计算,直到所有的字段都参与了。

复制代码
int hash = 7;

hash = 31 * hash + 字段1贡献分量;

hash = 31 * hash + 字段2贡献分量;

.....

return hash;

 

 

 

总结:

  == 和  equals()都是比较内存地址,只是==不能重写,而equals可以重写

  hashcode返回一个int型的hash地址。

 

例如:

package CollectionTest;

public class Person {

    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public int hashCode() {
        // TODO Auto-generated method stub
        return this.name.hashCode()+age;
    }
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Person)) {
            return false;
        }
        Person p = (Person) obj;
        return this.name.equals(p.name) && this.age == p.age;
    }
    protected Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
    
}

 

测试:

    public static void main(String[] args) {
        Person p1 = new Person("qz", 25);
        Person p2 = new Person("qz", 25);
        System.out.println(p1.equals(p2));//true
        System.out.println(p1==p2);//false
        System.out.println(p1.toString());//CollectionTest.Person@e42
    }

 

 

1.Java中String的hashCode()与equals()源码查看:

String实现原理(基于char[]实现):不可变类,每次值都是存放在char[]数组中,并且进行深复制。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

    private final char value[];

    private int hash; // Default to 0
    
    ,,,
}

 

 

 

hashCode()源码查看:

    public int hashCode() {
        int h = hash;//默认为0
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

 

 

 

看出来String是遍历每个char,h乘以31加上对应char的ASCII码。

验证:

        String s1 = "a";
        String s2 = "b";
        System.out.println(s1.hashCode());//97
        System.out.println(s2.hashCode());//98

 

 

 

equals(obj)源码查看:  是将形参转变为String,然后遍历里面的char[],两个char[]进行依次对比。也就是比较字符串的值是否相等。

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String) anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                            return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

 

 

 

我们利用两个hashCode相等的字符串作为key存入map,查看:

package cn.qlq.test;

import java.util.HashMap;

public class ArrayTest {
    public static void main(String[] args) {
        String s1 = "Aa";
        String s2 = "BB";
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());

        HashMap map = new HashMap();
        map.put(s1, "xxx");
        map.put(s2, "xxxdddd");
        System.out.println(map);
    }

}

2112
2112
{BB=xxxdddd, Aa=xxx}

 

"Aa" 与"BB"的hashCode相等,那么是如何存入map的?--验证hashmap的实现原理基于数据+链表

     

  先存入Aa,并放在第五个数组位置,当存BB的时候发现hashCode一样,会将BB存到第五个位置,并将第五个位置元素的next(也是一个Entry)存为Aa。也就是数组加链表实现原理。

 

 

2.8种基本数据类型的包装类型研究,以Integer为例

 先看一段代码:

package cn.qlq.test;

public class ArrayTest {
    public static void main(String[] args) {
        Integer i1 = new Integer(1);
        Integer i2 = new Integer(1);
        System.out.println(i1.hashCode());
        System.out.println(i2.hashCode());
        System.out.println(i1 == i2);
        System.out.println(i1.equals(i2));

        System.out.println("-------------------");

        Integer i3 = 1;
        Integer i4 = 1;
        System.out.println(i3.hashCode());
        System.out.println(i4.hashCode());
        System.out.println(i3 == i4);
        System.out.println(i3.equals(i4));

    }

}

 

结果:

1
1
false
true
-------------------
1
1
true
true

 

 

解释:Integer i1 = new Integer(1)的时候是在Java堆中创建一个Integer对象,同时将1存到常量池,i1指向堆中的对象,i1与常量池没关系,所以i1==i2为false。

  Integer i3=1;的时候是从常量池中查找值为1的常量,i3指向该常量;Integer i4=1的时候会直接指向该常量,所以 i3 == i4为true。注意integer的常量池只有-128到127.测试如下:

package cn.qlq.test;

public class ArrayTest {
    public static void main(String[] args) {
        
        Integer i3 = -128;
        Integer i4 = -128;
        Integer i33 = 128;
        Integer i44 = 128;
        System.out.println(i3 == i4);
        System.out.println(i33 == i44);

    }

}

 

结果:

true
false

 

1.Integer的hashCode()实现原理---直接返回的是值

    public int hashCode() {
        return value;
    }

 

 

value是其内部的一个int属性:(---也是其不可变类的实现原理)

    public static Integer valueOf(String s, int radix) throws NumberFormatException {
        return Integer.valueOf(parseInt(s,radix));
    }

    public static Integer valueOf(String s) throws NumberFormatException {
        return Integer.valueOf(parseInt(s, 10));
    }


    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
        }

        private IntegerCache() {}
    }

    public static Integer valueOf(int i) {
        assert IntegerCache.high >= 127;
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }


    private final int value;


    public Integer(int value) {
        this.value = value;
    }

 

 

2.Integer的equals(obj)源码查看:  也是直接 比较两者的内部的value值

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

 

 

 

   也就是8种基本数据类型的包装类型和包装类型比较值的时候用equals比较,比较内存的时候用  ==  

   其他我们的引用类型一般equals是比较内存地址,我们可以重写equals()方法进行比较。

 


http://www.niftyadmin.cn/n/1965487.html

相关文章

硬科技企业大放异彩,Nibiru、柔宇斩获国家优质投资项目特别奖

2016—2017年度国家优质投资项目推介表彰大会在京举行。 近日&#xff0c;由国家发展改革委主管&#xff0c;中国投资协会主办的2016—2017年度国家优质投资项目推介表彰活动的审定结果在北京钓鱼台国宾馆揭晓。中国投资协会会长杨庆蔚&#xff0c;中国投资协会副会长、创投委…

VirtualBox虚拟机Ubuntu设置共享文件夹,并自动挂载

一、环境 Win10系统&#xff0c;VirtualBox-5.1.22-115126Ubuntu16.04&#xff08;64位&#xff09;虚拟机二、目的在Ubuntu中能够共享Win10中的某个文件夹&#xff0c;而且能够自动挂载三、设置共享文档夹1) 安装好Ubuntu后&#xff0c;运行并登录2) 点击"设备(Devices) …

笨方法学习Python31-40

31、作出决定复习了raw_input("> ")if 变量 "Vaule":执行语句32、循环和列表count [1, 2, 3, 4, 5]for i in count:print "This is %d" % i #循环打印出count的值count.append(6) #从最后增加值count.insert(1,44) #从第…

Shell脚本入门-4

2019独角兽企业重金招聘Python工程师标准>>> 我们可以在执行 Shell 脚本时&#xff0c;向脚本传递参数&#xff0c;脚本内获取参数的格式为&#xff1a;$n。n 代表一个数字&#xff0c;1 为执行脚本的第一个参数&#xff0c;2 为执行脚本的第二个参数&#xff0c;以…

Docker初体验——踩过的那些坑!

2018‎年‎3‎月‎6‎日 Docker安装 环境&#xff1a;windows7 安装包&#xff1a;DockerToolbox-17.10.0-ce.exe &#xff08;下载地址&#xff1a;http://mirrors.aliyun.com/doc...&#xff09; 坑&#xff1a; //启动包错&#xff1a; Running pre-create checks... (def…

双11购物节火热,谨防木马乘机而入

腾讯电脑管家 2015/11/12 16:240x00 概况近期11.11购物节&#xff0c;无数的网页、软件都充斥着“血拼双11”的广告&#xff0c;这时的电脑桌面如果多了几个双11相关的快捷方式&#xff0c;或者浏览器主页被锁定成推送网购内容的导航网站&#xff0c;你会不会认为这也是正常的…

Android应用安全开发之防范无意识的数据泄露

gh0stbo 2016/01/29 10:160x00 简介OWASP移动安全漏洞Top 10中第4个就是无意识的数据泄漏。当应用程序存储数据的位置本身是脆弱的时&#xff0c;就会造成无意识的数据泄漏。这些位置可能包括剪贴板&#xff0c;URL缓存&#xff0c;浏览器的Cookies&#xff0c;HTML5数据存储&…

HTML基础 结构,标题h1和段落p 写一个三毛语录

先看代码: 1 <!DOCTYPE html>2 <html>3 <head>4 <meta charset"utf-8">5 <title>HTML:基础教程</title>7 </head>8 <body>9 <h1>三毛语录</h1> 10 <p>1.不做不可及的梦&#xff0c;这使我的睡…