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

[复制链接]
发表于 2025-10-12 01:13:23 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

一.String、StringBuffer、StringBuilder的区别?

1. 核心区别总结

特性StringStringBufferStringBuilder不可变性✅ 不可变(每次操纵天生新对象)❌ 可变(直接在原对象修改)❌ 可变(直接在原对象修改)线程安全✅(天然不可变,线程安全)✅(方法用synchronized修饰)❌(线程不安全,性能更高)性能低(频仍修改时)中(线程安全但同步开销)高(无同步开销)实用场景少量修改或作为常量使用多线程情况下的字符串操纵单线程情况下的高频字符串操纵
2. 具体实例演示

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

  1. public class StringVsStringBuffer {
  2.     public static void main(String[] args) {
  3.         // String的不可变性示例
  4.         String str = "Hello";
  5.         System.out.println("修改前String哈希码:" + System.identityHashCode(str)); // 输出初始地址
  6.         str += " World";
  7.         System.out.println("修改后String哈希码:" + System.identityHashCode(str)); // 地址变化
  8.         System.out.println("String结果:" + str); // Hello World
  9.         // StringBuffer的可变性示例
  10.         StringBuffer sb = new StringBuffer("Hello");
  11.         System.out.println("\n修改前StringBuffer哈希码:" + System.identityHashCode(sb)); // 初始地址
  12.         sb.append(" World");
  13.         System.out.println("修改后StringBuffer哈希码:" + System.identityHashCode(sb)); // 地址不变
  14.         System.out.println("StringBuffer结果:" + sb); // Hello World
  15.     }
  16. }
复制代码
  1. 修改前String哈希码:1956725890
  2. 修改后String哈希码:356573597
  3. String结果:Hello World
  4. 修改前StringBuffer哈希码:1735600054
  5. 修改后StringBuffer哈希码:1735600054
  6. StringBuffer结果:Hello World
  7. 哈希码不是内存地址,可以用来确定对象的存储位置。.
复制代码
示例2:线程安全验证

  1.     public class ThreadSafetyTest {
  2.         public static void main(String[] args) throws InterruptedException {
  3.             // StringBuffer线程安全验证
  4.             StringBuffer buffer = new StringBuffer();
  5.             Runnable bufferTask = () -> {
  6.                 for (int i = 0; i < 1000; i++) {
  7.                     buffer.append("a");
  8.                 }
  9.             };
  10.             Thread t1 = new Thread(bufferTask);
  11.             Thread t2 = new Thread(bufferTask);
  12.             t1.start();
  13.             t2.start();
  14.             t1.join();
  15.             t2.join();
  16.             System.out.println("StringBuffer长度:" + buffer.length()); // 应为2000
  17.             // StringBuilder线程不安全验证
  18.             StringBuilder builder = new StringBuilder();
  19.             Runnable builderTask = () -> {
  20.                 for (int i = 0; i < 1000; i++) {
  21.                     builder.append("a");
  22.                 }
  23.             };
  24.             Thread t3 = new Thread(builderTask);
  25.             Thread t4 = new Thread(builderTask);
  26.             t3.start();
  27.             t4.start();
  28.             t3.join();
  29.             t4.join();
  30.             System.out.println("StringBuilder长度:" + builder.length()); // 可能小于2000
  31.         }
  32.     }
复制代码


  • 观察控制台输出:
    ◦ 哈希码变革 → 证明是否天生新对象。
    ◦ 线程安全结果 → StringBuffer包管数据完备,StringBuilder大概丢失数据。
    ◦ 耗时对比 → 直观表现性能差别。

2. 线程安全的关键:synchronized

StringBuffer的append()方法源码如下:
  1. public synchronized StringBuffer append(String str) {
  2.     super.append(str);  // 调用父类AbstractStringBuilder的append方法
  3.     return this;
  4. }
复制代码


  • synchronized关键字:确保同一时间只有一个线程能进入该方法。
  • 实行流程:

    • 线程t1获取锁 → 实行append("a") → 开释锁。
    • 线程t2获取锁 → 实行append("a") → 开释锁。
       

    • 瓜代实行,无并发辩论,全部操纵均被精确实行。


