当前位置: 首页 > news >正文

Java hashCodeequals的关系

一、基础概念:从 Object 类说起

在 Java 的世界里,Object类就像是一座大厦的基石,所有的类都直接或间接继承自它。而equals和hashCode这两个方法,就如同基石上的两颗关键铆钉,看似普通,却在对象的比较和存储中起着至关重要的作用。

1.1 equals 方法:对象相等性的核心判定

equals方法定义在Object类中,它的使命是判断两个对象是否相等。不过,其默认实现相当 “简单粗暴”,仅仅比较两个对象的引用地址。也就是说,只有当两个引用指向内存中的同一个对象实例时,equals方法才会返回true。这种默认行为在很多情况下并不能满足我们的需求,毕竟在实际开发中,我们往往更关注对象的内容是否相等,而非它们是否是同一个实例。

幸运的是,Java 中的许多类都对equals方法进行了重写,以实现更符合实际需求的比较逻辑。就拿String类来说,它重写后的equals方法会仔细比较两个字符串的字符序列内容。假设有如下代码:

String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1.equals(str2));

上述代码的输出结果将会是true,尽管str1和str2是不同的对象实例,但它们的字符序列内容完全一致,因此equals方法判定它们相等。

同样,包装类(如Integer、Double等)也重写了equals方法,用于比较数值内容。例如:

Integer num1 = 100;
Integer num2 = 100;
System.out.println(num1.equals(num2));

这里的输出结果也为true,因为num1和num2所包装的数值相等,即便它们是不同的对象。

Java 规范对equals方法的重写提出了严格的要求,这些要求就像是一套行为准则,确保了equals方法在各种场景下的正确使用:

  • 对称性:如果a.equals(b)返回true,那么b.equals(a)也必须返回true。这就好比两个人相互打招呼,如果 A 向 B 打招呼(A 认识 B),那么 B 也应该回应 A(B 也认识 A),这是一种基本的对称关系。

  • 一致性:在对象的状态没有发生变化的前提下,多次调用a.equals(b),其结果应该始终保持一致。想象一下,你每天早上出门前都会照镜子确认自己的穿着是否整齐,镜子里的你(对象状态)没有改变,那么镜子呈现的结果(equals方法的返回值)也应该是一致的。

  • 非空性:任何对象调用equals(null)都应该返回false。就好像你不能说一个人等同于 “不存在” 一样,对象与null进行比较时,必然是不相等的。

1.2 hashCode 方法:哈希表的性能基石

hashCode方法同样来源于Object类,它的职责是返回一个对象的哈希码,这个哈希码是一个int类型的值。在默认情况下,hashCode方法的实现是基于对象的内存地址,由本地方法实现。这意味着不同的对象通常会返回不同的哈希码。

哈希码在 Java 中的主要应用场景是提升哈希表(如HashMap、HashSet)的存储和查询效率。哈希表就像是一个大型的仓库,每个货物(对象)都有一个对应的货架位置(哈希桶),而哈希码就像是这个货架位置的编号。通过哈希码,我们可以快速地找到对象应该存储的位置,从而大大提高了数据的存储和查询速度。

以String类为例,它的hashCode方法采用了一种巧妙的多项式哈希算法:s[0]31^(n - 1) + s[1]31^(n - 2) + ... + s[n - 1]。其中,s[i]表示字符串中第i个字符,n是字符串的长度。例如,对于字符串"abc",其哈希码的计算过程如下:

'a'的ASCII码值是97,'b'的ASCII码值是98,'c'的ASCII码值是99
hashCode = 'a' * 31² + 'b' * 31 + 'c'= 97 * 31² + 98 * 31 + 99= 97 * 961 + 98 * 31 + 99= 93217 + 3038 + 99= 96354

这种算法能够有效地将不同的字符串映射到不同的哈希码,从而减少哈希冲突的发生,提高哈希表的性能。

