当前位置: 首页 > news >正文

《Java核心三问:字符串、equals()、hashCode()的隐藏雷区与完美避坑手册》

目录

  • 一.String、StringBuffer、StringBuilder的区别?
    • 1. 核心区别总结
    • 2. 具体实例演示
      • 示例1:不可变性 vs 可变性
      • 示例2:线程安全验证
    • 2. 线程安全的关键:`synchronized`
    • 3. 对比`StringBuilder`的非线程安全
    • 4. 可视化执行流程
    • 5. 进一步验证
    • 6. 总结
  • 二.`==` 与 `equals()` 的区别及示例
    • 1. 核心区别
    • 2. 具体示例分析
      • 示例1:基本数据类型(`int`)
      • 示例2:字符串对象
      • 示例3:自定义对象
      • 示例4:重写`equals()`
    • 3. 特殊场景:自动装箱陷阱
    • 4. 总结
  • 三、hashCode()与 equals() 的关系及重写原则
    • 1. `hashCode()`与`equals()`的核心关系(前者是后者的必要不充分条件)
    • 2. 为什么重写`equals()`必须重写`hashCode()`?
    • 3. 如何正确重写`hashCode()`?
    • 4. 总结
    • 5. 额外注意事项


一.String、StringBuffer、StringBuilder的区别?

1. 核心区别总结

特性StringStringBufferStringBuilder
不可变性✅ 不可变(每次操作生成新对象)❌ 可变(直接在原对象修改)❌ 可变(直接在原对象修改)
线程安全✅(天然不可变,线程安全)✅(方法用synchronized修饰)❌(线程不安全,性能更高)
性能低(频繁修改时)中(线程安全但同步开销)高(无同步开销)
适用场景少量修改或作为常量使用多线程环境下的字符串操作单线程环境下的高频字符串操作

2. 具体实例演示

示例1:不可变性 vs 可变性

public class StringVsStringBuffer {
    public static void main(String[] args) {
        // String的不可变性示例
        String str = "Hello";
        System.out.println("修改前String哈希码:" + System.identityHashCode(str)); // 输出初始地址
        str += " World";
        System.out.println("修改后String哈希码:" + System.identityHashCode(str)); // 地址变化
        System.out.println("String结果:" + str); // Hello World

        // StringBuffer的可变性示例
        StringBuffer sb = new StringBuffer("Hello");
        System.out.println("\n修改前StringBuffer哈希码:" + System.identityHashCode(sb)); // 初始地址
        sb.append(" World");
        System.out.println("修改后StringBuffer哈希码:" + System.identityHashCode(sb)); // 地址不变
        System.out.println("StringBuffer结果:" + sb); // Hello World
    }
}
修改前String哈希码:1956725890
修改后String哈希码:356573597
String结果:Hello World

修改前StringBuffer哈希码:1735600054
修改后StringBuffer哈希码:1735600054
StringBuffer结果:Hello World
哈希码不是内存地址,可以用来确定对象的存储位置。.

示例2:线程安全验证


    public class ThreadSafetyTest {
        public static void main(String[] args) throws InterruptedException {
            // StringBuffer线程安全验证
            StringBuffer buffer = new StringBuffer();
            Runnable bufferTask = () -> {
                for (int i = 0; i < 1000; i++) {
                    buffer.append("a");
                }
            };
            Thread t1 = new Thread(bufferTask);
            Thread t2 = new Thread(bufferTask);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("StringBuffer长度:" + buffer.length()); // 应为2000

            // StringBuilder线程不安全验证
            StringBuilder builder = new StringBuilder();
            Runnable builderTask = () -> {
                for (int i = 0; i < 1000; i++) {
                    builder.append("a");
                }
            };
            Thread t3 = new Thread(builderTask);
            Thread t4 = new Thread(builderTask);
            t3.start();
            t4.start();
            t3.join();
            t4.join();
            System.out.println("StringBuilder长度:" + builder.length()); // 可能小于2000
        }
    }
  • 观察控制台输出
    ◦ 哈希码变化 → 证明是否生成新对象。
    ◦ 线程安全结果 → StringBuffer保证数据完整,StringBuilder可能丢失数据。
    ◦ 耗时对比 → 直观体现性能差异。

2. 线程安全的关键:synchronized

StringBufferappend()方法源码如下:

public synchronized StringBuffer append(String str) {
    super.append(str);  // 调用父类AbstractStringBuilder的append方法
    return this;
}
  • synchronized关键字:确保同一时间只有一个线程能进入该方法。

  • 执行流程

    1. 线程t1获取锁 → 执行append("a") → 释放锁。

    2. 线程t2获取锁 → 执行append("a") → 释放锁。

    • 交替执行,无并发冲突,所有操作均被正确执行。

3. 对比StringBuilder的非线程安全

StringBuilderappend()方法未加锁

public StringBuilder append(String str) {
    super.append(str);  // 直接操作,无同步
    return this;
}
  • 问题场景
    假设value是内部存储字符的数组,count是当前长度。

    1. 线程t1读取count=5,准备写入value[5]
    2. 线程t2同时读取count=5,写入value[5]
    3. 最终两个线程都认为count=6,但实际只写入一次 → 数据丢失
  • 结果:最终长度可能小于2000(如示例中的1983)。


4. 可视化执行流程

操作步骤StringBuffer(线程安全)StringBuilder(线程不安全)
线程t1操作获取锁 → 追加字符 → 释放锁直接追加字符(无锁)
线程t2操作等待锁 → 获取锁 → 追加字符 → 释放锁直接追加字符(可能与t1同时操作同一位置)
结果2000次追加全部生效可能因并发写入冲突导致部分操作丢失

5. 进一步验证

可以打印每次追加后的长度,观察是否稳定增长:

java

复制

Runnable bufferTask = () -> {
    for (int i = 0; i < 1000; i++) {
        buffer.append("a");
        System.out.println(Thread.currentThread().getName() + ": " + buffer.length());
    }
};
  • StringBuffer输出:长度逐步递增,无重复值。
  • StringBuilder输出:可能出现多个线程打印相同长度(并发冲突)。

6. 总结

  • StringBuffer的线程安全:通过synchronized保证操作的原子性,避免并发冲突。

  • StringBuilder的性能优势:牺牲线程安全换取更高性能,适用于单线程场景。

  • 选择依据

    • 多线程共享资源 → StringBuffer
    • 单线程高频操作 → StringBuilder

==和equals()的区别?加实际例子,markdown输出

二.==equals() 的区别及示例

1. 核心区别

比较项==equals()
比较对象基本类型比较,引用类型比较内存地址引用类型比较内容(需重写方法)
默认行为直接比较内存地址默认比较内存地址(同==
可定制性不可修改可通过重写方法自定义比较逻辑

2. 具体示例分析

示例1:基本数据类型(int

int a = 5;
int b = 5;
System.out.println(a == b);  // true(值相同)

示例2:字符串对象

String s1 = new String("hello");
String s2 = new String("hello");
String s3 = "hello";
String s4 = "hello";

// == 比较内存地址
System.out.println(s1 == s2);  // false(两个堆对象地址不同)
System.out.println(s3 == s4);  // true(指向常量池同一地址)

// equals() 比较内容
System.out.println(s1.equals(s2));  // true(内容相同)
System.out.println(s3.equals(s4));  // true(内容相同)

注意:String中的equals方法是被重写过的,因此String对象equals方法比较的是对象的值。

示例3:自定义对象

class Person {
    String name;
    public Person(String name) { this.name = name; }
}

Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
Person p3 = p1;

// == 比较内存地址
System.out.println(p1 == p2);  // false(不同对象地址)
System.out.println(p1 == p3);  // true(指向同一对象)

// equals() 默认比较地址(未重写时等同于 ==)
System.out.println(p1.equals(p2));  // false(未重写equals方法)

示例4:重写equals()

class Student {
    String id;
    public Student(String id) { this.id = id; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(id, student.id); // 按id比较内容
    }
}

Student stu1 = new Student("1001");
Student stu2 = new Student("1001");
System.out.println(stu1 == stu2);      // false(地址不同)
System.out.println(stu1.equals(stu2)); // true(id相同)

3. 特殊场景:自动装箱陷阱

Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2);       // true(缓存范围内,地址相同)

Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);       // false(超出缓存范围,地址不同)

System.out.println(i3.equals(i4));  // true(内容相同)

4. 总结

  • ==

    • 基本类型:直接比较值。
    • 引用类型:比较内存地址。
  • equals()

    • 默认行为:同==(比较地址)。
    • 重写后:按业务逻辑比较内容(如StringInteger等已重写)。
  • 关键原则

    • 若需内容比较,必须重写equals()
    • 重写equals()时,必须同时重写hashCode()(遵循哈希契约规则)。

