38. hashCode 方法重写原则

一、hashCode 方法基础概念

hashCode 方法的定义

hashCode 是 Java 中 Object 类的一个方法,其定义如下:

public native int hashCode();
  • native 方法:表示其实现由 JVM 底层提供,通常基于对象的内存地址或其他内部机制生成。
  • 返回值:一个 int 类型的哈希值,用于标识对象的“摘要”信息。

hashCode 方法的作用

hashCode 方法的主要作用是为对象提供一个散列值,用于支持基于哈希表的数据结构(如 HashMapHashSetHashtable)的高效存储和查找。具体表现为:

  1. 哈希表的键定位
    哈希表通过计算键的 hashCode 值确定存储位置(桶),从而快速定位数据。例如:

    HashMap<String, Integer> map = new HashMap<>();
    map.put("key", 1); // 内部调用 "key".hashCode() 计算存储位置
    
  2. 对象比较的辅助手段
    HashMap 等容器中,先通过 hashCode 快速筛选可能相等的对象,再通过 equals 方法精确比较。若两个对象的 hashCode 不同,则直接判定为不相等。

  3. 提高集合操作效率
    HashSet 中,hashCode 用于去重;在 HashMap 中,hashCode 决定了键值对的分布,直接影响查询性能。

核心原则

  1. 一致性
    在对象未被修改时,多次调用 hashCode() 应返回相同的值(与 equals 方法的行为一致)。

  2. 相等性
    如果 obj1.equals(obj2) 返回 true,则 obj1.hashCode() 必须等于 obj2.hashCode()。反之不成立(哈希冲突时,不同对象可能有相同哈希值)。

  3. 高效性
    哈希值的计算应尽量快速,避免成为性能瓶颈。

示例代码

@Override
public int hashCode() {
    // 基于对象字段的哈希值计算
    return Objects.hash(field1, field2); // JDK 提供的工具类
}

注意事项

  1. 重写 equals 必须重写 hashCode
    若只重写 equals 而不重写 hashCode,会导致违反“相等性”原则,破坏哈希容器的正常行为。

  2. 避免哈希冲突
    设计哈希算法时,应尽量使不同对象的哈希值均匀分布。例如,对多个字段的哈希值进行异或(^)或乘法运算。

  3. 不可变对象的哈希缓存
    对于不可变类,可以缓存哈希值以提高性能:

    private int cachedHashCode; // 延迟计算并缓存
    @Override
    public int hashCode() {
        if (cachedHashCode == 0) {
            cachedHashCode = Objects.hash(field1, field2);
        }
        return cachedHashCode;
    }
    

hashCode 方法与 equals 方法的关系

在 Java 中,hashCode 方法和 equals 方法是两个紧密相关的方法,它们共同用于对象的比较和哈希表(如 HashMapHashSet)中的存储和检索。理解它们之间的关系对于正确重写这两个方法至关重要。

基本关系
  1. 一致性要求

    • 如果两个对象通过 equals 方法比较是相等的,那么它们的 hashCode 值必须相同。
    • 反之,如果两个对象的 hashCode 值相同,它们通过 equals 方法比较不一定相等(哈希冲突是允许的)。
  2. 违反关系的后果

    • 如果违反了上述规则,可能会导致哈希表无法正常工作。例如,将对象存入 HashMap 后,可能无法通过相同的键检索到它。
为什么需要这种关系?

哈希表(如 HashMap)在存储和检索对象时,首先通过 hashCode 方法确定对象的存储位置(桶),然后在同一个桶内通过 equals 方法精确匹配对象。如果 hashCodeequals 不一致,可能会导致以下问题:

  • 相同的对象被分配到不同的桶中,无法正确匹配。
  • 不同的对象被分配到同一个桶中,但 equals 方法返回 false,导致无法检索。
示例代码

以下是一个正确重写 hashCodeequals 方法的示例:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
注意事项
  1. 同时重写

    • 如果重写了 equals 方法,必须同时重写 hashCode 方法,反之亦然。
  2. 不可变字段

    • 用于计算 hashCode 的字段应该是不可变的(或至少在对象生命周期内不改变)。如果字段变化,hashCode 值也会变化,可能导致哈希表中无法找到对象。
  3. 性能考虑

    • hashCode 方法应尽量高效,避免复杂计算。
    • hashCode 应尽量分布均匀,以减少哈希冲突。
  4. 工具类

    • 可以使用 Objects.hash() 或第三方库(如 Apache Commons 的 HashCodeBuilder)简化 hashCode 的实现。
常见误区
  1. 仅重写 equalshashCode

    • 只重写其中一个方法会导致哈希表行为异常。
  2. 依赖内存地址

    • 默认的 hashCode 实现通常基于内存地址,但重写 equals 后不应再依赖此实现。
  3. 忽略 null 检查

    • equals 方法中,应首先检查参数是否为 null

通过正确理解并实现 hashCodeequals 方法的关系,可以确保对象在哈希表中的行为符合预期。


Object 类中的 hashCode 默认实现

概念定义

Object 类中的 hashCode() 方法是 Java 中所有类的默认哈希码生成器。其默认实现通常返回对象的内存地址的整数表示(但并非绝对,具体取决于 JVM 实现)。该方法的签名如下:

public native int hashCode();
默认实现特点
  1. 内存地址关联性
    大多数 JVM 实现(如 HotSpot)会基于对象的内存地址计算哈希值,但规范并未强制要求。例如:

    Object obj1 = new Object();
    System.out.println(obj1.hashCode()); // 输出类似 356573597(与内存地址相关)
    
  2. 一致性
    在单次程序执行中,对同一对象多次调用 hashCode() 必须返回相同的值(即使对象被修改,除非重写逻辑)。

  3. equals() 的默认关系
    若未重写 equals(),默认的 Object.equals() 比较内存地址,此时与 hashCode() 的行为一致。但若重写 equals(),必须同步重写 hashCode()(后文详述)。

使用场景

默认实现适用于以下情况:

  • 对象唯一性仅由内存地址决定(如默认的 Object 实例)。
  • 不涉及哈希集合(如 HashMapHashSet)的键值存储。