通过对equals和hashCode方法的基础概念介绍,我们已经初步了解了它们的重要性和基本原理。在接下来的内容中,我们将深入探讨它们之间的微妙关系,以及在实际应用中如何正确地使用和重写它们。

二、黄金搭档:hashCode 与 equals 的约定关系

在 Java 的世界里,hashCode和equals就像是一对默契十足的黄金搭档,它们之间存在着紧密而微妙的约定关系。这种关系不仅是 Java 语言规范的重要组成部分,更是确保程序在涉及对象比较和存储时能够正确、高效运行的关键。接下来,让我们深入探究这对黄金搭档之间的约定关系,以及为什么在实际编程中我们必须同时重写它们。

2.1 规范强制约定(必须遵守!)

  • 相等对象的哈希码必相等:这是一条硬性规定,若a.equals(b)为true,则a.hashCode() == b.hashCode()必须成立。想象一下,在一个大型的员工信息管理系统中,每个员工对象都有唯一的工号。如果两个员工对象的工号相同,根据业务逻辑,我们认为这两个员工是相等的(即equals方法返回true)。此时,它们的哈希码也必须相等,否则在使用哈希表(如HashMap)来存储员工信息时,可能会出现同一个员工被存储多次的情况,这显然会导致数据的不一致和错误。

  • 哈希码相等对象未必相等:当a.hashCode() == b.hashCode()时,a.equals(b)可能为false,这就是所谓的哈希冲突。哈希冲突是由于哈希算法的局限性导致的,即使是设计得非常优秀的哈希算法,也难以完全避免不同的对象产生相同的哈希码。就好比不同的人可能会有相同的生日,但他们显然不是同一个人。在 Java 中,这种情况是被允许的,当哈希冲突发生时,会通过equals方法来进一步判断对象是否真正相等。

  • 不等对象的哈希码无需不同:虽然从理论上来说,为不等的对象生成不同的哈希码可以减少哈希冲突的发生,从而提升哈希表的性能,但这并不是 Java 规范的强制要求。在实际应用中,我们可以根据具体的业务场景和性能需求来设计哈希码的生成策略,以达到性能和实现复杂度之间的平衡。

2.2 为什么必须同时重写?

  • 仅重写 equals 的陷阱:假设我们自定义了一个类Student,并只重写了equals方法,用于比较两个学生对象的学号是否相同。然后,我们尝试将这些学生对象存入HashSet中。由于HashSet在判断元素是否重复时,首先会比较对象的哈希码,如果哈希码不同,就直接认为是不同的元素,而不会再调用equals方法。因此,即使两个学生对象的学号相同(即equals方法判断为相等),但由于它们的哈希码不同(默认的哈希码是基于对象的内存地址生成的),仍然会被HashSet视为不同的对象而重复存储,这显然违背了我们的预期。

  • 仅重写 hashCode 的缺陷:相反,如果我们只重写了hashCode方法,而没有重写equals方法,也会出现问题。例如,我们重写了hashCode方法,使得所有学生对象的哈希码都相同(这是一种极端的情况,仅为了说明问题)。此时,当我们将学生对象存入HashSet时,由于哈希码相同,HashSet会认为这些对象都相等,进而可能会将不同的学生对象误判为相同的对象,导致数据的混乱和错误,这也与我们的业务逻辑相违背。

通过以上对hashCode和equals约定关系的深入探讨,以及对不同重写情况的分析,我们可以清楚地看到,这两个方法在 Java 编程中是相辅相成的,必须同时重写才能确保对象在比较和存储时的正确性和一致性。在接下来的内容中,我们将通过实际的代码示例,展示如何正确地重写这两个方法,以及在重写过程中需要注意的事项。

三、实战指南:正确重写的步骤与最佳实践

了解了hashCode和equals的基本概念以及它们之间的约定关系后,接下来我们将进入实战环节,学习如何正确地重写这两个方法。这不仅是理论知识的实践应用,更是提升我们 Java 编程能力的关键一步。在实际项目中,正确重写hashCode和equals方法能够确保对象在集合中的正确存储和检索,避免潜在的错误和性能问题。下面,让我们通过详细的步骤和示例代码,深入探讨重写这两个方法的实战技巧。

