杰瑞科技汇

Java中equals与hashCode为何要同时重写?

Of course! This is a fundamental and crucial topic in Java. Understanding equals() and hashCode() is essential for writing correct, robust, and efficient Java applications.

Java中equals与hashCode为何要同时重写?-图1
(图片来源网络,侵删)

Let's break it down.


The Golden Rule: The Contract

The entire concept revolves around a contract defined in the Object class. If you override one, you almost always need to override the other.

The contract states:

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

Java中equals与hashCode为何要同时重写?-图2
(图片来源网络,侵删)

Important: The reverse is not true. Two objects can have the same hashCode() but not be equal. This is called a hash collision.

Analogy: The Library System

  • equals(): This is like checking if two books are the exact same edition (same title, same author, same ISBN). It's a deep comparison.
  • hashCode(): This is like the book's location in the library (e.g., "Aisle 3, Shelf 2, Bin 15"). It's a quick way to find a general area.
  • The Contract: If two books are the exact same edition (they are equals), they must be in the exact same location (they have the same hashCode).
  • Collision: Two different books (e.g., "Java for Beginners" and "Python for Beginners") could, by chance, be assigned to the same location ("Aisle 3, Shelf 2, Bin 15"). The librarian would then use equals() to check which one you actually want.

The equals() Method

What is it for?

The default equals() method in the Object class behaves like the operator: it checks if two references point to the exact same object in memory.

For most objects, this isn't what we want. We want to check if two objects have the same content or value. For example, two different Person objects with the same name and birth date should be considered "equal".

Java中equals与hashCode为何要同时重写?-图3
(图片来源网络,侵删)

How to Override equals() Correctly (The 7 Rules)

When you override equals(), you must follow this canonical procedure:

  1. Check for null: If the other object is null, they can't be equal.
  2. Check for this reference: If the other object is the same instance as this, they are obviously equal.
  3. Check for class type: If the other object is not an instance of the same class, they are not equal. (Using instanceof is better than getClass() == obj.getClass() for subclasses).
  4. Cast the object: Now you can safely cast the object to your specific class.
  5. Compare significant fields: Compare the fields that define the object's state. Use Objects.equals() for object fields to handle null safely, and for primitive fields.
  6. Return true if all significant fields are equal.
  7. Return false otherwise.

Example: Person Class

import java.util.Objects;
public class Person {
    private final String name;
    private final int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 1. Override equals()
    @Override
    public boolean equals(Object o) {
        // 1. Check for null
        if (o == null) {
            return false;
        }
        // 2. Check for this reference
        if (this == o) {
            return true;
        }
        // 3. Check for class type
        if (!(o instanceof Person)) {
            return false;
        }
        // 4. Cast the object
        Person person = (Person) o;
        // 5. Compare significant fields
        // Use Objects.equals() for String fields to handle nulls correctly.
        // Use == for primitive int fields.
        return age == person.age &&
               Objects.equals(name, person.name);
    }
    // Getters, constructor, toString()...
    public String getName() { return name; }
    public int getAge() { return age; }
    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
}

The hashCode() Method

What is it for?

hashCode() returns an integer, called a hash code, which is used by data structures like HashMap, HashSet, and Hashtable to store and retrieve objects efficiently.

When you put an object into a HashMap, it:

  1. Calculates the object's hashCode() to find the "bucket" (or location) where it should be stored.
  2. If the bucket is empty, it stores the object there.
  3. If the bucket is not empty (a collision), it uses equals() to compare the new object with the existing ones in that bucket to find the right spot or determine if the key is already present.

A good hashCode() function distributes objects across many buckets to minimize collisions and keep lookups fast (O(1) on average).

How to Override hashCode() Correctly

The goal is to produce a hash code that is consistent and based on the same fields you used in equals().

  1. Choose significant fields: Use the same fields that you used to determine equality in equals().
  2. Initialize a result: Start with a non-zero constant, like result = 17.
  3. Combine field hashes: For each significant field, update the result using a formula. A common and effective formula is: result = 31 * result + (field == null ? 0 : field.hashCode());
    • Why 31? It's a prime number. Multiplying by a prime number helps reduce collisions. It's also an odd number, which helps ensure that multiplication and bit shifting have good properties.
  4. Return the result.

Example: Person Class (continued)

public class Person {
    // ... (previous code) ...
    // 2. Override hashCode()
    @Override
    public int hashCode() {
        // 1. Choose significant fields (same as in equals)
        // 2. Initialize a result
        int result = 17;
        // 3. Combine field hashes
        // Use a prime number (like 31) as a multiplier.
        result = 31 * result + (name == null ? 0 : name.hashCode());
        result = 31 * result + age; // Autoboxing to Integer happens here
        // 4. Return the result
        return result;
    }
}

The Danger: Breaking the Contract

If you override equals() but not hashCode(), or vice-versa, you will break the contract and cause serious bugs in collection classes.

Example of a Broken Contract

import java.util.HashMap;
import java.util.Map;
public class BrokenContractExample {
    public static void main(String[] args) {
        Map<Person, String> personMap = new HashMap<>();
        Person p1 = new Person("Alice", 30);
        Person p2 = new Person("Alice", 30);
        // p1 and p2 are equal according to our equals() method
        System.out.println("p1.equals(p2): " + p1.equals(p2)); // true
        // Let's put p1 in the map
        personMap.put(p1, "Engineer");
        // Now, let's try to get the value using p2
        // Since p1 and p2 are equal, we should get "Engineer" back.
        // But we won't, because we broke the hashCode contract!
        String job = personMap.get(p2);
        System.out.println("Job for p2: " + job); // Prints null! BUG!
    }
}
// Assume the Person class above has an equals() but NO hashCode() override.
// The default hashCode() from Object is used, which is based on memory address.
// p1.hashCode() != p2.hashCode(), so the HashMap looks in the wrong bucket.

The Modern Way: Objects.hash()

Since Java 7, the java.util.Objects class provides a convenient utility method to generate hash codes. It simplifies the process and reduces the chance of error.

How it works: Objects.hash(Object... values) takes a list of fields and computes the hash code for you using the same prime-number logic.

Example: Person Class using Objects.hash()

import java.util.Objects;
public class Person {
    private final String name;
    private final int age;
    // ... constructor, getters, equals(), toString() ...
    // The modern, clean way to implement hashCode()
    @Override
    public int hashCode() {
        // This single line does all the work!
        return Objects.hash(name, age);
    }
}

This is the recommended approach for most cases. It's concise, readable, and less error-prone.


Summary and Best Practices

Method Purpose Key Rules Best Practice
equals() Checks if two objects have the same **
分享:
扫描分享到社交APP
上一篇
下一篇