问题与限制
  1. 哈希集合失效
    若将对象作为 HashMap 的键且未重写 hashCode(),可能导致无法正确检索:

    class Key {
        String id;
        Key(String id) { this.id = id; }
        @Override
        public boolean equals(Object o) { /* 比较 id 字段 */ }
        // 未重写 hashCode()!
    }
    
    Map<Key, String> map = new HashMap<>();
    map.put(new Key("k1"), "v1");
    System.out.println(map.get(new Key("k1"))); // 输出 null(哈希冲突)
    
  2. 不符合哈希契约
    若重写 equals() 但未重写 hashCode(),违反约定:
    相等对象必须拥有相同哈希码

示例代码(默认行为验证)
public class DefaultHashCodeDemo {
    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();
        
        System.out.println("obj1.hashCode(): " + obj1.hashCode());
        System.out.println("obj2.hashCode(): " + obj2.hashCode());
        System.out.println("obj1.equals(obj2): " + obj1.equals(obj2));
    }
}

输出示例(结果因运行环境而异):

obj1.hashCode(): 356573597
obj2.hashCode(): 1735600054
obj1.equals(obj2): false
注意事项
  • 不要依赖默认实现的具体值:不同 JVM 或同一程序的不同运行可能产生不同结果。
  • 重写原则:若重写 equals(),必须重写 hashCode(),确保逻辑一致。

二、hashCode 方法重写原则

一致性原则(对象不变则hashCode不变)

概念定义

一致性原则是Java中重写hashCode()方法时必须遵循的核心原则之一。它要求:在对象的生命周期内,只要用于计算哈希码的关键字段未被修改,该对象的hashCode()返回值必须始终保持一致。这一原则是哈希表类(如HashMapHashSet)正常工作的基础保障。

原理与必要性
  1. 哈希表依赖:当对象作为键存入HashMap时,哈希表会先通过hashCode()确定存储位置。若对象存入后hashCode()改变,后续get()操作将无法定位到原始位置,导致数据"丢失"。
  2. 契约性要求:Java规范明确规定:equals()比较相等的对象必须具有相同的hashCode()。若可变对象修改后hashCode()变化,可能破坏这一契约。
实现方式
public class User {
    private final String id;  // 关键字段设为final确保不变性
    private String name;

    @Override
    public int hashCode() {
        return Objects.hash(id);  // 仅用不可变字段计算哈希
    }
}
典型场景
  1. 不可变对象:如StringInteger等,天然满足一致性原则
  2. 可变对象作键:若必须用可变对象作HashMap的键,应:
    • 设计为仅用不可变字段计算哈希码
    • 或确保对象作为键期间不修改关键字段
违反后果示例
HashMap<Student, String> map = new HashMap<>();
Student s = new Student("2023001");
map.put(s, "张三");

s.setId("2023002");  // 修改关键字段
System.out.println(map.get(s));  // 输出null,无法找到原值
注意事项
  1. equals()同步:若重写equals(),必须同步重写hashCode(),且使用相同的字段集合
  2. 性能考虑:对于频繁用作键的对象,建议:
    • 实现为不可变类
    • 或缓存哈希码(适用于计算成本高的场景)
  3. 文档标注:在API文档中明确说明对象的哈希计算是否依赖可变字段
缓存优化示例
private volatile int cachedHashCode;  // 添加缓存字段

@Override
public int hashCode() {
    if (cachedHashCode == 0) {
        cachedHashCode = Objects.hash(id, name);
    }
    return cachedHashCode;
}

equals相等则hashCode必须相等

概念定义

在Java中,hashCode()equals()方法是两个紧密相关的方法。hashCode()方法返回对象的哈希码值,而equals()方法用于比较两个对象是否相等。根据Java规范,如果两个对象通过equals()方法比较相等,那么它们的hashCode()方法必须返回相同的值。这一原则被称为hashCode契约

使用场景
  1. 哈希表hashCode()方法主要用于哈希表(如HashMapHashSet等)中,用于快速定位对象的存储位置。如果两个对象相等(equals()返回true),但hashCode()不同,会导致哈希表无法正确工作。
  2. 对象比较:在需要比较对象时,hashCode()可以作为初步筛选条件。如果hashCode()不同,可以直接判定对象不相等,避免调用equals()方法。
常见误区或注意事项
  1. 违反契约的后果:如果equals()相等但hashCode()不相等,会导致哈希表无法正确存储或检索对象。例如,将对象存入HashMap后,可能无法通过相同的键值对取出。
  2. 性能影响:虽然hashCode()可以不同但equals()相等的情况不会导致程序错误,但会显著降低哈希表的性能。
  3. 不可变对象:如果对象的equals()hashCode()依赖于可变字段,一旦字段值改变,可能导致对象在哈希表中的行为异常。
示例代码
import java.util.Objects;

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

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

    public static void main(String[] args) {
        Person p1 = new Person("Alice", 30);
        Person p2 = new Person("Alice", 30);

        System.out.println(p1.equals(p2)); // true
        System.out.println(p1.hashCode() == p2.hashCode()); // true
    }
}
代码说明
  1. equals()方法比较nameage字段,确保逻辑一致性。
  2. hashCode()方法使用Objects.hash()生成哈希码,确保与equals()方法一致。
  3. 如果p1p2equals()返回true,它们的hashCode()也必然相同。

equals不相等则hashCode尽量不相等

概念定义

这是Java中重写hashCode()方法的一个重要原则,指的是:如果两个对象通过equals()方法比较结果为不相等,那么它们的hashCode()返回值应尽量不相同。这个原则是hashCode()equals()方法契约的一部分。

为什么需要这个原则
  1. 哈希表性能优化:在HashMapHashSet等哈希集合中,如果不同对象返回相同的哈希码,会导致哈希冲突增加,降低查找效率(退化为链表查找)。
  2. 逻辑一致性:如果两个不相等的对象具有相同的哈希码,虽然不违反hashCode契约,但会影响哈希表的正常使用。