3.1 重写 equals 的标准步骤

重写equals方法时,需要遵循一套严格的标准步骤,以确保方法的正确性和一致性。以下是重写equals方法的详细步骤:

  1. 引用地址判断:首先,使用==运算符判断当前对象(this)和传入对象(obj)是否为同一个引用。如果是同一个引用,说明它们指向内存中的同一个对象实例,自然是相等的,直接返回true。这一步是一个简单而高效的优化,可以避免不必要的后续比较操作。

if (this == obj) {return true;
}
  1. 类型检查:接着,通过instanceof关键字检查传入对象是否为当前类的实例。如果传入对象为null,或者其类型与当前类不匹配,那么这两个对象必然不相等,返回false。这一步可以有效避免ClassCastException异常的发生。

if (obj == null || getClass() != obj.getClass()) {return false;
}
  1. 属性逐个比较:将传入对象强制转换为当前类的类型,然后对类中的关键属性进行逐一比较。对于基本数据类型的属性,直接使用==运算符进行比较;对于引用数据类型的属性,使用Objects.equals方法进行比较,该方法能够安全地处理null值。例如,对于一个包含id(int类型)和name(String类型)属性的User类,其equals方法的比较逻辑如下:

User other = (User) obj;
return this.id == other.id && Objects.equals(this.name, other.name);

下面是一个完整的User类重写equals方法的示例:

import java.util.Objects;
​
public class User {private int id;private String name;
​public User(int id, String name) {this.id = id;this.name = name;}
​@Overridepublic boolean equals(Object obj) {if (this == obj) {return true;}if (obj == null || getClass() != obj.getClass()) {return false;}User other = (User) obj;return this.id == other.id && Objects.equals(this.name, other.name);}
}

3.2 重写 hashCode 的生成策略

重写hashCode方法时,需要确保生成的哈希码能够准确反映对象的内容,并且尽可能地减少哈希冲突的发生。以下是两种常见的重写hashCode方法的生成策略:

  1. 基于 equals 涉及的属性:哈希码的计算应该基于所有参与equals方法比较的属性。这样可以保证相等的对象具有相同的哈希码,满足hashCode和equals的约定关系。

  1. 使用高效算法

  • 推荐方法(JDK7+):使用Objects.hash(属性1, 属性2, ...)方法,该方法会自动处理null值,并根据传入的属性生成一个稳定的哈希值。例如,对于上述User类,可以使用以下方式重写hashCode方法:

@Override
public int hashCode() {return Objects.hash(id, name);
}
  • 传统方法:在 JDK7 之前,通常使用以下传统方法来计算哈希码。首先,将哈希码的初始值设为一个非零的常量,例如17。然后,依次对每个属性进行计算,计算公式为hash = 31 * hash + 属性.hashCode()。这里的31是一个质数,它具有良好的数学特性,能够在保证计算效率的同时,使生成的哈希值更加分散,减少哈希冲突的发生。例如:

@Override
public int hashCode() {int hash = 17;hash = 31 * hash + Integer.hashCode(id);hash = 31 * hash + (name != null ? name.hashCode() : 0);return hash;
}

3.3 测试用例不可少

编写完重写的equals和hashCode方法后,千万不能忘记编写测试用例来验证它们的正确性。测试用例应覆盖各种边界情况和常见场景,以确保方法在不同条件下都能正常工作。以下是一些建议的测试用例:

  1. 边界情况

  • 测试 null 输入:验证当传入null时,equals方法是否返回false。

User user = new User(1, "Alice");
assertFalse(user.equals(null));
  • 自身比较:测试对象与自身进行比较时,equals方法是否返回true。

User user = new User(1, "Alice");
assertTrue(user.equals(user));
  • 不同实例但属性相同的情况:创建两个不同的对象实例,但它们的属性值完全相同,验证equals方法是否返回true,以及hashCode方法是否返回相同的哈希值。

