杰瑞科技汇

hashcode与equals在Java中如何协同工作?

Of course! This is a fundamental concept in Java that every developer must understand. Let's break it down in detail.

hashcode与equals在Java中如何协同工作?-图1
(图片来源网络,侵删)

The Big Picture: Why Do We Need equals() and hashCode()?

Imagine you have a big warehouse (a Java Collection, like a HashSet or HashMap) and you want to store and retrieve boxes (objects).

  • equals() is like the detailed description of a box. It tells you if two boxes are identical in content. For example, "Box A contains a red apple, a green apple, and an orange." Box B has the exact same contents. equals() would say they are the same.
  • hashCode() is like the shipping label on the outside of the box. It's a quick, easy-to-read number that tells the warehouse system which section of the warehouse to put the box in.

Now, let's see how they work together in the warehouse:

  1. Storing a Box: You bring Box A to the warehouse. The worker looks at its shipping label (hashCode()), sees it belongs in "Section 5," and puts it there.
  2. Retrieving a Box: You come back later with a new Box C that has the exact same contents as Box A. You tell the worker you want Box C. The worker looks at Box C's shipping label (hashCode()), sees it also belongs in "Section 5," and goes directly to that section.
  3. The Final Check: Inside Section 5, there might be many boxes. The worker now uses the detailed description (equals()) to compare Box C with every box in Section 5 until they find the one that matches perfectly.

The Golden Rule: If two objects are equal according to the equals() method, they must have the same hashCode() value.

The Golden Rule's Consequence: If two objects have different hashCode() values, they are guaranteed not to be equal.

hashcode与equals在Java中如何协同工作?-图2
(图片来源网络,侵删)

The equals() Method

What it does:

The equals() method is used to compare two objects for equality. The default implementation in the Object class (from which all classes inherit) uses the operator, which checks if two references point to the exact same object in memory.

This is often not what you want. For your custom classes, you usually want to compare the state or values of the objects.

When to Override:

You should override equals() when you want to define a logical notion of equality for your objects, typically when your class has fields that represent its state.

Example: A Person Class

Let's say we have a Person class. Two Person objects should be considered equal if they have the same id.

hashcode与equals在Java中如何协同工作?-图3
(图片来源网络,侵删)
public class Person {
    private final int id;
    private String name;
    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }
    // BAD: Default equals() from Object class
    // public boolean equals(Object obj) {
    //     return (this == obj);
    // }
    // GOOD: Overridden equals() based on business logic
    @Override
    public boolean equals(Object o) {
        // 1. Check if it's the exact same instance
        if (this == o) return true;
        // 2. Check if the other object is null or of a different class
        if (o == null || getClass() != o.getClass()) return false;
        // 3. Cast the object and compare significant fields
        Person person = (Person) o;
        return id == person.id;
    }
    // ... constructor, getters, setters
}

The equals() Contract (from Object class documentation):

  1. Reflexive: x.equals(x) must return true.
  2. Symmetric: x.equals(y) must return the same result as y.equals(x).
  3. Transitive: If x.equals(y) is true and y.equals(z) is true, then x.equals(z) must be true.
  4. Consistent: Multiple calls to x.equals(y must consistently return true or false, provided no information used in the comparison has changed.
  5. Non-nullity: x.equals(null) must return false.

The hashCode() Method

What it does:

The hashCode() method returns an integer value, which is used by hash-based collections (HashSet, HashMap, Hashtable) to determine where an object should be stored and retrieved quickly.

The Problem Without It:

If you only override equals() and not hashCode(), and you try to use your object in a HashSet or HashMap, you will run into problems.

Set<Person> people = new HashSet<>();
Person p1 = new Person(1, "Alice");
Person p2 = new Person(1, "Alice"); // Equal to p1 by our equals() logic
people.add(p1);
System.out.println(people.contains(p2)); // What will this print?

It will print false!

Why?

  1. people.add(p1) calculates p1.hashCode(), say it's 1234. It puts p1 in the "bucket" for 1234.
  2. people.contains(p2) calculates p2.hashCode(). Since we didn't override it, it uses the default Object.hashCode(), which is based on memory address. This address is different from p1's, so p2.hashCode() might be 5678.
  3. The HashSet looks in the "bucket" for 5678 and finds nothing. It never even calls equals() because it's looking in the wrong place!

The Solution:

Override hashCode() to return the same value for objects that are equal according to your equals() method.

How to Implement hashCode():

A simple and effective way is to combine the hash codes of your significant fields using the Objects.hash() utility method.

import java.util.Objects;
public class Person {
    private final int id;
    private String name;
    // ... constructor, equals() method
    // GOOD: Overridden hashCode() based on the same fields as equals()
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
    // ... other methods
}

Now, when you run the HashSet example again, p1.hashCode() and p2.hashCode() will both return the same value. The HashSet will look in the correct bucket and then call equals() to confirm they are the same, and contains(p2) will correctly return true.

The hashCode() Contract (from Object class documentation):

  1. Consistency: Multiple calls to hashCode() on the same object must consistently return the same integer, provided no information used in equals() comparisons has been modified. This integer need not remain consistent from one execution of a Java application to another.
  2. The Golden Rule: If a.equals(b) is true, then a.hashCode() must return the same value as b.hashCode().
  3. Not Required (but good practice): If a.equals(b) is false, it is not required that a.hashCode() and b.hashCode() be different. However, for performance reasons, it's a very good idea to make them different. This reduces collisions in hash-based collections, making them faster.

The Modern Way: record (Java 14+)

If you are using Java 14 or later, you can use the record keyword to create immutable data carrier classes. The compiler automatically generates equals(), hashCode(), and toString() methods for you, based on the record's components (fields).

// A record automatically generates a proper equals() and hashCode()
public record Person(int id, String name) {}
// Usage is the same, but you don't have to write the boilerplate!
Person p1 = new Person(1, "Alice");
Person p2 = new Person(1, "Alice");
System.out.println(p1.equals(p2));      // true
System.out.println(p1.hashCode());     // Some consistent integer
System.out.println(p2.hashCode());     // The same integer as p1.hashCode()

Summary Table

Feature equals() hashCode()
Purpose Compares two objects for equality of state. Returns an integer for efficient storage/retrieval in hash-based collections.
Default this == obj (compares memory addresses). A number derived from the object's memory address.
When to Override When you want to define your own logic for object equality. Whenever you override equals(). This is a critical rule.
Key Contract If a.equals(b) is true, then b.equals(a) must also be true. If a.equals(b) is true, then a.hashCode() must equal b.hashCode().
Implementation Compare the fields that define the object's identity.
分享:
扫描分享到社交APP
上一篇
下一篇