java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > java equals 和 hashCode方法

Java 中的 equals 和 hashCode 方法关系与正确重写实践案例

作者:Joyous

在Java中,equals和 hashCode方法是Object类的核心方法,广泛用于对象比较和哈希集合(如 HashMap、HashSet)的操作,本文深入剖析equals和hashCode方法的关系、契约、正确重写方式及实践案例,感兴趣的朋友一起看看吧

在 Java 中,equalshashCode 方法是 Object 类的核心方法,广泛用于对象比较和哈希集合(如 HashMapHashSet)的操作。根据 2024 年 Stack Overflow 开发者调查,Java 仍是企业级开发的主流语言之一,约 30% 的开发者在使用 Java 时遇到过因不当重写 equalshashCode 导致的 bug。本文深入剖析 equalshashCode 方法的关系、契约、正确重写方式及实践案例

一、背景与需求分析

1.1 equals 和 hashCode 的背景

equalshashCode 方法是 Java 中 Object 类的两个关键方法,用于对象比较和哈希表操作:

在实际开发中,HashMapHashSet 依赖 equalshashCode 来确保键或元素的唯一性。如果未正确重写,可能导致键丢失、重复元素或性能问题。例如,2023 年某电商平台因未正确重写 hashCode,导致订单系统中键冲突,影响了数千笔交易。

1.2 需求分析

1.3 技术挑战

1.4 目标

1.5 技术栈

组件技术选择优点
编程语言Java 21高性能、生态成熟、长期支持
框架Spring Boot 3.3集成丰富,简化开发
测试框架JUnit 5.10功能强大,易于验证契约
工具IntelliJ IDEA 2024.2调试和重构支持优异
依赖管理Maven 3.9.8依赖管理高效

二、equals 和 hashCode 的关系与契约

2.1 equals 方法

2.2 hashCode 方法

2.3 equals 和 hashCode 的关系

class Product {
    String productId;
    @Override
    public boolean equals(Object obj) { return productId.equals(((Product) obj).productId); }
    // 未重写 hashCode
}
Product p1 = new Product("1");
Product p2 = new Product("1");
HashMap<Product, String> map = new HashMap<>();
map.put(p1, "Product1");
System.out.println(map.get(p2)); // null(因 hashCode 不同)

2.4 常见问题

三、正确重写 equals 和 hashCode

3.1 重写 equals 的步骤

  1. 检查引用相等:若 this == obj,返回 true
  2. 检查 null 和类型:若 objnull 或类型不匹配,返回 false
  3. 转换类型:将 obj 转换为目标类。
  4. 比较字段:逐一比较关键字段,考虑 null 安全。
  5. 确保契约:验证自反性、对称性、传递性和一致性。

示例

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Product other = (Product) obj;
    return Objects.equals(productId, other.productId) && 
           Objects.equals(name, other.name);
}

3.2 重写 hashCode 的步骤

  1. 选择字段:使用与 equals 相同的字段。
  2. 计算哈希:对每个字段计算哈希值,组合生成唯一 hashCode
  3. 优化分布:使用质数(如 31)组合,减少冲突。
  4. 使用 Objects.hash:Java 7+ 提供的工具方法,简化实现。

示例

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

3.3 实现原则

3.4 工具支持

四、系统设计

4.1 架构

Client -> Service (Product) -> HashMap/HashSet -> equals/hashCode
                   |
                 JUnit Tests

4.2 数据模型

Product 类

public class Product {
    private String productId;
    private String name;
    // getters, setters, equals, hashCode
}

HashMap 存储

Map<Product, String> productMap = new HashMap<>();

4.3 性能估算

4.4 容错与验证

五、核心实现

以下基于 Java 21 实现 Product 类的 equalshashCode,并集成到 Spring Boot 3.3 项目中,包含 JUnit 测试验证。

5.1 项目设置

5.1.1 Maven 配置

```xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>equals-hashcode</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <java.version>21</java.version>
        <spring-boot.version>3.3.0</spring-boot.version>
        <junit.version>5.10.0</junit.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>${spring-boot.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
#### **5.1.2 Spring Boot 配置**
```yaml
spring:
  application:
    name: equals-hashcode
