StringBuilder 与可变字符串 | JavaSE

StringBuilder 与可变字符串

一、学习目标

完成本章后,你应该能够:

  1. 解释 String 与 StringBuilder 在“可变性”方面的核心区别。
  2. 能够创建 StringBuilder 对象,并使用 append() 完成字符串拼接。
  3. 熟练使用 reverse()length()toString() 等基础 API。
  4. 理解为什么大量、重复的字符串修改通常适合使用 StringBuilder。
  5. 理解 StringBuilder 方法链调用的原理。
  6. 能够根据场景判断应该使用 String 还是 StringBuilder。
  7. 了解 StringBuilder 的容量机制以及它与 StringBuffer 在线程安全方面的基本区别。

二、核心知识

2.1 为什么已经有 String,还需要 StringBuilder

上一章我们不断强调:

String 是不可变字符串。

例如:

String s = "Java";

s = s + "SE";

最终:

JavaSE

看起来好像:

原来的 Java 被修改成 JavaSE

实际上不能这样理解。

String 本身不可变。

程序得到的是新的字符串结果,再让:

s

引用这个新的结果。

如果只是偶尔拼接几次,这完全没有问题。

例如:

String name = "星雨";
String message = "你好," + name;

这种代码:

  • 简单
  • 可读性高
  • 没有必要为了“性能”机械改成 StringBuilder

真正值得注意的是:

在循环中进行大量、重复、逐步累积的字符串修改。

例如:

String text = "";

for (int i = 0; i < 10000; i++) {
    text = text + i;
}

这种代码不断产生新的字符串结果。

此时更适合使用:

StringBuilder

2.2 StringBuilder 是什么

StringBuilder 可以理解为:

一个可变的字符序列容器。

例如:

StringBuilder builder = new StringBuilder();

builder.append("Java");
builder.append("SE");

第一次:

builder
   ↓
"Java"

第二次:

builder
   ↓
"JavaSE"

核心区别是:

String
不可变字符串

StringBuilder
可变字符序列

2.3 一个非常形象的理解

String 更像:

打印好的纸条

纸条上写:

Java

想变成:

JavaSE

不是直接把原来的纸条变长,而是得到新的结果。

而 StringBuilder 更像:

白板

一开始写:

Java

然后继续往后写:

SE

白板本身仍然是那个白板,但里面保存的内容可以变化。

所以:

String        → 内容稳定
StringBuilder → 内容需要持续修改

2.4 StringBuilder 常用 API

本阶段重点掌握:

| API | 作用 | | --------------------------- | -------------------- | | new StringBuilder() | 创建空 StringBuilder | | new StringBuilder(String) | 根据已有字符串创建 | | append(...) | 在尾部追加数据 | | reverse() | 反转字符序列 | | length() | 获取当前字符数量 | | toString() | 转换为 String |

此外 StringBuilder 还提供:

insert()
delete()

等真正意义上的可变操作。

但本章核心仍然是:

append
reverse
length
toString

三、使用方法

3.1 创建 StringBuilder

方式一:

StringBuilder builder = new StringBuilder();

创建一个空的 StringBuilder。

当前内容可以理解为:

""

方式二:

StringBuilder builder =
        new StringBuilder("Java");

当前内容:

Java

3.2 append():追加内容

最核心的方法:

append()

例如:

StringBuilder builder =
        new StringBuilder();

builder.append("Java");
builder.append("SE");
builder.append(21);

System.out.println(builder);

得到:

JavaSE21

append() 有很多重载形式,可以追加:

String
char
int
long
double
boolean
Object
……

例如:

StringBuilder builder =
        new StringBuilder();

builder.append("姓名:");
builder.append("张三");
builder.append(",年龄:");
builder.append(20);
builder.append(",Java通过:");
builder.append(true);

System.out.println(builder);

3.3 append() 为什么可以链式调用

经常会看到:

StringBuilder builder =
        new StringBuilder();

