深入理解 Java 中的 toString() 方法与 String 不可变性原理(含字符串编码详解)

2 阅读6分钟

深入理解 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-8Unicode 可变长度实现

现代 Java 默认:

UTF-16(内部)
UTF-8(外部常用)

3、Java String 内部编码机制

JDK8:

char[]

本质:

UTF-16 编码

每个字符:

2字节

示例:

String str = "A中";

内存:

A2字节
中 → 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 不可变原因

三个核心机制:

finalfinal数组引用
无修改方法

不可变设计优势

三个核心优势:

线程安全
支持常量池
支持Hash缓存

String 修改建议方案

推荐:

StringBuilder

多线程环境:

StringBuffer

编码机制重点理解

必须掌握:

UTF-8
UTF-16
ASCII
Unicode
乱码原理
Compact String

掌握这些内容,你对 Java 字符串底层机制的理解已经达到 Java 中级开发工程师水平。