logging:
  level:
    com.example: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

5.2 核心代码实现

5.2.1 Product 类

package com.example.equalshashcode;
import java.util.Objects;
public class Product {
    private String productId;
    private String name;
    public Product(String productId, String name) {
        this.productId = productId;
        this.name = name;
    }
    public String getProductId() {
        return productId;
    }
    public void setProductId(String productId) {
        this.productId = productId;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Product other = (Product) obj;
        return Objects.equals(productId, other.productId) &&
               Objects.equals(name, other.name);
    }
    @Override
    public int hashCode() {
        return Objects.hash(productId, name);
    }
    @Override
    public String toString() {
        return "Product{productId='" + productId + "', name='" + name + "'}";
    }
}

5.2.2 服务层

package com.example.equalshashcode;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class ProductService {
    private final Map<Product, String> productMap = new HashMap<>();
    public void addProduct(Product product, String description) {
        productMap.put(product, description);
    }
    public String getProductDescription(Product product) {
        return productMap.get(product);
    }
    public int getProductCount() {
        return productMap.size();
    }
}

5.2.3 控制器

package com.example.equalshashcode;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/products")
public class ProductController {
    private final ProductService service;
    public ProductController(ProductService service) {
        this.service = service;
    }
    @PostMapping
    public void addProduct(@RequestBody Product product, @RequestParam String description) {
        service.addProduct(product, description);
    }
    @GetMapping
    public String getProductDescription(@RequestBody Product product) {
        return service.getProductDescription(product);
    }
    @GetMapping("/count")
    public int getProductCount() {
        return service.getProductCount();
    }
}

5.2.4 JUnit 测试

package com.example.equalshashcode;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
public class ProductTest {
    @Test
    void testEqualsReflexive() {
        Product p = new Product("1", "Laptop");
        assertTrue(p.equals(p), "equals should be reflexive");
    }
    @Test
    void testEqualsSymmetric() {
        Product p1 = new Product("1", "Laptop");
        Product p2 = new Product("1", "Laptop");
        assertTrue(p1.equals(p2) && p2.equals(p1), "equals should be symmetric");
    }
    @Test
    void testEqualsTransitive() {
        Product p1 = new Product("1", "Laptop");
        Product p2 = new Product("1", "Laptop");
        Product p3 = new Product("1", "Laptop");
        assertTrue(p1.equals(p2) && p2.equals(p3) && p1.equals(p3), "equals should be transitive");
    }
    @Test
    void testEqualsNull() {
        Product p = new Product("1", "Laptop");
        assertFalse(p.equals(null), "equals should return false for null");
    }
    @Test
    void testEqualsDifferentClass() {
        Product p = new Product("1", "Laptop");
        assertFalse(p.equals(new Object()), "equals should return false for different class");
    }
    @Test
    void testHashCodeConsistency() {
        Product p = new Product("1", "Laptop");
        int hash1 = p.hashCode();
        int hash2 = p.hashCode();
        assertEquals(hash1, hash2, "hashCode should be consistent");
    }
    @Test
    void testHashCodeEqualsContract() {
        Product p1 = new Product("1", "Laptop");
        Product p2 = new Product("1", "Laptop");
        assertTrue(p1.equals(p2) && p1.hashCode() == p2.hashCode(), "Equal objects must have same hashCode");
    }
    @Test
    void testHashMapBehavior() {
        Product p1 = new Product("1", "Laptop");
        Product p2 = new Product("1", "Laptop");
        Map<Product, String> map = new HashMap<>();
        map.put(p1, "Laptop Description");
        assertEquals("Laptop Description", map.get(p2), "HashMap should retrieve value for equal key");
    }
    @Test
    void testHashSetBehavior() {
        Product p1 = new Product("1", "Laptop");
        Product p2 = new Product("1", "Laptop");
        Set<Product> set = new HashSet<>();
        set.add(p1);
        set.add(p2);
        assertEquals(1, set.size(), "HashSet should not contain duplicates");
    }
}