User user1 = new User(1, "Alice");
User user2 = new User(1, "Alice");
assertTrue(user1.equals(user2));
assertEquals(user1.hashCode(), user2.hashCode());
  1. 哈希冲突验证:故意构造两个哈希码相同但属性不同的对象,验证equals方法是否返回false。这可以通过选择合适的属性值,使它们在哈希码计算中产生相同的结果来实现。

// 假设通过特定的属性值构造出两个哈希码相同但属性不同的对象
User user3 = new User(2, "Bob");
User user4 = new User(3, "Charlie");
// 使用一些特殊的计算方式,使user3和user4的hashCode相同
// 例如通过自定义的哈希算法,让这两个对象的某些属性组合产生相同的哈希值
// 然后验证equals方法
assertFalse(user3.equals(user4));

通过以上详细的实战指南,我们学习了如何正确重写equals和hashCode方法,以及如何编写测试用例来验证它们的正确性。在实际项目中,务必严格按照这些步骤和策略进行操作,以确保对象在比较和存储时的正确性和高效性。

四、集合框架中的深度应用

4.1 HashSet/HashMap 的存储逻辑

在 Java 集合框架中,HashSet和HashMap堪称基于哈希算法的两大经典实现,它们的高效性和广泛应用,让hashCode和equals方法在其中扮演着举足轻重的角色。接下来,让我们深入剖析它们的存储逻辑,一探这两个方法的神奇之处。

HashSet 的存储逻辑

当我们向HashSet中添加元素时,其内部的存储逻辑严谨而有序。首先,系统会调用该元素的hashCode方法,计算出它的哈希码。这个哈希码就像是一把神奇的钥匙,能够快速确定该元素在哈希表(本质是一个数组)中对应的桶(bucket)位置。例如,假设有一个HashSet,其内部哈希表的长度为 16,当我们添加一个元素时,通过计算该元素的哈希码对 16 取模,就可以得到一个 0 到 15 之间的整数,这个整数就是该元素在哈希表中的桶索引。

然后,系统会检查该桶位置是否已经存在元素。如果该桶为空,说明这个位置还没有被占用,新元素就可以直接插入到这个桶中,就像把一件物品直接放在一个空的货架上一样简单。但如果该桶中已经存在其他元素,这就意味着发生了哈希冲突(不同的元素计算出了相同的哈希码)。此时,系统会遍历该桶内的所有元素,通过调用元素的equals方法,逐一判断桶内已有的元素与待添加的元素是否相等。只有当桶内所有元素与待添加元素通过equals方法比较都不相等时,新元素才会被插入到该桶中,以确保集合中元素的唯一性。

HashMap 的存储逻辑

HashMap的存储逻辑与HashSet类似,但由于它是用于存储键值对(key-value)的,所以在处理过程中会更加复杂一些。当我们调用put方法向HashMap中插入一个键值对时,同样会先计算键(key)的哈希码,以此确定该键值对在哈希表中的桶位置。如果该桶为空,键值对就会直接插入到这个桶中。

若该桶中已有元素,就需要检查桶内已有的键与待插入的键是否相等。这里的相等判断同样依赖于equals方法。如果找到一个键与待插入的键通过equals方法比较相等,那么新插入的键值对中的值(value)会覆盖原有的值,而键保持不变。例如,我们向HashMap中先插入一个键值对("name", "Alice"),然后再插入("name", "Bob"),由于键"name"相同,最终HashMap中存储的键值对将是("name", "Bob"),后面插入的值覆盖了前面的值。

如果桶内没有找到与待插入键相等的元素,那么新的键值对就会被插入到该桶中。在 Java 8 及以上版本中,如果桶内元素的数量超过一定阈值(默认为 8),并且哈希表的容量大于 64 时,桶内的链表会转换为红黑树,以提高查找和插入的效率。这种数据结构的动态转换,充分体现了HashMap在性能优化方面的精妙设计。

反例:未重写导致的重复存储