builder.append("Java")
       .append("SE")
       .append("-")
       .append(21);

为什么可以一直:

.append()
.append()
.append()

因为:

append()

执行完成以后,会返回当前 StringBuilder 对象本身。

可以粗略理解为:

builder.append("Java");

完成以后返回:

builder

于是:

builder
    .append("Java")
    .append("SE")
    .append(21);

实际上就是不断继续操作同一个 StringBuilder。

这种写法叫:

链式调用(Method Chaining)。


3.4 reverse():反转字符串内容

例如:

StringBuilder builder =
        new StringBuilder("abc");

builder.reverse();

System.out.println(builder);

结果:

cba

与 String 的很多 API 不同,这里:

builder.reverse();

会直接改变当前 StringBuilder 中保存的字符序列。


3.5 length():获取当前长度

例如:

StringBuilder builder =
        new StringBuilder("JavaSE");

System.out.println(builder.length());

得到:

6

追加之后:

builder.append(21);

System.out.println(builder.length());

当前内容:

JavaSE21

长度:

8

3.6 toString():转换回 String

这是 StringBuilder 使用过程中非常重要的一步。

例如:

StringBuilder builder =
        new StringBuilder();

builder.append("Java");
builder.append("SE");

String result = builder.toString();

现在:

result

类型就是:

String

为什么需要转换?

因为:

StringBuilder
通常是字符串构建过程中的工具

String
通常才是最终需要保存、传输、返回的数据

典型流程:

StringBuilder
      ↓
不断 append
      ↓
完成字符串构建
      ↓
toString()
      ↓
String

3.7 一个完整示例

public class StringBuilderDemo {
    public static void main(String[] args) {
        StringBuilder builder =
                new StringBuilder();

        builder.append("Java")
               .append("SE")
               .append('-')
               .append(21);

        System.out.println(builder);

        System.out.println(
                builder.length()
        );

        builder.reverse();

        System.out.println(builder);

        String result =
                builder.toString();

        System.out.println(result);
    }
}

构建过程:

""

↓ append("Java")

"Java"

↓ append("SE")

"JavaSE"

↓ append('-')

"JavaSE-"

↓ append(21)

"JavaSE-21"

3.8 StringBuilder 拼接案例

假设需要拼接:

1,2,3,4,5

不能简单:

for (int i = 1; i <= 5; i++) {
    builder.append(i).append(",");
}

因为最终会得到:

1,2,3,4,5,

最后多了一个逗号。

可以判断:

StringBuilder builder =
        new StringBuilder();

for (int i = 1; i <= 5; i++) {

    builder.append(i);

    if (i < 5) {
        builder.append(",");
    }
}

System.out.println(builder);

结果:

1,2,3,4,5

这就是 StringBuilder 非常典型的应用:

动态构建复杂字符串。


3.9 StringBuilder 判断回文

例如判断:

level

是否为回文字符串。

回文:

正着读
和
反着读
相同

可以借助:

reverse()

思考:

原字符串
   ↓
复制到 StringBuilder
   ↓
reverse()
   ↓
toString()
   ↓
与原字符串比较

例如:

String text = "level";

String reversed =
        new StringBuilder(text)
                .reverse()
                .toString();

boolean result =
        text.equals(reversed);

System.out.println(result);

3.10 insert():在指定位置插入内容

StringBuilder 还可以真正修改字符序列。

例如:

StringBuilder builder =
        new StringBuilder("JavaSE");

builder.insert(4, "-");

System.out.println(builder);

得到:

Java-SE

这里:

insert(4, "-")

表示:

在索引 4 的位置插入 "-"


3.11 delete():删除一段内容

例如:

StringBuilder builder =
        new StringBuilder("Java--SE");

builder.delete(4, 6);

System.out.println(builder);

结果:

JavaSE

delete(start, end) 同样使用:

[start, end)

即:

包前不包后。

这个规则与:

substring()

