欢迎光临
我们一直在努力

【Java八股|第2篇】String 为什么是不可变的?

前言

上一篇我们学习了 == 和 equals 的区别,其中反复提到了 String。

在 Java 面试中,String 是一个非常高频的知识点。

面试官可能会问:

  • String 为什么是不可变的?
  • String 不可变体现在哪里?
  • String 不可变有什么好处?
  • StringBuilder 为什么可以修改?
  • 字符串拼接会不会改变原来的字符串?

这一篇我们就来单独梳理 String 不可变这个问题。

一、什么叫 String 不可变?

所谓 String 不可变,意思是:

一个 String 对象一旦创建,它所表示的字符串内容就不能被修改。

比如:

public class Test {
public static void main(String[] args) {
String str = "hello";
str = str + " world";

System.out.println(str);
}
}

输出结果:

hello world

看起来好像 str 从 "hello" 变成了 "hello world"。

但实际上,原来的 "hello" 字符串对象并没有被修改。

真正发生的是:

1. 创建字符串对象 "hello"
2. 拼接后创建新的字符串对象 "hello world"
3. 让 str 指向新的字符串对象
4. 原来的 "hello" 对象内容没有改变

也就是说,变的是变量 str 的引用指向,不是原来字符串对象的内容。

二、用代码理解不可变

看下面这个例子:

public class Test {
public static void main(String[] args) {
String s1 = "hello";
String s2 = s1;

s1 = s1 + " world";

System.out.println(s1);
System.out.println(s2);
}
}

输出结果:

hello world
hello

解释一下:

一开始:

String s1 = "hello";
String s2 = s1;

s1 和 s2 都指向 "hello"。

后来:

s1 = s1 + " world";

这行代码创建了一个新的字符串 "hello world",然后让 s1 指向新对象。

但是 s2 仍然指向原来的 "hello"。

所以最后输出:

s1 = hello world
s2 = hello

这就说明原来的字符串对象没有被修改。

三、String 类本身是 final 的

String 类是 final 修饰的。

可以简单理解为:

public final class String {
// 省略源码
}

final 修饰类时,表示这个类不能被继承。

也就是说,我们不能写一个类继承 String:

public class MyString extends String {
}

这是不允许的。

为什么不允许继承?

因为如果允许继承,子类就可能破坏 String 原本的不可变设计。

所以 String 被设计成 final,从类继承层面防止被扩展和篡改。

四、String 底层保存字符的数组不能随便改

在不同 Java 版本中,String 的底层实现略有不同。

在 Java 8 中,String 底层主要使用 char[] 保存字符。

在 Java 9 之后,String 底层改成了 byte[] 加编码标记的方式,用来节省内存。

但是不管底层是 char[] 还是 byte[],核心思想都一样:

保存字符串内容的字段是私有的,并且不会对外提供直接修改内容的方法。

可以简单理解为:

public final class String {
private final char[] value;
}