如果我们自定义一个类,并且没有重写hashCode和equals方法,那么在使用HashSet或HashMap时,就可能会出现不符合预期的结果。以HashSet为例,假设我们有一个自定义类Student:

public class Student {private int id;private String name;
​public Student(int id, String name) {this.id = id;this.name = name;}
}

然后我们尝试将两个Student对象添加到HashSet中:

HashSet<Student> set = new HashSet<>();
Student student1 = new Student(1, "Alice");
Student student2 = new Student(1, "Alice");
set.add(student1);
set.add(student2);
System.out.println(set.size()); 

按照我们的预期,如果两个Student对象的id和name都相同,它们应该被视为同一个对象,HashSet的大小应该为 1。但实际上,由于我们没有重写Student类的hashCode和equals方法,HashSet会认为这两个对象是不同的(因为它们的默认哈希码是基于对象的内存地址生成的,不同的对象实例内存地址不同),所以最终HashSet的大小会是 2,这显然与我们的预期不符。

同样,在HashMap中,如果作为键的对象没有正确重写hashCode和equals方法,也会导致键值对的存储和查找出现问题,比如可能无法正确获取到之前插入的值,或者插入了重复的键值对等。

4.2 常见误区与解决方案

在使用hashCode和equals方法的过程中,开发者往往容易陷入一些误区,这些误区可能会导致程序出现难以察觉的错误。下面,让我们来详细剖析这些常见误区,并提供相应的解决方案。

误区 1:认为 hashCode 完全决定对象相等性,忽略 equals 的作用

在 Java 中,有一种常见的误解是认为只要两个对象的hashCode相等,它们就一定相等。这种观点忽略了equals方法在对象相等性判断中的关键作用。实际上,hashCode只是用于快速定位对象在哈希表中的位置,它的主要目的是提高哈希表的操作效率。而最终判断两个对象是否真正相等,还需要依赖equals方法的比较结果。

例如,考虑以下代码:

public class Point {private int x;private int y;
​@Overridepublic int hashCode() {return 1; }
​// 未重写equals方法
}
​
Point p1 = new Point(1, 2);
Point p2 = new Point(3, 4);
System.out.println(p1.hashCode() == p2.hashCode()); 

在上述代码中,Point类重写了hashCode方法,使其始终返回 1。这意味着p1和p2的哈希码是相等的。但从业务逻辑上来说,这两个点的坐标不同,它们显然不应该被视为相等的对象。如果在使用哈希表(如HashSet或HashMap)时,仅仅依赖hashCode来判断对象的相等性,就会导致错误的结果。比如,将p1和p2添加到HashSet中时,由于它们的哈希码相同,HashSet可能会错误地认为它们是同一个对象,从而只存储一个,这与我们的预期不符。

误区 2:重写时遗漏关键属性

在重写hashCode和equals方法时,另一个常见的错误是遗漏了类中的关键属性。这可能会导致相等的对象被判断为不相等,或者不相等的对象被错误地认为相等。

例如,对于一个表示用户的User类:

public class User {private int id;private String name;private int age;
​@Overridepublic boolean equals(Object obj) {if (this == obj) return true;if (obj == null || getClass() != obj.getClass()) return false;User other = (User) obj;return this.id == other.id && Objects.equals(this.name, other.name); }
​@Overridepublic int hashCode() {return Objects.hash(id, name); }
}

假设在重写equals和hashCode方法时,我们只考虑了id和name属性,而忽略了age属性。如果有两个User对象,它们的id和name相同,但age不同,按照上述重写的方法,这两个对象会被认为是相等的。然而,在某些业务场景下,age可能是一个重要的属性,这样的判断结果可能不符合实际需求。

解决方案:使用 IDE 的自动生成功能

为了避免上述误区,我们可以充分利用集成开发环境(IDE)提供的自动生成功能。以 IntelliJ IDEA 为例,当我们需要重写hashCode和equals方法时,只需在类中右键点击,选择 “Generate”,然后选择 “equals () and hashCode ()”,IDE 会自动根据类中的属性生成相应的方法代码。