具有很强的一致性。


四、原理与进阶

4.1 StringBuilder 为什么适合不断追加内容

StringBuilder 内部维护一个:

可扩展的字符存储空间。

可以把它抽象理解成:

StringBuilder

┌──────────────────────────┐
│ J │ a │ v │ a │   │   │
└──────────────────────────┘

继续:

append("SE");

可以在现有字符序列后继续写入:

┌──────────────────────────┐
│ J │ a │ v │ a │ S │ E │
└──────────────────────────┘

当内部空间不足时:

StringBuilder 会自动扩大容量。

因此开发者通常不需要自己管理扩容。


4.2 length 和 capacity 是两回事

例如:

length

表示:

当前已经有多少字符。

而:

capacity

表示:

当前内部空间能够容纳多少字符,超过后需要重新分配更大的内部存储。

默认:

new StringBuilder()

初始容量为:

16

例如:

StringBuilder builder =
        new StringBuilder();

System.out.println(builder.length());
System.out.println(builder.capacity());

刚创建时:

length   = 0
capacity = 16

因此:

length
≠
capacity

可以类比:

length   = 水杯里当前有多少水
capacity = 水杯最多能装多少水

4.3 容量不够怎么办

一般情况下:

StringBuilder

会自动扩大内部容量。

开发者不需要频繁操作它。

如果提前知道最终字符串可能很大,也可以指定初始容量:

StringBuilder builder =
        new StringBuilder(1000);

这并不是:

字符串长度直接变成1000

而只是:

提前准备容量

此时:

builder.length()

仍然是:

0

4.4 StringBuilder 和字符串 + 到底怎么选

不要形成错误结论:

看到 + 就必须替换成 StringBuilder。

例如:

String message =
        "Hello, " + username + "!";

这种代码:

  • 简洁
  • 清晰
  • 可读性很好

完全可以使用。

真正值得显式使用 StringBuilder 的典型情况是:

String result = "";

for (...) {
    result = result + 数据;
}

即:

循环中不断构造越来越长的字符串。

推荐:

StringBuilder builder =
        new StringBuilder();

for (...) {
    builder.append(数据);
}

String result =
        builder.toString();

因此应该理解成:

简单拼接
→ 优先可读性

大量重复构建
→ 考虑 StringBuilder

而不是:

String + 一律错误

4.5 不要死记“+ 底层一定变成 StringBuilder”

Java 编译器允许对字符串拼接进行优化。

不同 JDK、不同编译器实现细节可能不同。

因此不应该机械背:

字符串 + 底层一定就是 new StringBuilder().append(...)

这种说法过于绝对。

真正稳定、应该掌握的语义是:

String 本身不可变

以及:

非编译期常量的字符串连接最终产生新的 String 结果

至于 JVM 和编译器具体采用什么优化机制:

属于实现细节。

在现代 JDK 中,这些实现细节可能随着版本演进。


4.6 toString() 之后发生了什么

例如:

StringBuilder builder =
        new StringBuilder("Java");

String result =
        builder.toString();

builder.append("SE");

System.out.println(result);
System.out.println(builder);

此时:

result

仍然表示转换时得到的:

Java

而:

builder

已经变成:

JavaSE

也就是说:

toString() 得到 String 后,后续修改 StringBuilder 不会跟着修改已经得到的 String。

这也符合:

String 不可变

的设计。


4.7 StringBuilder 的线程安全问题

StringBuilder:

不保证同步。

简单来说:

它不是为多个线程同时修改同一个实例而设计的。

这也是 StringBuilder 与:

StringBuffer

的重要区别之一。

StringBuffer 的很多操作具有同步机制。

可以初步理解:

| 类型 | 可变 | 同步保证 | | ------------- | ------ | ---------------------- | | String | 不可变 | 不需要通过可变操作同步 | | StringBuilder | 可变 | 不提供同步保证 | | StringBuffer | 可变 | 提供同步机制 |