实现方式
@Override
public int hashCode() {
    // 根据对象中参与equals比较的字段计算哈希码
    return Objects.hash(field1, field2, ...);
}
注意事项
  1. 不是绝对要求:规范中只要求equals相等的对象必须具有相同hashCode,反过来是建议而非强制
  2. 冲突不可避免:由于哈希码范围有限(int类型),不同对象仍可能产生相同哈希码(哈希冲突)
  3. 性能考量:好的哈希算法应在减少冲突和计算效率之间取得平衡
示例场景
class Person {
    String name;
    int age;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person person = (Person) o;
        return age == person.age && name.equals(person.name);
    }
    
    @Override
    public int hashCode() {
        // 使用相同字段计算哈希码
        return Objects.hash(name, age);
    }
}
违反后果

如果违反此原则(不同对象返回相同哈希码):

  1. HashSet可能包含"重复"元素
  2. HashMap的查找效率降低
  3. 可能引发难以排查的逻辑错误

不可变对象的最佳实践

什么是不可变对象

不可变对象是指一旦创建后,其状态(即属性值)就不能被修改的对象。任何修改操作都会返回一个新的对象,而不是改变原有对象的状态。

为什么使用不可变对象
  1. 线程安全:不可变对象天然线程安全,无需同步机制。
  2. 简化代码:减少了状态变化的复杂性,更容易理解和维护。
  3. 缓存友好:可以安全地缓存哈希值或其他计算结果。
  4. 避免副作用:防止对象被意外修改,减少bug。
实现不可变对象的最佳实践
1. 将类声明为final

防止子类覆盖方法改变对象状态。

public final class ImmutablePerson {
    // ...
}
2. 所有字段设为private final

确保字段不能被修改,且只能在构造函数中初始化。

private final String name;
private final int age;
3. 不提供setter方法

避免外部代码修改对象状态。

4. 深度防御性拷贝

对于可变对象引用:

  • 在构造函数中创建副本
  • 在getter方法中返回副本
public ImmutablePerson(String name, int age, List<String> hobbies) {
    this.name = name;
    this.age = age;
    this.hobbies = new ArrayList<>(hobbies); // 防御性拷贝
}

public List<String> getHobbies() {
    return new ArrayList<>(hobbies); // 返回副本
}
5. 使用不可变集合

考虑使用Collections.unmodifiableList等包装器:

private final List<String> hobbies;

public ImmutablePerson(List<String> hobbies) {
    this.hobbies = Collections.unmodifiableList(new ArrayList<>(hobbies));
}
6. 谨慎处理数组

数组元素可以被修改,应该:

  • 私有化数组
  • 不直接返回数组引用
  • 必要时返回数组的拷贝
private final String[] tags;

public String[] getTags() {
    return tags.clone();
}
7. 方法返回新对象而非修改当前对象

对于修改操作,返回新实例:

public ImmutablePerson withAge(int newAge) {
    return new ImmutablePerson(this.name, newAge, this.hobbies);
}
不可变对象的性能考虑
  1. 频繁创建对象可能带来GC压力
  2. 解决方案
    • 使用对象池(如String的常量池)
    • 重用常见值实例
    • 考虑使用Builder模式构建复杂对象
示例:完整的不可变类
public final class ImmutablePerson {
    private final String name;
    private final int age;
    private final List<String> hobbies;
    
    public ImmutablePerson(String name, int age, List<String> hobbies) {
        this.name = name;
        this.age = age;
        this.hobbies = Collections.unmodifiableList(new ArrayList<>(hobbies));
    }
    
    public String getName() { return name; }
    public int getAge() { return age; }
    public List<String> getHobbies() { return new ArrayList<>(hobbies); }
    
    public ImmutablePerson withName(String newName) {
        return new ImmutablePerson(newName, this.age, this.hobbies);
    }
}
Java中的不可变类示例
  • String
  • 基本类型的包装类(Integer, Long等)
  • BigInteger, BigDecimal
  • 不可变集合(Collections.unmodifiableXxx)

三、hashCode 方法实现技巧

素数乘数减少碰撞的原理

为什么使用素数乘数

在重写 hashCode() 方法时,使用素数作为乘数可以减少哈希碰撞的概率。这是因为:

  1. 素数性质:素数只能被1和自身整除,减少了因乘法运算导致哈希值分布不均匀的可能性。
  2. 数学特性:素数与其他数字相乘时,结果更容易均匀分布,降低哈希冲突。
常见素数选择

常用的素数乘数包括:

  • 31:Java String 类的默认选择,性能与分布均衡。
  • 17:较小的素数,适合简单对象。
  • 37:更大的素数,适合复杂对象。
实现示例
@Override
public int hashCode() {
    int result = 17; // 初始值为素数
    result = 31 * result + field1.hashCode();
    result = 31 * result + (field2 != null ? field2.hashCode() : 0);
    return result;
}
注意事项
  1. 性能权衡:较大的素数(如37)可能增加计算开销。
  2. 字段顺序:乘数顺序不影响结果,但需保持一致。
  3. 不可变对象:若对象不可变,可缓存哈希值。
为什么31被广泛使用
  • 优化计算31 * i 可优化为 (i << 5) - i,JVM自动处理。
  • 经验验证:长期实践证明其分布均匀性较好。
扩展场景

对于数组或集合类字段,可进一步结合 Arrays.hashCode()List.hashCode()

result = 31 * result + Arrays.hashCode(arrayField);

基本类型字段的hash计算

在重写hashCode()方法时,处理基本类型字段的哈希计算是关键步骤。基本类型(如intlongfloatdoublecharbooleanbyteshort)的哈希值计算方式各有特点。