5.3 部署配置

5.3.1 Spring Boot 应用

package com.example.equalshashcode;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EqualsHashCodeApplication {
    public static void main(String[] args) {
        SpringApplication.run(EqualsHashCodeApplication.class, args);
    }
}

5.3.2 Kubernetes 部署

apiVersion: apps/v1
kind: Deployment
metadata:
  name: equals-hashcode
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: equals-hashcode
  template:
    metadata:
      labels:
        app: equals-hashcode
    spec:
      containers:
      - name: equals-hashcode
        image: equals-hashcode:1.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: "200m"
            memory: "512Mi"
          limits:
            cpu: "500m"
            memory: "1Gi"
        env:
        - name: JAVA_OPTS
          value: "-XX:+UseParallelGC -Xms512m -Xmx1g"
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: equals-hashcode
  namespace: default
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
  selector:
    app: equals-hashcode
  type: ClusterIP

5.4 测试运行

  1. 构建项目
    mvn clean package
  2. 运行测试
    mvn test
  3. 部署应用
    docker build -t equals-hashcode:1.0 .
    docker push equals-hashcode:1.0
    kubectl apply -f deployment.yaml
  4. 验证 API
    • POST http://equals-hashcode/products?description=Laptop%20Description
      {"productId":"1","name":"Laptop"}
    • GET http://equals-hashcode/products
      {"productId":"1","name":"Laptop"}
      
      返回 "Laptop Description"

六、案例实践:电商商品系统

6.1 背景

6.2 解决方案

6.2.1 equals 实现

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Product other = (Product) obj;
    return Objects.equals(productId, other.productId) &&
           Objects.equals(name, other.name);
}

6.2.2 hashCode 实现

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

6.2.3 HashMap 测试

Product p1 = new Product("1", "Laptop");
Product p2 = new Product("1", "Laptop");
Map<Product, String> map = new HashMap<>();
map.put(p1, "Laptop Description");
assertEquals("Laptop Description", map.get(p2));

6.2.4 HashSet 测试

Product p1 = new Product("1", "Laptop");
Product p2 = new Product("1", "Laptop");
Set<Product> set = new HashSet<>();
set.add(p1);
set.add(p2);
assertEquals(1, set.size());

6.3 成果

七、最佳实践

7.1 正确重写 equals

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Product other = (Product) obj;
    return Objects.equals(productId, other.productId) &&
           Objects.equals(name, other.name);
}

7.2 正确重写 hashCode

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

7.3 使用 Lombok

代码

@EqualsAndHashCode
public class Product {
    private String productId;
    private String name;
}

7.4 性能优化

private final int hashCode;
public Product(String productId, String name) {
    this.productId = productId;
    this.name = name;
    this.hashCode = Objects.hash(productId, name);
}
@Override
public int hashCode() {
    return hashCode;
}

7.5 测试验证

@Test
void testHashCodeEqualsContract() {
    Product p1 = new Product("1", "Laptop");
    Product p2 = new Product("1", "Laptop");
    assertTrue(p1.equals(p2) && p1.hashCode() == p2.hashCode());
}

八、常见问题与解决方案

8.1 仅重写 equals

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

8.2 仅重写 hashCode

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Product other = (Product) obj;
    return Objects.equals(productId, other.productId) &&
           Objects.equals(name, other.name);
}

8.3 字段修改导致不一致

public final class Product {
    private final String productId;
    private final String name;
}

8.4 性能问题

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

8.5 空指针异常

Objects.equals(productId, other.productId)

九、未来趋势

9.1 记录类(Record)

9.2 性能优化

9.3 工具支持

十、总结

equalshashCode 是 Java 哈希集合的核心,需满足契约:equals 相等的对象 hashCode 必须相等。本文通过电商 Product 类案例,展示如何正确重写:

推荐实践

到此这篇关于Java 中的 equals 和 hashCode 方法关系与正确重写实践案例的文章就介绍到这了,更多相关java equals 和 hashCode方法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文