在绝大多数普通局部字符串构建场景:

StringBuilder

通常已经足够。

等学习多线程时,再进一步理解:

线程安全
竞态条件
锁
同步

等概念。


五、实践应用

5.1 动态生成文章摘要

假设有多段文字:

String[] words = {
        "Java",
        "SE",
        "是",
        "Java",
        "技术体系",
        "的",
        "基础"
};

可以:

StringBuilder builder =
        new StringBuilder();

for (String word : words) {
    builder.append(word);
}

String result =
        builder.toString();

这种:

不断追加
→ 最终生成 String

正是 StringBuilder 的典型工作模型。


5.2 动态构造日志信息

例如:

StringBuilder log =
        new StringBuilder();

log.append("userId=")
   .append(1001)
   .append(", username=")
   .append("LingXi")
   .append(", action=")
   .append("LOGIN");

String result =
        log.toString();

最终:

userId=1001, username=LingXi, action=LOGIN

5.3 构造 HTML / SQL / JSON 时需要注意

StringBuilder 从技术上当然可以拼:

HTML
SQL
JSON

例如:

builder.append("<h2>")
       .append(title)
       .append("</h2>");

但是实际工程中通常还有:

模板引擎
JSON 序列化框架
SQL 参数化
ORM

等更专业方案。

尤其 SQL:

不应该因为会 StringBuilder,就大量手工拼接用户输入 SQL。

因为还会涉及:

SQL 注入
转义
参数绑定

等安全问题。

因此 StringBuilder 是:

字符串构建工具。

不是:

所有文本格式都应该手工拼接。


六、常见问题

6.1 StringBuilder 是 String 的子类吗?

不是。

它们是不同的类。

不能写:

String s =
        new StringBuilder();

需要:

StringBuilder builder =
        new StringBuilder();

最终转换:

String s =
        builder.toString();

6.2 append() 为什么可以追加数字?

例如:

builder.append(100);

因为 StringBuilder 为不同参数类型提供了多种:

append(...)

重载。

数字会以对应字符串形式追加。

因此:

builder.append(100);

最终追加的是字符序列:

100

6.3 append() 为什么能一直点下去?

因为:

append()

返回当前 StringBuilder 对象本身。

因此:

builder.append("A")
       .append("B")
       .append("C");

可以连续调用。


6.4 StringBuilder 可以直接 println 吗?

可以:

System.out.println(builder);

因为输出过程中会得到它对应的字符串表现。

但是:

如果业务 API 明确要求 String 类型,应该调用 toString()

例如:

String result =
        builder.toString();

6.5 为什么最终通常还要转成 String?

因为 StringBuilder 通常承担:

构建

而 String 承担:

最终字符串值

很多方法、字段、DTO、数据库参数和业务接口最终使用的仍然是:

String

6.6 StringBuilder 是否一定比 String 快?

不能简单这样说。

错误理解:

StringBuilder 永远比 String 快

正确理解:

String 和 StringBuilder 解决的问题不同。

例如:

String name = "Java";

根本没有理由改成:

StringBuilder name =
        new StringBuilder("Java");

而:

for (...) {
    result += value;
}

这种重复字符串累积才是 StringBuilder 的经典场景。


6.7 StringBuilder 能作为多线程共享对象随便修改吗?

不应该。

StringBuilder 本身:

不保证多线程同步安全。

普通单线程、局部方法内构建字符串:

StringBuilder

通常最合适。

涉及多个线程共享修改时,则必须重新考虑并发安全设计。


6.8 new StringBuilder(100) 是创建了100个字符吗?

不是。

它只是指定:

初始容量 = 100

此时:

builder.length()

仍然是:

0

这是:

容量

与:

实际长度

的区别。


七、练习与验收