基本类型与哈希计算规则
  1. int类型
    直接使用字段值作为哈希值,或通过简单运算(如乘法)分散哈希分布。

    int value = 42;
    int hashCode = value; // 直接使用
    
  2. long类型
    long是64位,需通过位运算将其混合到32位哈希值中。常用方法是异或高32位和低32位:

    long value = 123456789L;
    int hashCode = (int)(value ^ (value >>> 32));
    
  3. float类型
    使用Float.floatToIntBits()将浮点数转为IEEE 754标准的整数形式,再计算哈希:

    float value = 3.14f;
    int hashCode = Float.floatToIntBits(value);
    
  4. double类型
    类似long,先转为64位整数,再拆分高低位:

    double value = 2.71828;
    long bits = Double.doubleToLongBits(value);
    int hashCode = (int)(bits ^ (bits >>> 32));
    
  5. boolean类型
    通常用true为1,false为0的固定值:

    boolean flag = true;
    int hashCode = flag ? 1 : 0;
    
  6. charbyteshort类型
    直接转为int即可:

    char ch = 'A';
    byte b = 127;
    short s = 100;
    int hashCode = (int) ch + b + s;
    

组合多个基本类型字段的哈希值

实际场景中,对象的哈希值通常由多个字段组合生成。推荐使用31作为乘数(因其是奇素数,可减少哈希冲突):

@Override
public int hashCode() {
    int result = 17; // 初始值
    result = 31 * result + intField;
    result = 31 * result + (int)(longField ^ (longField >>> 32));
    result = 31 * result + Float.floatToIntBits(floatField);
    return result;
}

注意事项
  1. 一致性:相同字段值必须生成相同的哈希值(即使对象被修改后不应再参与哈希计算)。
  2. 避免溢出31 * result可能导致溢出,但无需处理,溢出是哈希计算的正常行为。
  3. 性能:基本类型的计算是高效的,无需过度优化。

引用类型字段的hash处理

概念定义

引用类型字段的hash处理是指在重写hashCode()方法时,如何处理类中的引用类型成员变量(如String、自定义类、集合等)。由于引用类型存储的是对象的内存地址,直接使用其默认hashCode()可能导致相同逻辑内容的对象产生不同的哈希值。

核心原则
  1. 一致性:当对象参与equals比较的字段未改变时,多次调用hashCode()应返回相同值
  2. 等价性:如果两个对象equals()返回true,它们的hashCode()必须相同
  3. 分散性:不相等的对象应尽量产生不同的哈希值(非强制要求,但影响哈希表性能)
常见处理方法
1. String类型字段
@Override
public int hashCode() {
    return name.hashCode();  // String已良好实现hashCode
}
2. 自定义类字段
@Override
public int hashCode() {
    return department.hashCode();  // 要求Department类也正确实现了hashCode
}
3. 数组类型字段
@Override
public int hashCode() {
    return Arrays.hashCode(scores);  // 使用Arrays工具类
}
4. 集合类型字段
@Override
public int hashCode() {
    return projects.hashCode();  // List/Set等集合已实现hashCode
}
组合多个字段的推荐做法
// 使用Objects.hash()(Java 7+)
@Override
public int hashCode() {
    return Objects.hash(name, age, department); 
}

// 传统实现方式
@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + name.hashCode();
    result = 31 * result + age;
    result = 31 * result + (department == null ? 0 : department.hashCode());
    return result;
}
注意事项
  1. 空引用处理:必须检查字段是否为null

    return (field == null) ? 0 : field.hashCode();
    
  2. 递归问题:避免循环引用导致无限递归

    // 类A包含类B实例,类B又包含类A实例
    
  3. 性能考虑

    • 对于频繁使用的对象,可缓存哈希值(但需保证对象不可变)
    • 复杂的hash计算可能影响性能
  4. 与equals()的同步

    // 错误示例:equals比较name,hashCode却用了id
    public boolean equals(Object o) {
        return this.name.equals(((Student)o).name);
    }
    public int hashCode() {
        return id;  // 违反等价性原则
    }
    
最佳实践示例
public class Employee {
    private String name;
    private int age;
    private Department dept;
    private List<Project> projects;
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age, dept, projects);
    }
    
    // 对应的equals实现
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Employee)) return false;
        Employee e = (Employee) o;
        return age == e.age &&
               Objects.equals(name, e.name) &&
               Objects.equals(dept, e.dept) &&
               Objects.equals(projects, e.projects);
    }
}

数组类型字段的hash处理

在重写hashCode()方法时,如果类中包含数组类型的字段,需要特别注意如何正确计算其哈希值。由于数组是引用类型,直接使用默认的hashCode()方法可能会导致不符合预期的结果。

为什么需要特殊处理数组字段?
  1. 数组的默认hashCode()行为
    数组对象继承自ObjecthashCode()方法,其计算基于内存地址。即使两个数组内容完全相同,只要引用不同,哈希值也会不同。

  2. 违反hashCode契约
    如果两个对象逻辑相等(通过equals()判断为true),但hashCode()返回不同值,会破坏哈希集合(如HashMap)的正常工作。

处理方法
方法1:使用Arrays.hashCode()

Java提供了Arrays.hashCode()静态方法,支持对基本类型数组和对象数组计算哈希值:

int[] arr = {1, 2, 3};
int hash = Arrays.hashCode(arr);  // 基于数组内容计算
方法2:手动实现

对于多维数组或需要特殊处理的情况:

@Override
public int hashCode() {
    int result = 17;
    for (Object element : arrayField) {
        result = 31 * result + (element == null ? 0 : element.hashCode());
    }
    return result;
}
多维数组处理

对于多维数组,使用Arrays.deepHashCode()

String[][] matrix = {{"a", "b"}, {"c", "d"}};
int hash = Arrays.deepHashCode(matrix);  // 递归计算每个元素
注意事项
  1. 空数组处理
    空数组应返回固定值(通常为0),与Arrays.hashCode()行为一致。

  2. 元素为null
    需要显式处理null元素,避免NullPointerException

  3. 性能考虑
    大型数组的哈希计算可能较耗时,必要时可缓存哈希值(但需确保对象不可变)。

完整示例
public class Matrix {
    private int[][] data;

    @Override
    public int hashCode() {
        return Arrays.deepHashCode(data);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Matrix)) return false;
        Matrix matrix = (Matrix) o;
        return Arrays.deepEquals(data, matrix.data);
    }
}
常见误区
  1. 直接调用arrayField.hashCode()
    错误示例:

    // 错误!基于引用而非内容计算
    @Override
    public int hashCode() {
        return arrayField.hashCode();
    }
    
  2. 忽略多维数组的深层比较
    对于多维数组,Arrays.hashCode()仅计算第一维的哈希值。

  3. 哈希计算与equals()不一致
    必须确保当equals()返回true时,hashCode()返回值相同。