3. 对比StringBuilder的非线程安全

StringBuilder的append()方法未加锁:
  1. public StringBuilder append(String str) {
  2.     super.append(str);  // 直接操作,无同步
  3.     return this;
  4. }
复制代码


  • 标题场景:
    假设value是内部存储字符的数组,count是当前长度。

    • 线程t1读取count=5,准备写入value[5]。
    • 线程t2同时读取count=5,写入value[5]。
    • 终极两个线程都以为count=6,但实际只写入一次 → 数据丢失。

  • 结果:终极长度大概小于2000(如示例中的1983)。

4. 可视化实行流程

操纵步调StringBuffer(线程安全)StringBuilder(线程不安全)线程t1操纵获取锁 → 追加字符 → 开释锁直接追加字符(无锁)线程t2操纵等候锁 → 获取锁 → 追加字符 → 开释锁直接追加字符(大概与t1同时操纵同一位置)结果2000次追加全部见效大概因并发写入辩论导致部分操纵丢失
5. 进一步验证

可以打印每次追加后的长度,观察是否稳固增长:
java
复制
  1. Runnable bufferTask = () -> {
  2.     for (int i = 0; i < 1000; i++) {
  3.         buffer.append("a");
  4.         System.out.println(Thread.currentThread().getName() + ": " + buffer.length());
  5.     }
  6. };
复制代码


  • StringBuffer输出:长度渐渐递增,无重复值。
  • StringBuilder输出:大概出现多个线程打印雷同长度(并发辩论)。

6. 总结



  • StringBuffer的线程安全:通过synchronized包管操纵的原子性,克制并发辩论。
  • StringBuilder的性能上风:捐躯线程安全变更更高性能,实用于单线程场景。
  • 选择依据:

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

==和equals()的区别?加实际例子,markdown输出
二.== 与 equals() 的区别及示例

1. 核心区别

比力项==equals()比力对象根本范例比力值,引用范例比力内存所在引用范例比力内容(需重写方法)默认活动直接比力内存所在默认比力内存所在(同==)可定制性不可修改可通过重写方法自界说比力逻辑2. 具体示例分析

示例1:根本数据范例(int)

  1. int a = 5;
  2. int b = 5;
  3. System.out.println(a == b);  // true(值相同)
复制代码
示例2:字符串对象

  1. String s1 = new String("hello");
  2. String s2 = new String("hello");
  3. String s3 = "hello";
  4. String s4 = "hello";
  5. // == 比较内存地址
  6. System.out.println(s1 == s2);  // false(两个堆对象地址不同)
  7. System.out.println(s3 == s4);  // true(指向常量池同一地址)
  8. // equals() 比较内容
  9. System.out.println(s1.equals(s2));  // true(内容相同)
  10. System.out.println(s3.equals(s4));  // true(内容相同)
复制代码
注意:String中的equals方法是被重写过的,因此String对象equals方法比力的是对象的值。
示例3:自界说对象

  1. class Person {
  2.     String name;
  3.     public Person(String name) { this.name = name; }
  4. }
  5. Person p1 = new Person("Alice");
  6. Person p2 = new Person("Alice");
  7. Person p3 = p1;
  8. // == 比较内存地址
  9. System.out.println(p1 == p2);  // false(不同对象地址)
  10. System.out.println(p1 == p3);  // true(指向同一对象)
  11. // equals() 默认比较地址(未重写时等同于 ==)
  12. System.out.println(p1.equals(p2));  // false(未重写equals方法)
复制代码

示例4:重写equals()

  1. class Student {
  2.     String id;
  3.     public Student(String id) { this.id = id; }
  4.     @Override
  5.     public boolean equals(Object o) {
  6.         if (this == o) return true;
  7.         if (o == null || getClass() != o.getClass()) return false;
  8.         Student student = (Student) o;
  9.         return Objects.equals(id, student.id); // 按id比较内容
  10.     }
  11. }
  12. Student stu1 = new Student("1001");
  13. Student stu2 = new Student("1001");
  14. System.out.println(stu1 == stu2);      // false(地址不同)
  15. System.out.println(stu1.equals(stu2)); // true(id相同)
