深入理解 Java 中的 toString() 方法与 String 不可变性原理(含字符串编码详解)
在 Java 学习过程中,toString() 方法与 String 类是最基础却最容易被忽视的核心知识点之一。很多开发者在初学阶段只知道“能打印对象”“字符串不能修改”,却不了解背后的设计思想与运行机制。
本文将系统讲清楚:
toString()方法的设计意义- 为什么要重写
toString() - 什么是 String 不可变性
- String 为什么必须设计成不可变
- StringBuilder 与 StringBuffer 的区别
- String 的 hashCode 缓存机制
- 字符串拼接性能问题
- Java 字符串底层编码机制(重点补充)
适合作为 Java 基础阶段的核心知识体系文章。
一、什么是 toString() 方法?
toString() 是 Java 中 Object 类提供的一个核心方法:
public String toString()
由于 Java 中所有类都默认继承 Object 类,因此所有类天然拥有 toString() 方法。
它的作用是:
返回对象的字符串表示形式,用于打印对象信息或调试程序。
二、默认 toString() 的实现机制
Object 类中默认实现如下:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
返回格式:
类名@哈希值(十六进制)
例如:
Student@5a07e868
这个字符串对开发者来说几乎没有任何可读价值,因此实际开发中通常需要重写该方法。
三、为什么要重写 toString() 方法?
默认输出:
Student@5a07e868
重写后输出:
Student{name='张三', age=18}
代码示例:
class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}
}
测试:
public class ToStringDemo {
public static void main(String[] args) {
Student stu = new Student("张三", 18);
System.out.println(stu);
}
}
输出:
Student{name='张三', age=18}
注意:
System.out.println(stu);
实际上等价于:
System.out.println(stu.toString());
四、什么是 String 不可变性?
String 不可变性指:
一旦 String 对象创建完成,它的内容永远不能被修改。
例如:
String a = "abc";
a = a + "d";
看似修改:
abc → abcd
实际上:
创建新对象 "abcd"
变量 a 指向新对象
原对象 "abc" 仍然存在
五、String 为什么是不可变的?
这是 Java 最经典的面试问题之一。
主要原因有三个:
原因一:底层数组被 final 修饰
JDK 8:
private final char[] value;
JDK 9 以后优化为:
private final byte[] value;
特点:
- final 修饰引用地址不可改变
- private 外部无法访问
- 无修改数组元素方法
因此:
字符内容不可改变
原因二:String 类被 final 修饰
源码:
public final class String
作用:
禁止继承
防止子类破坏不可变性设计
例如:
如果允许继承:
class MyString extends String
就可能重写内部方法破坏安全机制。
原因三:没有提供修改字符的方法
例如:
replace()
substring()
concat()
这些方法本质都是:
创建新对象
而不是修改原对象
示例:
String str = "abc";
str.replace("a","b");
实际发生:
创建新对象 "bbc"
六、为什么 String 必须设计为不可变?
这是 JVM 级别的重要设计思想。
原因如下:
原因一:线程安全(无需加锁)
例如:
String str = "hello";
多个线程访问:
线程A读取
线程B读取
线程C读取
因为不可变:
永远不会被修改
因此:
天然线程安全
无需同步锁
性能极高
原因二:支持字符串常量池优化(节省内存)
示例:
String a = "abc";
String b = "abc";
内存结构:
字符串常量池:
"abc"
↑ ↑
a b
两个变量共享同一个对象
如果 String 可变:
a 修改
b 跟着变化
将导致严重逻辑错误
因此必须不可变
原因三:支持 HashMap 高效运行
例如:
HashMap<String,String>
String 作为 key:
依赖:
hashCode()
equals()
如果 String 可变:
key 变化
hashCode 变化
会导致:
无法找到原来的数据
严重错误
因此:
String 必须不可变
七、String 的 hashCode 缓存机制(懒加载)
源码:
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
缓存结果
后续调用:
直接返回缓存值
优势:
避免重复计算
提高性能
特别适用于:
HashMap
HashSet
高频访问场景
八、String 的缺点:修改即创建新对象
示例:
String str = "a";
str += "b";
str += "c";
str += "d";
内存变化:
a
ab
abc
abcd
创建多个对象:
性能下降
垃圾对象增多
九、循环拼接字符串的性能灾难
错误写法:
String str = "";
for(int i=0;i<10000;i++){
str += i;
}
原因:
每次循环:
创建新对象
时间复杂度:
O(n²)
正确写法:
使用:
StringBuilder
示例:
StringBuilder builder = new StringBuilder();
for(int i=0;i<10000;i++){
builder.append(i);
}
String result = builder.toString();
时间复杂度:
O(n)
性能提升巨大
十、StringBuilder 与 StringBuffer 的区别
| 类 | 线程安全 | 性能 | 适用场景 |
|---|---|---|---|
| String | 安全 | 低 | 少量字符串操作 |
| StringBuilder | 不安全 | 高 | 单线程推荐 |
| StringBuffer | 安全 | 中 | 多线程环境 |
原因:
StringBuffer 方法:
synchronized 修饰
线程安全但性能较低
StringBuilder:
无线程锁
性能更高
十一、Java 字符串底层编码机制(重点补充)
理解字符串必须理解编码机制。
否则容易出现:
乱码问题
跨平台问题
接口传输问题
数据库存储问题
1、什么是字符编码?
字符编码本质是:
字符 → 数字 → 二进制
示例:
字符:
A
ASCII 编码:
65
二进制:
01000001
2、常见编码格式
Java 常见编码:
| 编码 | 说明 |
|---|---|
| ASCII | 英文编码 |
| ISO-8859-1 | 西欧编码 |
| GBK | 中文编码(简体) |
| Unicode | 全球统一编码 |
| UTF-8 | Unicode 可变长度实现 |
现代 Java 默认:
UTF-16(内部)
UTF-8(外部常用)
3、Java String 内部编码机制
JDK8:
char[]
本质:
UTF-16 编码
每个字符:
2字节
示例:
String str = "A中";
内存:
A → 2字节
中 → 2字节
共:
4字节
4、JDK9 之后的优化(Compact String)
JDK9 引入:
Compact String
结构:
byte[] value
新增字段:
coder
作用:
判断编码方式:
LATIN1
UTF16
示例:
字符串:
abc
使用:
LATIN1
占用:
1字节 × 3
字符串:
中文
使用:
UTF16
占用:
2字节 × 2
优势:
节省 50% 内存
十二、字符串乱码问题产生原因(高频面试题)
乱码本质:
编码方式 ≠ 解码方式
示例:
编码:
UTF-8
解码:
GBK
结果:
乱码
解决方案:
统一编码:
推荐:
UTF-8
例如:
new String(bytes,"UTF-8");
十三、总结
本文核心知识总结如下:
toString()
作用:
返回对象字符串描述信息
建议:
所有实体类都应该重写
String 不可变原因
三个核心机制:
final类
final数组引用
无修改方法
不可变设计优势
三个核心优势:
线程安全
支持常量池
支持Hash缓存
String 修改建议方案
推荐:
StringBuilder
多线程环境:
StringBuffer
编码机制重点理解
必须掌握:
UTF-8
UTF-16
ASCII
Unicode
乱码原理
Compact String
掌握这些内容,你对 Java 字符串底层机制的理解已经达到 Java 中级开发工程师水平。