组合多个字段的hash值

为什么需要组合多个字段的hash值

在重写hashCode()方法时,通常会遇到需要基于多个字段来计算哈希值的情况。这是因为:

  1. 对象的唯一性往往由多个字段共同决定
  2. 单独使用某个字段可能导致哈希冲突率过高
  3. 需要满足"相等的对象必须有相等的哈希码"的约定
常用组合方法
1. 使用Objects.hash()方法(Java 7+推荐)
@Override
public int hashCode() {
    return Objects.hash(field1, field2, field3);
}
  • 自动处理null值
  • 内部使用31作为乘数
  • 简洁易读
2. 传统方式(手动实现)
@Override
public int hashCode() {
    int result = 17;  // 非零初始值
    result = 31 * result + (field1 == null ? 0 : field1.hashCode());
    result = 31 * result + (field2 == null ? 0 : field2.hashCode());
    result = 31 * result + (field3 == null ? 0 : field3.hashCode());
    return result;
}
  • 31是经验选择的质数,具有良好的分布特性
  • 31的另一个好处是编译器可以优化为移位操作:31 * i = (i << 5) - i
3. 使用Arrays.hashCode()

适用于数组字段:

@Override
public int hashCode() {
    int result = Objects.hash(field1, field2);
    result = 31 * result + Arrays.hashCode(arrayField);
    return result;
}
注意事项
  1. 一致性:在对象生命周期内,只要用于比较的字段不变,hashCode应返回相同值
  2. 性能:计算不应过于复杂
  3. 冲突率:应尽可能减少不同对象的哈希冲突
  4. 不可变字段优先:最好基于不可变字段计算哈希码
  5. 排除冗余字段:不参与equals比较的字段不应参与hashCode计算
示例:完整类实现
public class Person {
    private final String name;
    private final int age;
    private final String[] addresses;
    
    @Override
    public int hashCode() {
        int result = Objects.hash(name, age);
        result = 31 * result + Arrays.hashCode(addresses);
        return result;
    }
    
    @Override
    public boolean equals(Object o) {
        // equals实现应与hashCode一致
        // ...
    }
}
为什么选择31作为乘数
  1. 奇质数,有助于减少哈希冲突
  2. 足够小,避免溢出问题
  3. 可以被JVM优化为位运算
  4. 经验证在各种数据集上表现良好
处理特殊字段类型
  1. 数组字段:使用Arrays.hashCode()
  2. 集合字段:使用集合自身的hashCode()
  3. 自定义对象:调用其hashCode()方法
  4. 基本类型:使用包装类的hashCode()或直接使用值

null值的安全处理

概念定义

在Java中,null表示一个引用变量不指向任何对象。当尝试调用null引用对象的方法或访问其属性时,会抛出NullPointerException(NPE)。安全处理null值是指在编程中采取预防措施,避免因null引用导致的运行时异常。

使用场景
  1. 对象方法调用前:在调用对象方法前检查对象是否为null
  2. 集合操作时:处理可能为null的集合
  3. 方法返回值:方法可能返回null时,调用方需要处理
  4. 外部数据输入:如用户输入、数据库查询结果、API响应等
常见处理方式
1. 显式null检查
if (obj != null) {
    obj.doSomething();
} else {
    // 处理null情况
}
2. Objects工具类(Java 7+)
Objects.requireNonNull(obj, "对象不能为null");
3. Optional类(Java 8+)
Optional<String> optional = Optional.ofNullable(getString());
String result = optional.orElse("default");
4. 字符串处理
String str = null;
String result = str == null ? "" : str;
常见误区
  1. 过度防御:对不可能为null的对象进行不必要的检查
  2. 忽略检查:假设外部输入或方法返回值永远不会为null
  3. 隐藏问题:使用空值代替实际处理,可能掩盖潜在逻辑错误
  4. 性能影响:过多的null检查可能影响代码可读性和性能
最佳实践
  1. 明确约定:在方法文档中明确说明参数和返回值是否允许null
  2. 尽早失败:在方法开始处检查必要参数是否为null
  3. 合理设计:考虑使用空对象模式替代null
  4. 统一策略:团队应制定统一的null处理规范
示例代码
public class NullSafetyExample {
    public static void main(String[] args) {
        // 传统方式
        String name = getName();
        if (name != null) {
            System.out.println(name.length());
        }
        
        // Optional方式
        Optional.ofNullable(getName())
                .ifPresent(n -> System.out.println(n.length()));
    }
    
    private static String getName() {
        // 可能返回null
        return Math.random() > 0.5 ? "Alice" : null;
    }
}
注意事项
  1. 集合类与nullCollections.emptyList()比返回null更好
  2. 数组与null:空数组优于null数组
  3. 自动装箱:注意基本类型包装类可能为null
  4. 框架集成:如Spring的@Nullable@NonNull注解

四、hashCode 方法性能优化

缓存hashCode值

概念定义

缓存hashCode值是指在对象内部存储其hashCode计算结果,避免重复计算的一种优化技术。当对象的hashCode()方法被多次调用时,直接返回预先计算并存储的值,而不是每次都重新计算。

使用场景
  1. 不可变对象:对于不可变对象(如String、Integer等),其hashCode值在生命周期内不会改变,非常适合缓存。
  2. 频繁调用hashCode():当对象的hashCode()方法会被频繁调用时(如作为HashMap的键),缓存可以显著提升性能。
  3. 计算成本高:如果hashCode的计算涉及复杂运算或大量数据,缓存可以避免重复计算的开销。
实现方式
延迟初始化
private int cachedHashCode; // 默认为0

@Override
public int hashCode() {
    if (cachedHashCode == 0) {
        // 实际计算逻辑
        cachedHashCode = Objects.hash(field1, field2);
    }
    return cachedHashCode;
}
初始化时计算(适用于不可变对象)
private final int cachedHashCode;