复制代码

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

  1. Integer i1 = 127;
  2. Integer i2 = 127;
  3. System.out.println(i1 == i2);       // true(缓存范围内,地址相同)
  4. Integer i3 = 128;
  5. Integer i4 = 128;
  6. System.out.println(i3 == i4);       // false(超出缓存范围,地址不同)
  7. System.out.println(i3.equals(i4));  // true(内容相同)
复制代码

4. 总结



  • ==

    • 根本范例:直接比力值。
    • 引用范例:比力内存所在。

  • equals()

    • 默认活动:同==(比力所在)。
    • 重写后:按业务逻辑比力内容(如String、Integer等已重写)。

  • 关键原则

    • 若需内容比力,必须重写equals()。
    • 重写equals()时,必须同时重写hashCode()(遵照哈希左券规则)。

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

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



  • 哈希左券(Hash Contract):
    若两个对象通过equals()判断为相称,则它们的hashCode()必须返回雷同的值。
    反之,若两个对象的hashCode()雷同,equals()不肯定返回true(哈希辩论是允许的)。
  • 默认活动:

    • Object类中的equals()默认比力内存所在(==)。
    • hashCode()默认返回对象的内存所在的哈希值。


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

关键缘故起因:确保对象在哈希聚集中精确工作。
哈希聚集(如HashMap、HashSet)依靠以下逻辑:

  • 存储时:

    • 先通过hashCode()盘算桶的位置。
    • 再通过equals()查抄桶内是否存在雷同对象。

  • 查找时:

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

标题场景:
若两个对象equals()为true但hashCode()差别,它们会被分配到差别的桶中,导致哈希聚集无法精确去重或检索。
示例:
  1. class Person {
  2.     String name;
  3.     int age;
  4.     @Override
  5.     public boolean equals(Object o) {
  6.         if (this == o) return true;
  7.         if (o == null || getClass() != o.getClass()) return false;
  8.         Person person = (Person) o;
  9.         return age == person.age && Objects.equals(name, person.name);
  10.     }
  11.     // 未重写hashCode()!
  12. }
  13. public static void main(String[] args) {
  14.     Person p1 = new Person("Alice", 30);
  15.     Person p2 = new Person("Alice", 30);
  16.     System.out.println(p1.equals(p2)); // true(内容相同)
  17.     System.out.println(p1.hashCode() == p2.hashCode()); // false(哈希码不同)
  18.     Set<Person> set = new HashSet<>();
  19.     set.add(p1);
  20.     set.add(p2);
  21.     System.out.println(set.size()); // 输出2(预期应为1,违反唯一性)
  22. }
复制代码

3. 怎样精确重写hashCode()?



  • 原则:
    使用与equals()中比力的属性雷同的属性组合天生哈希码。
  • 保举方法:
    使用Objects.hash()工具类,传入全部加入equals()比力的属性。
示例:
  1. @Override
  2. public int hashCode() {
  3.     return Objects.hash(name, age); // 基于name和age生成哈希码
  4. }
复制代码

4. 总结

操纵须要性重写equals()界说对象内容相称的逻辑。重写hashCode()确保哈希聚集精确工作,服从哈希左券(相称对象必有雷同哈希码)。关键点:


  • 哈希码是对象在哈希表中的“所在”,equals()是正确匹配的“钥匙”。
  • 二者必须协同工作,才华包管哈希聚集(如HashMap、HashSet)的精确性和性能。
违反结果:


  • 数据重复:哈希聚集无法去重。
  • 数据丢失:无法通过contains()等方法精确检索对象。
  • 性能降落:哈希辩论增长,桶内链表或红黑树过长。

5. 额外注意事项



  • 不可变对象:
    如果对象的哈希码盘算依靠可变属性,当其属性改变后,哈希码会变革,大概导致哈希聚集中无法找到该对象。
    办理方案:计划不可变对象,或将关键属性声明为final。
  • 性能优化:
    哈希码应只管匀称分布,淘汰辩论。比方,克制全部对象返回雷同的哈希码。
通过遵照哈希左券,可以确保对象在哈希聚集中的活动符合预期,同时提拔步伐的可维护性和性能。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表