使用 IDE 自动生成的好处是显而易见的。首先,它能够确保我们不会遗漏任何关键属性,因为 IDE 会自动识别类中的所有非静态属性,并将它们纳入equals和hashCode的计算逻辑中。其次,生成的代码遵循了标准的重写规范,能够保证方法的正确性和一致性。这样,我们就可以避免因手动编写代码而可能出现的错误,提高开发效率和代码质量。

通过对HashSet和HashMap存储逻辑的深入分析,以及对常见误区的剖析与解决方案的探讨,我们对hashCode和equals方法在集合框架中的应用有了更全面、更深入的理解。在实际开发中,我们必须严格遵循这些方法的使用规范,正确地重写它们,以确保集合的正常运作和程序的正确性。

五、总结:面试高频考点与开发最佳实践

5.1 面试必问点

在 Java 开发相关的面试中,hashCode和equals方法的相关问题几乎是必问的高频考点。以下是一些常见的面试问题及详细解答:

  1. 为什么重写 equals 时必须重写 hashCode?

这是因为 Java 的集合框架(如HashSet、HashMap)在处理对象时,首先会根据对象的hashCode来确定其存储位置,然后再通过equals方法来判断对象是否相等。如果只重写了equals方法,而没有重写hashCode方法,那么两个在逻辑上相等的对象可能会具有不同的hashCode,这将导致它们在集合中被视为不同的对象,从而无法正确地存储和检索,违反了 Java 中关于equals和hashCode的约定关系,即相等的对象必须具有相等的哈希码。

  1. 两个对象 hashCode 相同,一定相等吗?

不一定。虽然hashCode相同意味着两个对象在哈希表中可能存储在同一个桶(bucket)中,但这并不意味着它们在逻辑上是相等的。由于哈希算法的局限性,不同的对象可能会产生相同的哈希码,这种情况被称为哈希冲突。因此,当两个对象的hashCode相同时,还需要通过equals方法进行进一步的比较,以确定它们是否真正相等。只有当equals方法也返回true时,才能判定这两个对象是相等的。

http://www.dtcms.com/a/408078.html

相关文章:

  • 个人建什么样的网站好wordpress qq登录免费
  • 一个网站的优势有哪些安装文件出现乱码
  • 做网站的时候表格怎么去掉最近网站改版文章突然不收录
  • 网站怎么做才有收录租用服务器建设网站费用
  • 数学物理公式
  • 谁能给个网站谢谢wordpress目录内容分页显示
  • 网站建设及维护业务服务合同佛山seo整站优化
  • 做网站的软件高中 通用技术虚拟主机管理怎么做网站
  • 新增支持优化
  • 第7篇 c#推理自己训练的yolov5 onnx模型文件
  • 新网站不被收录的原因网页大图素材
  • 工程建设云网站深圳企业企业网站建设
  • 私募基金网站开发流程巫山做网站哪家强
  • 2、深入理解 C++ 引用、指针、内联函数与效率对比 —— 实战讲解与代码分析
  • 现在哪个招聘网站做的比较好敬请期待还是说尽情期待
  • 广东网站设计与建设上海第五届中国国际进口博览会直播
  • Java 浅复制与深复制
  • 网站建设app大学生网页设计期末作业
  • 网站怎么做图片栏目html网页设计过程
  • 专注网站建站重庆企业公司网站建设
  • 【LLIE专题】GT-Mean Loss:一种低照度图像增强的损失函数
  • Transformer-位置编码(Position Embedding)
  • 【MySQL】内连接优化order by+limit 以及添加索引再次改进
  • 吉林省高等级公路建设局 网站成品网站源码在线看
  • 邢台网站建设制作口碑好网站建设公司哪家好
  • 什么网站做微信公众账号wordpress php 文件
  • 热可可怎么做视频网站营销网页
  • 找不到实验方案怎么办?
  • 找人做的网站推广被坑石家庄网站建设就找企行家
  • TRL的安装