Java中==和equals、equals和hashCode的关系详解

==运算符

在java中==是运算符,用于比较两个变量是否相等,该操作符生成的是一个boolean结果

  • 基本数据类型,也称原始数据类型。byte,short,char,int,long,float,double,boolean, 它计算的是操作数的值之间的关系
  • 复合数据类型(类) ,当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象他们的比较后的才能得到为true的结果,否则比较后结果均为false;经典案例:超详细String讲解

equals(java.lang.Obejct中的实例方法)

官网文档

指示其他对象是否“等于”此对象。

equals方法实现了非空对象引用的等价关系:

它是自反的:对于任何非空参考值x, x.equals(x)应该返回true。

它是对称的:对于任何非空引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)应返回true。

它是可传递的:对于任何非空引用值x, y和z,如果x.equals(y)返回true, y.equals(z)返回true,那么x.equals(z)应该返回true。

它是一致的:对于任何非空的参考值x和y, x.equals(y)的多次调用一致地返回true或一致地返回false,前提是在对象的相等比较中使用的信息没有被修改。

对于任何非空参考值x, x.equals(null)应该返回false。

Object类的equals方法实现了对象上最具鉴别性的等价关系;也就是说,对于任何非空引用值x和y,当且仅当x和y指向同一对象时,该方法返回true (x == y的值为true)。

请注意,通常需要在重写hashCode方法时重写该方法,以便维护hashCode方法的一般契约,该契约规定相等的对象必须具有相等的哈希码。

参数:要与之比较的引用对象。

返回:如果该对象与obj参数相同,则为True;否则错误。

参见:java.util.HashMap hashCode ()

equals是Objec类的方法,用于比较两个对象是否相等,作用和==是一样的,Object的equals方法如下:

    public boolean equals(Object obj) {
        return (this == obj);
    }

JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地 址,但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了

案例代码

String aaa = new String("aaa");
System.out.println("aaa" == aaa); //false
System.out.println("aaa".equals(aaa)); //true

对象aaa存储再堆中,字符串aaa存储在常量池里,所以用==肯定是返回false,而String对equals方法进行了重写,可见String中equals的重写策略是,①判断是不是String类似②长度是否一样③每个组成字符串的字符是否完全一致

 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;
    }

或者根据我们自己的业务逻辑定义equals,如下,我定义的equals是判断这个对象的各个属性是否一致

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    private String name;
    private String age;
    private String sex;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person Person = (Person) o;
        return Objects.equals(name, Person.name) && Objects.equals(age, Person.age) && Objects.equals(sex, Person.sex);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, sex);
    }

    public static void main(String[] args) {
        Person person = new Person("wzh", "22", "man");
        Person person2 = new Person("wzh", "22", "man");
        System.out.println(person == person2); //false
        System.out.println(person.equals(person)); //true
    }
}

在正式讲解equals和hashCode的关系前,我们有必要讲下hash和相关集合案例

哈希表

概念 : Hash 就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出(int),该输出就是散列值。这种转换是一种 压缩映射,也就是说,散列值的空间通常远小于输入的空间。不同的输入可能会散列成相同的输出,从而不可能从散列值来唯一的确定输入值

HashMap

  • HashMap = 数据 + 链表,jdk8后HashMap = 数据 + 链表 + 红黑树,是基于哈希表和Map接口实现的,k-v位置都允许出null,但k位置只能有一个null,是无序的存储键值对(映射关系)的集合
  • HashMap中的key是Set类型,无序不可重复;Value是Collection类型,推荐重写equals方法,无序可重复,一个键值对就是一个Entry对象,map中的entry无序,不可重复,用Set存储
  • 底层实现原理,以put方法为例如下
Map map = new HashMap(); //初始化对象时底层没有变化
put数据的过程中底层会创建一个大小为16位的node数组,根据插入键值对的key值计算哈希值,
根据哈希值推算出待插入位置的数组
      如果数组位置是空就直接插入
      如果插入位置有一个或链表形式的数据,那么首先对比哈希值
            如果哈希值都不相同那么插入成功,尾插法
            如果哈希值相同,那么调用equals来对比两键值的key
                 返回false则插入成功,尾插
                 返回true,那么在key不变的情况下,用新的value替换旧的value