7.1 知识问答

  1. StringBuilder 是什么?
  2. String 与 StringBuilder 最核心的区别是什么?
  3. 为什么循环中的大量字符串拼接适合考虑 StringBuilder?
  4. append() 有什么作用?
  5. 为什么 append() 可以进行链式调用?
  6. reverse() 是否会改变当前 StringBuilder?
  7. toString() 的作用是什么?
  8. 为什么最终业务结果通常仍然需要转成 String?
  9. length()capacity() 有什么区别?
  10. StringBuilder 容量不足时会发生什么?
  11. new StringBuilder(100) 中的 100 表示什么?
  12. StringBuilder 是否线程安全?
  13. StringBuilder 与 StringBuffer 的基本区别是什么?
  14. 为什么不能简单说“任何字符串 + 都必须换成 StringBuilder”?
  15. 为什么不应该死记“+ 底层一定使用 StringBuilder”?

7.2 代码阅读

不运行代码,写出执行结果:

public class Test {
    public static void main(String[] args) {
        StringBuilder builder =
                new StringBuilder();

        builder.append("Java")
               .append("SE")
               .append(21);

        System.out.println(builder);
        System.out.println(builder.length());

        builder.reverse();

        System.out.println(builder);
    }
}

继续阅读:

public class Test {
    public static void main(String[] args) {
        StringBuilder builder =
                new StringBuilder("Java");

        String text =
                builder.toString();

        builder.append("SE");

        System.out.println(text);
        System.out.println(builder);
    }
}

回答:

  1. 两次输出分别是什么?
  2. 为什么修改 buildertext 没有发生变化?
  3. 这体现了 String 的什么特性?

7.3 手写代码

任务一:高效拼接数字

使用 StringBuilder 构造:

1-2-3-4-5-6-7-8-9-10

要求:

  • 最后不能出现多余 -
  • 最终转换成 String

任务二:反转字符串

输入:

JavaSE

输出:

ESavaJ

要求:

  • 使用 StringBuilder
  • 不手写逆序循环

任务三:回文判断

判断以下字符串是否为回文:

level
abcba
Java

要求使用:

StringBuilder + reverse + toString + equals

任务四:大量数据构造

使用:

StringBuilder

构建包含:

0 ~ 9999

全部数字的字符串。

思考:

  1. 为什么这种场景比 String += 更适合 StringBuilder?
  2. 是否有必要提前设置容量?

7.4 Debug

下面代码存在设计问题:

public class DebugDemo {
    public static void main(String[] args) {
        String result = "";

        for (int i = 0; i < 100000; i++) {
            result = result + i;
        }

        System.out.println(result);
    }
}

要求:

  1. 代码语法是否错误?
  2. 真正的问题是什么?
  3. 使用 StringBuilder 重构。
  4. 保证最终结果仍然是 String。

继续 Debug:

StringBuilder builder =
        new StringBuilder("Java");

String result = builder;

回答:

  1. 是否可以编译?
  2. 为什么?
  3. 如何正确转换?

7.5 综合训练

设计一个控制台“学生信息报告生成器”。

已有:

姓名
学号
Java成绩
数据结构成绩
数据库成绩

要求生成:

===== 学生成绩报告 =====
姓名:...
学号:...
Java:...
数据结构:...
数据库:...
平均分:...
=======================

要求:

  1. 使用 StringBuilder 构建报告。
  2. 使用链式 append()
  3. 最终转换为 String。
  4. 不允许把整个结果直接写成一个固定字符串。
  5. 思考哪些内容适合动态生成,哪些属于固定模板。

7.6 本章验收

闭卷完成以下内容:

能够口述:

String
vs
StringBuilder

核心区别。

能够手写:

new StringBuilder()
append()
reverse()
length()
toString()

能够解释:

为什么大量重复拼接考虑 StringBuilder

能够解释:

为什么普通的少量 + 并不是错误写法

能够区分:

length
capacity

能够知道:

StringBuilder 不提供线程同步保证

能够完成:

String
→ StringBuilder
→ 修改
→ String

这一完整数据转换流程。