public MyClass(Object field1, Object field2) {
    this.field1 = field1;
    this.field2 = field2;
    this.cachedHashCode = Objects.hash(field1, field2);
}

@Override
public int hashCode() {
    return cachedHashCode;
}
注意事项
  1. 可变对象风险:如果对象是可变的,缓存hashCode会导致不一致性。修改对象后hashCode值不会更新,可能导致哈希表操作错误。

  2. 零值冲突:使用延迟初始化时,要确保实际计算的hashCode不会为0(或使用包装类型Integer并初始化为null)。

  3. 线程安全:多线程环境下需要同步处理,或使用volatile关键字:

    private volatile int cachedHashCode;
    
  4. 空间权衡:缓存会增加每个对象的内存开销,需根据实际情况权衡。

性能对比示例
// 无缓存版本
@Override
public int hashCode() {
    return Objects.hash(name, age, address); // 每次调用都重新计算
}

// 有缓存版本
@Override
public int hashCode() {
    int h = cachedHashCode;
    if (h == 0) {
        h = Objects.hash(name, age, address);
        cachedHashCode = h;
    }
    return h;
}
Java标准库示例

String类的hashCode实现:

private int hash; // 缓存字段

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

延迟初始化hashCode

概念定义

延迟初始化hashCode(Lazy Initialization of hashCode)是一种优化技术,指在对象创建时不立即计算hashCode值,而是在首次调用hashCode()方法时才进行计算,并将结果缓存以供后续使用。这种技术特别适用于计算hashCode成本较高的对象。

使用场景
  1. 计算代价高的hashCode:当对象的hashCode需要基于复杂计算或大量数据时
  2. 不一定会使用hashCode的情况:如果对象可能不会被放入哈希集合(如HashMap/HashSet)
  3. 不可变对象:特别适合不可变对象,因为hashCode一旦计算就不需要改变
实现方式

典型的延迟初始化hashCode实现包含以下要素:

  1. 一个volatile或AtomicInteger字段存储缓存值
  2. 双重检查锁定模式(对可变对象)
  3. 对于不可变对象可以简化实现
示例代码
// 可变对象的线程安全实现
public class LazyHashCodeExample {
    private volatile int hashCode; // 使用volatile保证可见性
    
    @Override
    public int hashCode() {
        int result = hashCode;
        if (result == 0) { // 第一次检查(无锁)
            synchronized(this) {
                result = hashCode;
                if (result == 0) { // 第二次检查(加锁后)
                    // 实际计算hashCode的逻辑
                    result = Objects.hash(field1, field2, field3);
                    hashCode = result;
                }
            }
        }
        return result;
    }
}

// 不可变对象的简化实现
public final class ImmutableExample {
    private final int field1;
    private final String field2;
    private int hashCode; // 不需要volatile,因为对象不可变
    
    @Override
    public int hashCode() {
        if (hashCode == 0) {
            hashCode = Objects.hash(field1, field2);
        }
        return hashCode;
    }
}
注意事项
  1. 线程安全

    • 对于可变对象必须使用适当的同步机制
    • 不可变对象可以不需要同步
  2. hashCode为0的情况

    • 当计算出的hashCode确实为0时,会导致重复计算
    • 可以通过使用特殊值标记(如Integer.MIN_VALUE)或接受少量性能损失
  3. 对象可变性

    • 如果对象是可变的且用作HashMap的键,延迟初始化可能导致问题
    • 这种情况下应该确保对象作为键时不会修改影响hashCode的字段
  4. 性能考量

    • 只有当hashCode计算确实昂贵时才值得使用
    • 简单对象的延迟初始化可能反而降低性能
与立即初始化的对比
特性延迟初始化立即初始化
初始化时机首次调用hashCode()时对象构造时
内存占用可能更少(未计算时不占空间)总是占用
计算成本分散在首次使用时集中在构造时
适用场景hashCode计算昂贵/可能不用hashCode简单/必定使用

避免复杂计算

概念定义

在重写 hashCode() 方法时,"避免复杂计算"指的是确保哈希值的生成逻辑尽可能简单高效,避免在计算哈希值时执行过多的运算或调用耗时的操作。哈希值的计算应该快速且稳定,以保证对象的哈希码能够高效地被使用(例如在 HashMapHashSet 等集合中)。

使用场景
  1. 高频调用的场景:在集合类(如 HashMapHashSet)中,hashCode() 方法会被频繁调用(例如在插入、查找、删除时),因此必须保证其计算速度足够快。
  2. 性能敏感的应用:在需要高性能的应用程序中,复杂的哈希计算可能导致性能瓶颈,因此应尽量简化计算逻辑。
常见误区或注意事项
  1. 避免递归或深度嵌套计算:如果 hashCode() 依赖于其他对象的 hashCode(),而这些对象又可能形成循环依赖,可能会导致无限递归或性能问题。
  2. 避免调用耗时方法:不要在 hashCode() 方法中执行 I/O 操作、数据库查询或网络请求等耗时操作。
  3. 避免频繁的对象创建:在计算哈希值时,避免创建临时对象(如字符串拼接或数组构造),以减少 GC 压力。
  4. 保持一致性:虽然计算要简单,但仍需确保哈希值的分布均匀,以减少哈希冲突。
示例代码

以下是一个优化的 hashCode() 实现示例,避免复杂计算:

public class Person {
    private String name;
    private int age;
    private String address;

    @Override
    public int hashCode() {
        // 使用简单的算术运算和位运算,避免复杂计算
        int result = 17; // 初始值,通常选择一个质数
        result = 31 * result + (name != null ? name.hashCode() : 0);
        result = 31 * result + age;
        result = 31 * result + (address != null ? address.hashCode() : 0);
        return result;
    }
}
优化点说明:
  1. 使用 31 作为乘数31 是一个奇质数,且 31 * x 可以优化为 (x << 5) - x,提高计算效率。
  2. 避免重复计算:直接使用字段的 hashCode() 或原始类型的值,不进行额外处理。
  3. 处理 null:在计算时检查 null,避免 NullPointerException,但逻辑仍然简单。

权衡计算速度与分布均匀性

