杰瑞科技汇

java hashmap key

Of course! This is a fundamental and crucial concept in Java. Here’s a comprehensive guide to understanding HashMap keys in Java, from the basics to the best practices.


The Core Concept: What Makes a Key in a HashMap?

A HashMap stores data as key-value pairs. To efficiently retrieve a value, you provide its corresponding key. The magic behind HashMap's speed (O(1) average time complexity for get and put operations) lies in how it uses the key.

The process is:

  1. Calculate Hash Code: When you add a key-value pair (put(key, value)), HashMap first calls the key.hashCode() method to get an integer hash code.
  2. Find Bucket: This hash code is then used to determine an index (or "bucket") in the underlying array where the entry will be stored.
  3. Handle Collisions: If two different keys produce the same hash code (a collision), they are stored in the same bucket, typically as a linked list (or a balanced tree in Java 8+ if the list gets too long).

When you retrieve a value (get(key)), HashMap repeats this process:

  1. It calculates the hash code of the key you're providing.
  2. It goes directly to the calculated bucket.
  3. It then uses the equals() method to find the exact key within that bucket to return the correct value.

The Golden Rule: For a HashMap to work correctly, the key's hash code must not change while it is being used as a key in the map. If it does, you will likely be unable to retrieve the value.


The Two "Contracts" for a Key

For a custom object to be used as a key in a HashMap, it must correctly implement two methods from the Object class: hashCode() and equals(). These two methods must follow a specific contract with each other.

The Contract

  1. Consistency: If two objects are equal according to the equals() method, they must have the same hash code.

    • a.equals(b) is true => a.hashCode() == b.hashCode() must be true.
  2. Inequality (The Reverse is NOT True): If two objects have the same hash code, they are not necessarily equal. This is called a hash collision.

    • a.hashCode() == b.hashCode() does not imply that a.equals(b) is true.
  3. Consistency over Time: The hash code of an object must remain the same as long as it is used as a key in a HashMap. This is why it's critical to not use mutable objects as keys or, if you do, to never change the fields used in hashCode() and equals().


Implementing hashCode() and equals() for a Custom Key Class

Let's create a Person class to be used as a key. A person is uniquely identified by their id.

// BAD IMPLEMENTATION - DO NOT DO THIS!
public class PersonBad {
    private final int id;
    private String name;
    public PersonBad(int id, String name) {
        this.id = id;
        this.name = name;
    }
    public int getId() { return id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; } // Name is mutable
    // BAD: Only uses 'id' for equals, but what if we also want to consider name?
    // Let's assume for this example 'id' is the only unique field.
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PersonBad personBad = (PersonBad) o;
        return id == personBad.id;
    }
    // GOOD: Consistent with equals. Only uses 'id'.
    @Override
    public int hashCode() {
        return id; // Simple, but okay for a primitive int
    }
}

Now, let's see a good implementation. A common and robust way is to use java.util.Objects.hash().

// GOOD IMPLEMENTATION
public final class PersonGood { // Mark class as final to prevent inheritance issues
    private final int id;
    private final String name; // Mark fields as final to make the object immutable
    public PersonGood(int id, String name) {
        this.id = id;
        this.name = name;
    }
    public int getId() { return id; }
    public String getName() { return name; }
    // GOOD: The contract is fulfilled.
    // 1. Symmetry, Reflexivity, Transitivity are handled by the IDE/standard practice.
    // 2. It's consistent: if two objects are equal, their hash codes will be the same.
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PersonGood that = (PersonGood) o;
        // Using '==' for primitives and 'Objects.equals()' for objects is safe
        return id == that.id && Objects.equals(name, that.name);
    }
    // GOOD: The contract is fulfilled.
    // 1. It's consistent with equals.
    // 2. It uses the same fields as equals.
    @Override
    public int hashCode() {
        // A common and robust pattern. It combines the hash codes of the fields.
        return Objects.hash(id, name);
    }
}

Why is the PersonGood version better?

  • Immutable: The fields id and name are final. This guarantees that their values cannot change after the object is created, which is the safest way to use objects as HashMap keys.
  • Correct Contract: equals() and hashCode() are implemented correctly and consistently.
  • Final Class: Making the class final prevents subclassing, which can break the equals() contract if a subclass adds state that affects equality.

Best Practices for HashMap Keys

  1. Prefer Immutable Objects: This is the single most important rule. If the key's state changes, its hashCode() will change, and the HashMap will be unable to find the entry.

    • Good: String, Integer, Long, custom classes with final fields.
    • Bad: StringBuilder, ArrayList, or any custom object with setters that modify fields used in hashCode().
  2. Implement equals() and hashCode() Correctly: If you use a custom object as a key, you must implement both methods and follow the contract. Most modern IDEs (IntelliJ, Eclipse) can generate these methods for you correctly.

  3. Be Aware of Hash Code Quality: A good hashCode() function distributes keys evenly across the buckets. A poor one (e.g., always returning the same number) will turn your HashMap into a linked list, degrading performance to O(n). Objects.hash() is a good starting point.

  4. Common Key Types:

    • String: The most common key type. It's immutable and has a well-implemented hashCode() that is cached for performance.
    • Primitive Wrappers (Integer, Long, etc.): These are also immutable and excellent keys.
    • Enums: Perfect keys. They are singletons, immutable, and have a well-defined hashCode().

Code Example

Let's see our PersonGood class in action.

import java.util.HashMap;
import java.util.Map;
public class HashMapKeyExample {
    public static void main(String[] args) {
        Map<PersonGood, String> userRoles = new HashMap<>();
        PersonGood person1 = new PersonGood(101, "Alice");
        PersonGood person2 = new PersonGood(102, "Bob");
        PersonGood person3 = new PersonGood(101, "Alice"); // Same data as person1
        userRoles.put(person1, "Admin");
        userRoles.put(person2, "Editor");
        System.out.println("Map size: " + userRoles.size()); // Output: 2
        // Let's retrieve the role for person1
        // We create a new object with the same data. It will be considered equal.
        PersonGood person1Key = new PersonGood(101, "Alice");
        String role = userRoles.get(person1Key);
        System.out.println("Role for person1: " + role); // Output: Admin
        // Let's check if person3 is found in the map
        // person1.equals(person3) is true, so it should find the entry.
        System.out.println("Role for person3: " + userRoles.get(person3)); // Output: Admin
        // Let's check if person2 is found
        System.out.println("Role for person2: " + userRoles.get(person2)); // Output: Editor
    }
}

Summary Table

Topic Key Takeaway
Core Mechanism HashMap uses key.hashCode() to find a bucket and key.equals() to find the exact entry within the bucket.
The Contract If a.equals(b) is true, then a.hashCode() must equal b.hashCode(). The reverse is not required.
Implementation For custom keys, you must override both equals() and hashCode(). They must use the same set of fields.
Best Practice Use immutable objects as keys. This prevents the hashCode from changing while the key is in the map.
Common Keys String, Integer, Long, UUID, and custom immutable classes are excellent choices.
分享:
扫描分享到社交APP
上一篇
下一篇