三、hashCode()与 equals() 的关系及重写原则

1. hashCode()equals()的核心关系(前者是后者的必要不充分条件)

  • 哈希契约(Hash Contract)
    若两个对象通过equals()判断为相等,则它们的hashCode()必须返回相同的值
    反之,若两个对象的hashCode()相同,equals()不一定返回true(哈希冲突是允许的)。

  • 默认行为

    • Object类中的equals()默认比较内存地址(==)。
    • hashCode()默认返回对象的内存地址的哈希值。

2. 为什么重写equals()必须重写hashCode()

关键原因:确保对象在哈希集合中正确工作。
哈希集合(如HashMapHashSet)依赖以下逻辑:

  1. 存储时

    • 先通过hashCode()计算桶的位置。
    • 再通过equals()检查桶内是否存在相同对象。
  2. 查找时

    • 同样先根据hashCode()定位桶,再用equals()精确匹配。

问题场景
若两个对象equals()truehashCode()不同,它们会被分配到不同的桶中,导致哈希集合无法正确去重或检索。
示例

class Person {
    String name;
    int 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);
    }

    // 未重写hashCode()!
}

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()); // false(哈希码不同)

    Set<Person> set = new HashSet<>();
    set.add(p1);
    set.add(p2);
    System.out.println(set.size()); // 输出2(预期应为1,违反唯一性)
}

3. 如何正确重写hashCode()

  • 原则
    使用与equals()中比较的属性相同的属性组合生成哈希码。
  • 推荐方法
    使用Objects.hash()工具类,传入所有参与equals()比较的属性。

示例

@Override
public int hashCode() {
    return Objects.hash(name, age); // 基于name和age生成哈希码
}

4. 总结

操作必要性
重写equals()定义对象内容相等的逻辑。
重写hashCode()确保哈希集合正确工作,遵守哈希契约(相等对象必有相同哈希码)。

关键点

  • 哈希码是对象在哈希表中的“地址”,equals()是精确匹配的“钥匙”。
  • 二者必须协同工作,才能保证哈希集合(如HashMapHashSet)的正确性和性能。

违反后果

  • 数据重复:哈希集合无法去重。
  • 数据丢失:无法通过contains()等方法正确检索对象。
  • 性能下降:哈希冲突增加,桶内链表或红黑树过长。

5. 额外注意事项

  • 不可变对象
    如果对象的哈希码计算依赖可变属性,当其属性改变后,哈希码会变化,可能导致哈希集合中无法找到该对象。
    解决方案:设计不可变对象,或将关键属性声明为final
  • 性能优化
    哈希码应尽量均匀分布,减少冲突。例如,避免所有对象返回相同的哈希码。

通过遵循哈希契约,可以确保对象在哈希集合中的行为符合预期,同时提升程序的可维护性和性能。

相关文章:

  • UltraSearch一键直达文件,高效搜索新体验
  • 双指针算法-day14(分组循环)
  • java数据结构之双端对列
  • 力扣刷题——25.K个一组翻转链表
  • 【全国产化主板】解决方案探讨:CPU、FPGA、GPU、AI的融合与优化
  • 【最后203篇系列】020 rocksdb agent
  • 《视觉SLAM十四讲》ch13 设计SLAM系统 相机轨迹实现
  • Neo4j GDS-04-图的中心性分析介绍
  • 力扣977. 有序数组的平方(双指针技巧)
  • 【STM32】I²CC通信外设硬件I²CC读写MPU6050(学习笔记)
  • kubernetes高级实战
  • 6.3考研408数据结构中BFS与DFS的易错点及难点解析
  • 9、Python collections模块高效数据结构
  • 前端面试常考基础题目详解
  • 3月20号
  • 通过调整相邻分区实现Linux根分区扩容(ext4文件系统)
  • vue里localStorage可以直接用吗
  • Spring Boot 集成 Kafka 消息发送方案
  • idea配置gitee
  • QT 实现信号源实时采集功能支持频谱图,瀑布图显示
  • 泉州市建设工程交易网站/链接提交入口
  • 4399在线观看免费高清1080/seo优化销售话术
  • seo高手培训/seo快速入门教程
  • 找人做网站需要什么/中央突然宣布一个大消息
  • 网站建设仟首先金手指13/网站定制
  • PR做视频需要放网站上/长沙有实力seo优化