概念定义

在重写 hashCode() 方法时,计算速度分布均匀性是两个关键考量因素:

  • 计算速度:指生成哈希值的效率,通常希望 hashCode() 方法尽可能快,尤其是在高频调用的场景(如哈希表操作)。
  • 分布均匀性:指哈希值在不同对象间应尽可能均匀分布,以减少哈希冲突(即不同对象产生相同哈希值),从而提升哈希表性能(如 HashMap 的查找效率)。

两者往往需要权衡:过于复杂的算法可能保证均匀性但牺牲速度;过于简单的算法可能计算快但导致冲突增多。


使用场景
  1. 高频读写场景(如实时计算):优先考虑计算速度,选择简单高效的哈希算法。
  2. 大数据量存储(如缓存系统):优先考虑分布均匀性,减少冲突以提升整体性能。

常见误区与注意事项
  1. 过度优化均匀性

    • 使用加密哈希(如 SHA-256)虽然分布均匀,但计算成本极高,不适合常规 hashCode()
    • 应选择适合业务数据的轻量级算法(如素数乘法、位运算)。
  2. 忽视关键字段

    • 若仅对部分字段计算哈希,可能导致不同对象哈希值相同(如 Person 类只对 name 哈希,忽略 age)。
  3. 依赖可变字段

    • 若哈希值基于可变字段(如 age),对象修改后会导致哈希值变化,破坏哈希表的一致性(如 HashMap 中无法正确查找)。

示例代码
平衡速度与均匀性的典型实现
@Override
public int hashCode() {
    // 使用素数 31 乘法 + 字段哈希组合
    int result = 17; // 非零初始值
    result = 31 * result + name.hashCode(); // String 已有良好哈希实现
    result = 31 * result + age;           // 直接使用基本类型值
    return result;
}

说明

  • 31 的优化31 * i 可优化为 (i << 5) - i(JVM 自动处理),兼顾速度与分布。
  • 字段选择:覆盖所有关键字段(nameage),避免冲突。
不推荐的极端案例
// 过度追求速度(易冲突)
@Override
public int hashCode() {
    return 1; // 所有对象哈希相同,导致哈希表退化为链表
}

// 过度追求均匀性(计算慢)
@Override
public int hashCode() {
    return Objects.hash(name, age, birthDate, address); // 内部使用数组哈希,开销较大
}

五、常见问题与陷阱

违反hashCode契约的后果

概念定义

hashCode契约是Java中Object类定义的规范,要求所有重写hashCode()方法的类必须遵守以下规则:

  1. 在程序执行期间,若对象未被修改(用于equals比较的字段不变),则多次调用hashCode()必须返回相同值
  2. 若两个对象通过equals()比较相等,则它们的hashCode()必须返回相同值
  3. 若两个对象通过equals()比较不相等,它们的hashCode()不要求必须不同(但不同时能提升哈希表性能)
主要后果
1. 哈希集合异常行为

当对象作为HashMap/HashSet的键时:

Map<Student, String> map = new HashMap<>();
Student s1 = new Student("Alice", 20);  // 假设hashCode只计算name
Student s2 = new Student("Alice", 21);  // 相同name不同age

map.put(s1, "value");
System.out.println(map.containsKey(s2));  // 若equals比较age但hashCode不比较,可能返回错误结果
2. 数据丢失风险

在HashSet中可能出现重复元素:

Set<Point> set = new HashSet<>();
Point p1 = new Point(1, 2);  // 假设hashCode只使用x坐标
Point p2 = new Point(1, 3);  // x相同y不同

set.add(p1);
set.add(p2);  // 可能被错误判定为已存在
3. 性能退化

违反第三条规则(不等对象相同hashCode)会导致:

  • HashMap退化为链表(哈希冲突加剧)
  • 时间复杂度从O(1)降为O(n)
典型违反场景
1. 可变对象作为键
class Employee {
    private String name;
    // 省略setter
    
    @Override 
    public int hashCode() {
        return name.hashCode();
    }
}

Employee e = new Employee("Bob");
Map<Employee, String> map = new HashMap<>();
map.put(e, "data");

e.setName("Alice");  // 修改后hashCode改变
map.get(e);  // 可能返回null
2. equals/hashCode不一致
class Person {
    private String id;
    
    public boolean equals(Object o) {
        return ((Person)o).id.equals(this.id);
    }
    