这里有几个重点:

  • private:外部不能直接访问
  • final:引用不能随便改变
  • 没有提供修改原内容的方法
  • 所以普通代码不能直接修改 String 对象内部保存的字符数据。

    五、String 没有提供修改原对象的方法

    比如我们调用:

    String s = "hello";
    String result = s.replace("h", "H");

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

    输出结果:

    hello
    Hello

    可以看到:

    s.replace("h", "H")

    并没有修改原来的 s。

    它返回的是一个新的字符串。

    类似的方法还有:

    substring()
    toUpperCase()
    toLowerCase()
    concat()
    replace()

    这些方法看起来像是在修改字符串,其实都是返回新字符串。

    比如:

    String s = "java";
    String upper = s.toUpperCase();

    System.out.println(s);
    System.out.println(upper);

    输出结果:

    java
    JAVA

    原来的 java 没有改变。

    六、String 不可变和字符串常量池有关

    String 不可变的一个重要好处是:可以安全地使用字符串常量池。

    比如:

    String s1 = "hello";
    String s2 = "hello";

    s1 和 s2 可能指向字符串常量池中的同一个 "hello" 对象。

    如果 String 是可变的,那么就会出现一个严重问题。

    假设 s1 修改了 "hello" 的内容:

    hello -> hi

    那么 s2 指向的内容也会跟着变化。

    这显然是不安全的。

    正因为 String 不可变,所以多个引用可以安全地共享同一个字符串对象。

    这也是字符串常量池能够存在的重要原因。

    七、String 不可变有什么好处?

    1. 安全性更高

    字符串经常用于保存重要信息,比如:

    • 文件路径
    • 数据库连接地址
    • 用户名
    • 网络请求地址
    • 类名
    • 配置项
    • Token 的一部分

    如果字符串可以随便被修改,就可能带来安全问题。

    比如方法接收到一个数据库连接字符串,如果中途被其他地方修改了,就可能连接到错误地址。

    String 不可变可以避免这种问题。

    2. 可以安全共享

    因为字符串不可变,所以多个变量可以安全地引用同一个字符串对象。

    比如:

    String s1 = "java";
    String s2 = "java";
    String s3 = "java";

    它们都可以指向常量池中的同一个对象。

    这样可以减少重复对象创建,节省内存。

    3. 适合作为 HashMap 的 key

    String 经常作为 HashMap 的 key。

    比如:

    Map<String, String> map = new HashMap<>();

    map.put("username", "zhangsan");
    map.put("password", "123456");

    如果 String 是可变的,那么作为 key 放入 HashMap 后,内容一旦改变,hash 值也可能改变。

    这样会导致原来的 key 找不到。

    因为 String 不可变,所以它作为 key 更稳定。

    4. hash 值可以缓存

    String 的 hashCode 计算比较常用。

    因为 String 不可变,所以它的 hash 值一旦计算出来,就可以缓存起来。

    下次再调用 hashCode() 时,就不用重复计算。

    如果字符串内容可以变化,那么缓存 hash 值就不安全了。

    5. 线程安全

    不可变对象天然更容易保证线程安全。

    多个线程同时读取同一个 String 对象,不会出现一个线程修改内容影响另一个线程的问题。

    所以 String 在多线程环境下可以安全共享。

    八、String 拼接到底发生了什么?

    看下面的代码:

    String s = "hello";
    s += " world";

    很多初学者会以为这是在原来的 hello 后面追加内容。

    其实不是。

    它大致相当于创建了一个新的字符串对象:

    hello world

    然后让变量 s 指向这个新对象。

    原来的 "hello" 对象没有改变。

    如果在循环中大量拼接字符串:

    String result = "";

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

    这种写法性能不太好。

    因为每次拼接都会产生新的字符串对象。

    更推荐使用:

    StringBuilder builder = new StringBuilder();

    for (int i = 0; i < 10000; i++) {
    builder.append(i);
    }

    String result = builder.toString();

    这就是为什么后面要学习 StringBuilder。

    九、StringBuilder 为什么可以修改?

    StringBuilder 是可变的。

    比如:

    StringBuilder builder = new StringBuilder("hello");

    builder.append(" world");

    System.out.println(builder);

    输出结果:

    hello world

    这里的 append 会在原来的 StringBuilder 对象上追加内容。

    所以 StringBuilder 适合大量字符串拼接。

    但是它和 String 的定位不一样。

    类型是否可变常见用途
    String 不可变 保存字符串常量、普通字符串
    StringBuilder 可变 单线程大量字符串拼接
    StringBuffer 可变 多线程环境下字符串拼接

    后面我们会专门写一篇讲它们的区别。

    十、final 一定能保证对象不可变吗?

    不一定。

    这是一个容易混淆的点。

    比如:

    final int[] arr = {1, 2, 3};

    arr[0] = 100;

    System.out.println(arr[0]);

    输出结果:

    100

    虽然 arr 被 final 修饰,但是数组内容仍然可以修改。

    final 修饰引用类型变量时,表示这个引用不能再指向其他对象,但不代表对象内部内容一定不能变。

    所以 String 不可变不是只靠一个 final 实现的,而是多个设计共同保证的:

  • String 类是 final 的
  • 内部保存数据的字段是私有的
  • 不对外暴露修改内部数据的方法
  • 修改类方法时返回新字符串
  • 底层实现避免外部直接修改内部内容
  • 十一、面试标准回答

    如果面试官问:

    String 为什么是不可变的?

    可以这样回答:

    String 是不可变对象,一个字符串对象创建之后,它的内容就不能再被修改。

    从设计上看,String 类被 final 修饰,不能被继承;内部保存字符数据的字段是私有的,外部不能直接访问;并且 String 没有提供修改原对象内容的方法。像 replace、substring、toUpperCase 这些方法,看起来像修改字符串,实际上都是返回新的字符串对象。

    String 设计成不可变有很多好处,比如可以安全使用字符串常量池,多个变量可以共享同一个字符串对象;可以提高安全性;适合作为 HashMap 的 key;hash 值可以缓存;在多线程环境下也更安全。

    十二、常见问题总结

    1. String 真的完全不能被改吗?

    在正常 Java 代码中,不能修改。

    虽然通过反射等非常规方式可能破坏内部结构,但这不是正常开发方式,面试和项目中一般不讨论这种写法。

    2. s = s + “abc” 是不是修改了原字符串?

    不是。

    这是创建了新的字符串对象,然后让变量 s 指向新对象。

    原来的字符串对象没有被修改。

    3. replace 方法会修改原字符串吗?

    不会。

    replace 会返回一个新的字符串。

    比如:

    String s = "hello";
    String r = s.replace("h", "H");

    s 仍然是 "hello",r 是 "Hello"。

    4. String 不可变是不是只因为 final?

    不是。

    final 只是其中一个原因。

    真正的不可变是由类设计、字段封装、没有修改方法、返回新对象等多个因素共同保证的。

    5. 为什么大量拼接字符串不建议用 String?

    因为 String 不可变,每次拼接都可能产生新对象。

    大量拼接时更建议使用 StringBuilder。

    十三、总结

    这一篇主要学习了 String 为什么是不可变的。

    String 不可变的意思是:字符串对象创建之后,它表示的内容不能被修改。变量可以重新指向新的字符串对象,但原来的字符串对象本身不会变。

    String 的不可变设计和 final 类、私有字段、没有修改原内容的方法、返回新对象等机制有关。

    它的好处也很明显:可以安全使用字符串常量池,可以作为稳定的 HashMap key,可以缓存 hash 值,也更适合多线程共享。

    理解这一点之后,再看 String 拼接、字符串常量池、StringBuilder、HashMap key 等问题,就会清楚很多。

    下一篇我们继续学习 String、StringBuilder、StringBuffer 的区别。

    赞(0)
    未经允许不得转载:171主机测评 » 【Java八股|第2篇】String 为什么是不可变的?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址