《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. 核心区别总结
特性 | String | StringBuffer | StringBuilder |
---|---|---|---|
不可变性 | ✅ 不可变(每次操作生成新对象) | ❌ 可变(直接在原对象修改) | ❌ 可变(直接在原对象修改) |
线程安全 | ✅(天然不可变,线程安全) | ✅(方法用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
StringBuffer
的append()
方法源码如下:
public synchronized StringBuffer append(String str) {
super.append(str); // 调用父类AbstractStringBuilder的append方法
return this;
}
-
synchronized
关键字:确保同一时间只有一个线程能进入该方法。 -
执行流程:
-
线程t1获取锁 → 执行
append("a")
→ 释放锁。 -
线程t2获取锁 → 执行
append("a")
→ 释放锁。
- 交替执行,无并发冲突,所有操作均被正确执行。
-
3. 对比StringBuilder
的非线程安全
StringBuilder
的append()
方法未加锁:
public StringBuilder append(String str) {
super.append(str); // 直接操作,无同步
return this;
}
-
问题场景:
假设value
是内部存储字符的数组,count
是当前长度。- 线程t1读取
count=5
,准备写入value[5]
。 - 线程t2同时读取
count=5
,写入value[5]
。 - 最终两个线程都认为
count=6
,但实际只写入一次 → 数据丢失。
- 线程t1读取
-
结果:最终长度可能小于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()
- 默认行为:同
==
(比较地址)。 - 重写后:按业务逻辑比较内容(如
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()
不同,它们会被分配到不同的桶中,导致哈希集合无法正确去重或检索。
示例:
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()
是精确匹配的“钥匙”。 - 二者必须协同工作,才能保证哈希集合(如
HashMap
、HashSet
)的正确性和性能。
违反后果:
- 数据重复:哈希集合无法去重。
- 数据丢失:无法通过
contains()
等方法正确检索对象。 - 性能下降:哈希冲突增加,桶内链表或红黑树过长。
5. 额外注意事项
- 不可变对象:
如果对象的哈希码计算依赖可变属性,当其属性改变后,哈希码会变化,可能导致哈希集合中无法找到该对象。
解决方案:设计不可变对象,或将关键属性声明为final
。 - 性能优化:
哈希码应尽量均匀分布,减少冲突。例如,避免所有对象返回相同的哈希码。
通过遵循哈希契约,可以确保对象在哈希集合中的行为符合预期,同时提升程序的可维护性和性能。