    // 忘记重写hashCode
}
解决方案
  1. 使用final字段计算hashCode
  2. 保证equals比较的所有字段都参与hashCode计算
  3. 使用IDE自动生成方法(如IntelliJ的Generate→equals()和hashCode()
  4. 对于可变对象,避免作为哈希集合的键
JDK工具支持
// Java 7+推荐方式
@Override
public int hashCode() {
    return Objects.hash(field1, field2, field3);
}
调试技巧

当发现哈希集合行为异常时,可通过以下方式验证:

System.out.println("obj1 hash: " + obj1.hashCode());
System.out.println("obj2 hash: " + obj2.hashCode());
System.out.println("equals: " + obj1.equals(obj2));

可变对象作为key的风险

概念定义

在Java中,当我们将可变对象(Mutable Object)用作HashMapHashSet等哈希表结构的键(key)时,如果对象的内容在存入哈希表后被修改,可能会导致严重的逻辑错误和数据不一致问题。这是因为哈希表依赖hashCode()equals()方法来定位和操作键值对。

核心问题
  1. 哈希值变化导致定位失败
    哈希表在存储键值对时,会根据键的hashCode()计算桶(bucket)的位置。如果键的哈希值在存入后被修改,后续通过该键查找时,计算出的新哈希值可能指向错误的桶,导致无法找到原本存储的值。

  2. 破坏哈希表的不变性
    哈希表的设计假设键的哈希值在其生命周期内保持不变。如果键被修改,哈希表的内部结构会被破坏,可能导致数据丢失或死循环(例如在并发场景中)。

示例代码
import java.util.HashMap;
import java.util.Map;

class MutableKey {
    private int value;

    public MutableKey(int value) {
        this.value = value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return value; // 哈希值直接依赖可变字段value
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (!(obj instanceof MutableKey)) return false;
        return this.value == ((MutableKey) obj).value;
    }
}

public class Main {
    public static void main(String[] args) {
        Map<MutableKey, String> map = new HashMap<>();
        MutableKey key = new MutableKey(1);
        map.put(key, "Original Value");

        System.out.println(map.get(key)); // 输出: "Original Value"

        key.setValue(2); // 修改key的字段
        System.out.println(map.get(key)); // 输出: null(定位失败)
    }
}
风险场景
  1. 数据丢失
    如示例所示,修改键后无法通过get()获取原本存储的值。
  2. 内存泄漏
    哈希表中残留无法访问的键值对(因为原键的哈希值已改变)。
  3. 并发问题
    多线程环境下,键的修改可能导致哈希表内部状态不一致。
解决方案
  1. 使用不可变对象作为key
    StringInteger等,其哈希值在创建后不会改变。
  2. 深拷贝键对象
    如果必须使用可变对象,存入哈希表前创建其深拷贝副本,避免外部修改。
  3. 避免修改已作为key的对象
    通过设计约束(如私有字段+无setter)确保键的不可变性。
注意事项
  • 即使重写了hashCode()equals(),也无法规避可变键的风险。
  • 使用IdentityHashMap(依赖==而非equals())可缓解问题,但会牺牲逻辑相等性。

IDE自动生成的潜在问题

概念定义

IDE(集成开发环境)自动生成的hashCode()方法通常基于对象的字段值计算哈希码,旨在简化开发流程。然而,这种自动化实现可能存在隐藏问题,尤其在涉及对象状态变化、继承关系或特定业务场景时。

常见问题及场景分析
可变对象哈希码不一致
public class User {
    private String name;
    private int age;
    
    // IDE生成的hashCode
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    
    public void setAge(int age) { this.age = age; }
}

User user = new User("Alice", 25);
Set<User> set = new HashSet<>();
set.add(user);
user.setAge(30);  // 修改后哈希码改变
System.out.println(set.contains(user));  // 可能返回false
  • 问题:对象存入哈希集合后修改字段值,导致后续查找失败
  • 建议:设计不可变对象或避免修改参与哈希计算的字段
继承关系破坏约定
class Animal {
    private String species;
    // IDE生成hashCode仅包含species
}

class Dog extends Animal {
    private String breed;
    // 忘记重写hashCode导致父子类实例可能产生相同哈希码
}
  • 问题:子类未正确重写方法,违反"相等对象必须具有相同哈希码"原则
  • 建议:使用@Override注解检查,或使用getClass() == obj.getClass()严格比较
性能隐患
public class Product {
    private String[] tags;  // 大数组
    private String description;  // 长文本
    
    // IDE生成hashCode会遍历所有字段
    @Override
    public int hashCode() {
        return Objects.hash(tags, description);
    }
}
  • 问题:对大型数组或复杂对象计算哈希码效率低下
  • 建议:选择关键标识字段计算或缓存哈希值
最佳实践方案
选择性字段参与
@Override
public int hashCode() {
    // 只选唯一标识字段(如数据库主键)
    return Objects.hash(id);
}
不可变对象优化
private volatile int cachedHashCode;  // 缓存哈希值

@Override
public int hashCode() {
    if (cachedHashCode == 0) {
        cachedHashCode = Objects.hash(name, createTime);
    }
    return cachedHashCode;
}
使用第三方库
// Apache Commons Lang
@Override
public int hashCode() {
    return new HashCodeBuilder(17, 37)
            .append(id)
            .append(name)
            .toHashCode();
}
验证工具推荐
  1. 单元测试:验证对称性、传递性、一致性
  2. SonarQube:检测违反hashCode-equals约定的代码
  3. JArchitect:分析哈希码计算性能热点

不同JVM实现的差异考虑

概念定义

在Java中,hashCode()方法是Object类的一个方法,用于返回对象的哈希码值。哈希码主要用于哈希表(如HashMapHashSet等)中快速定位对象。由于Java虚拟机(JVM)有多种实现(如HotSpot、OpenJ9、GraalVM等),不同的JVM可能在hashCode()方法的实现上存在差异,尤其是在默认的Object.hashCode()实现中。

使用场景
  1. 哈希表性能优化:不同的JVM实现可能采用不同的哈希算法,从而影响哈希表的性能。
  2. 跨JVM一致性:如果应用需要在不同的JVM上运行(如从HotSpot切换到OpenJ9),默认的hashCode()行为可能不一致,导致程序行为异常。
  3. 对象序列化与反序列化:如果依赖默认的hashCode()实现,序列化后的对象在不同JVM上反序列化时可能会产生不同的哈希值。
常见误区或注意事项
  1. 默认hashCode()的不一致性:默认的hashCode()实现可能依赖于对象的内存地址或JVM内部的某种算法,不同JVM的实现可能不同。例如:
    • HotSpot JVM 默认使用一种基于对象内存地址的算法。
    • OpenJ9 可能使用其他算法。
    • 某些JVM可能会在对象移动时(如GC后)改变哈希值。
  2. 依赖默认hashCode()的风险:如果未重写hashCode(),程序的行为可能因JVM不同而发生变化,尤其是在分布式系统或持久化场景中。
  3. 哈希碰撞:不同JVM的哈希算法可能导致哈希碰撞的概率不同,从而影响性能。
示例代码

以下是一个重写hashCode()方法的示例,确保在不同JVM上行为一致:

public class Person {
    private String name;
    private int age;

    @Override
    public int hashCode() {
        // 使用Objects.hash()方法生成一致的哈希值
        return Objects.hash(name, age);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }
}
解决跨JVM一致性的建议
  1. 始终重写hashCode():避免依赖默认实现,而是基于对象的逻辑状态计算哈希值。
  2. 使用稳定的哈希算法:如Objects.hash()或Apache Commons的HashCodeBuilder
  3. 测试多JVM兼容性:在部署前,验证应用在不同JVM上的行为是否一致。

通过以上措施,可以确保hashCode()方法在不同JVM实现中表现一致,避免潜在的问题。


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值