插入元素注意点:
1、初始设容量为16,加载因素0.75,加载因子就是12,也就是说只要加入的元素个数超过12个就会进行扩容,
无论这个元素就加到链表头(数组第一个)或者是加在链表上或者加载了红黑树上,都算是添加元素的个数,扩容到原
来的二倍然后得出新的临界值 
2、插入到数组统一位置的元素开始是链表结构,只有当数组容量>=64且当前数组位置链表格个数>=8,当前链表会转为红黑树结构存储
3、hashmap初始化容量为为2的幂,这是通过构造方法内不断进行左移运算实现的,而且扩容默认位2倍,也就是
hashmap容量始终为2的幂,插入元素时,如果使用%计算元素的落脚点效率很低,使用哈希值和map的length-1进行与运算这种算法效率更高,而且这样可以减少hash碰撞的概率
4、这种插入策略①

HashCode(java.lang.Obejct中的native方法)

源码(java.lang.Object)中的,是本地方法

public native int hashCode();

官网介绍

1、hashcode方法返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。 

2、hashCode 的常规协定是: 
在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。
从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。 

3、如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。 

以下情况不是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。
但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。 实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。
(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。) 

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

我对官网文档总结下

  • 规则一:hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;这点和我们讲的HashMap知识点前后呼应
  • 规则二:如果两个对象a和b相同,那么a.equals(b) 一定要返回true,且a和b的hashCode值一定相同
  • 在实际开发中实体类中的equals和hashCode要么一起重写要么都不重写,否则就不会满足上述规则,因为Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。这一般是通过将该对象的内部地址转换成一个整数来实现的,Object 类定义的equals则是直接比较两个对象的地址,判断两个对象是否指向同一块堆空间,重写时也要满足上述规则2,比如以下案例,重写的equals通过对比对象的每个属性是否一致判断两个对象是否相同,hashcode通过每个属性计算得到,这样就很好的满足了规则二
 @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person Person = (Person) o;
        return Objects.equals(name, Person.name) && Objects.equals(age, Person.age) && Objects.equals(sex, Person.sex);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, sex);
    }
  • 规则三:两个对象a,b的hash值相同不一定代表这两个对象就是同一个对象,而且a.equals(b)也不一定就返回false,因为我们重写后的hashCode说穿了就是调用了java.util.Objects中的hash方法,源码如下
   public static int hash(Object... values) {
        return Arrays.hashCode(values);
    }

java.util.Arrays中的hashCode

 public static int hashCode(Object a[]) {
        if (a == null)
            return 0;

        int result = 1;

        for (Object element : a)
            result = 31 * result + (element == null ? 0 : element.hashCode());

        return result;
    }

所以根据源码我们不难看出不同的对象间也可能会得到相同的hash值,官方也说了:不同的对象一定要有不同的hash值不是必须的,但官方同时也推荐开发者设计的hash算法尽量保证不同的对象有不同的hash值,这样可以有效避免哈希碰撞的概率,使得插入的节点均匀的分布在hashmap上,这样可以提高程序运行效率;

总之:hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的

我们再深入追究下,为什么如果两个对象a和b相同,那么a.equals(b) 一定要返回true,且a和b的hashCode值一定相同???

在用到hash进行管理的数据结构中,比如hashmap,hash值(key)存在的目的是①能用来计算出合理的节点插入位置②可以从hashmap快速取出我们需要的值,key的作用是为了将元素适当地放在各个桶里,对于抗碰撞的要求没有那么高。换句话说,hash出来的key,只要保证value大致均匀的放在不同的桶里就可以了

我们用反证法,比如上述的Person类,我们重写equals但没有重写hashcode,Person类中我们的策略其实是判断对象的属性只要完全一直我们就认为这两个对象相同,同时重写equals和hashCode,那么我们讲person作为key插入节点node时,属性相同的person计算的hash值一样那么会落到hashmap的同一个位置上直接比较、然后插入。反正,如果我们重写equals但每天重写hashCode,那么不同的person对象都会被计算出不同的hash值,上述已经说了Object中的hashCode是根据对象的地址来计算得出的,那就可以看成是随机分配,比如person和person2我们通过重写equals返回为true,但由于hashCode没重写,那就会得到不同hashcode,被分配到hashmap不同的位置,压根没有通过equals判重的机制,造成hashmap出现相key相同的问题,案例如下

上述Person类中测试代码,同时重写equals和hashCode

    public static void main(String[] args) {
        Person person = new Person("wzh", "22", "man");
        Person person2 = new Person("wzh", "22", "man");
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put(person,"1");
        hashMap.put(person2,"2");
        for (Object o : hashMap.keySet()) {
            System.out.println("key=" +o +"," + "value=" +hashMap.get(o));
        }
    }

在这里插入图片描述
测试没有问题

上述Person类中测试代码,重写equals但不重写hashCode,也出现了我们预期的异常效果
在这里插入图片描述

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值