杰瑞科技汇

Java String是引用类型,为何值不变?

String 是一个引用类型,但它有一些特殊的待遇。

这句话包含了两层意思:

Java String是引用类型,为何值不变?-图1
(图片来源网络,侵删)
  1. 本质是引用类型:和 Object, Array, List 一样,String 变量存储的是对象在内存中的地址(引用),而不是对象本身的数据。
  2. 有特殊的语法和优化:Java 为 String 提供了特殊的语法糖(如字面量)和内存优化机制(字符串常量池),这使得它的使用方式看起来和基本类型很像。

String 作为引用类型的工作原理

让我们从最基础的开始。

a. 对象的创建

当你创建一个 String 对象时,实际上是在堆内存中分配了一块空间来存储字符序列,而变量则指向这块空间的地址。

使用 new 关键字(显式创建)

String str1 = new String("Hello");
  • new String("Hello"):这部分在堆内存中创建了一个 String 对象,内容为 "Hello"。
  • str1:这是一个引用变量(可以理解为指针),它存储了上面创建的那个对象的内存地址。

使用字面量(隐式创建)

Java String是引用类型,为何值不变?-图2
(图片来源网络,侵删)
String str2 = "Hello";
  • "Hello":这是一个字符串字面量,当 Java 虚拟机在编译或运行时遇到这种字面量,它会去一个特殊的内存区域——字符串常量池——查找是否已经存在 "Hello" 这个字符串。
    • 如果不存在,就在常量池中创建一个新的 "Hello" 字符串,并将 str2 引用指向它。
    • 如果已经存在,就直接将 str2 引用指向常量池中已有的那个 "Hello" 字符串。

无论是 new 还是字面量,str1str2 这两个变量本身都只是引用,它们指向了真正存放 "Hello" 数据的对象。

b. 引用赋值与“指向”

引用类型的经典特性就是多个变量可以指向同一个对象。

String a = "hello"; // 在常量池中创建 "hello",a 指向它
String b = a;       // b 被赋值为 a 的引用,b 也指向同一个 "hello" 对象
// 现在修改 a 指向一个新的对象
a = a + " world";   // 1. 创建一个新的字符串 "hello world" (可能在堆或常量池)
                     // 2. 让 a 澄清这个新对象
                     // 3. b 仍然指向原来的 "hello"
System.out.println(a); // 输出: hello world
System.out.println(b); // 输出: hello

这个例子清晰地表明,ab 是独立的引用,它们可以指向不同的对象。a = a + " world" 并没有修改 a 原来指向的那个对象,而是让 a 指向了一个全新的对象。


String 的特殊性:不可变性与字符串常量池

String 最大的特殊性在于它的不可变性

Java String是引用类型,为何值不变?-图3
(图片来源网络,侵删)

a. 不可变性

一旦一个 String 对象被创建,它的内容(内部的字符数组 char[] value)就无法被修改

任何看起来像是在修改 String 的操作(如 concat(), replace(), 运算符),实际上都是:

  1. 基于原始字符串创建一个新的 String 对象。
  2. 将修改后的内容放入这个新对象中。
  3. 返回这个新对象的引用。

示例:

String s = "original";
s = s.concat(" modified"); // concat() 方法返回了一个新的字符串对象
// s 现在指向了新的 "original modified" 对象
// 原来的 "original" 对象如果没有任何引用指向它,就会被垃圾回收

不可变性的好处:

  • 线程安全不能被改变,所以多个线程可以同时访问一个 String 对象而无需同步,不会发生数据冲突。
  • 安全:在处理文件路径、网络地址、数据库用户名密码等敏感信息时,不可变性确保了它们在创建后不会被恶意修改。
  • 性能优化(字符串常量池的基础)不变,JVM 可以放心地将字符串字面量放入一个公共的“池子”中,供所有代码共享,避免了重复创建相同内容的字符串,从而节省内存。

b. 字符串常量池

这是一个位于方法区(或元空间)的特殊内存区域,专门用来存储字符串字面量和 intern() 方法返回的字符串引用。

工作流程:

  1. 当编译器遇到 String s = "abc"; 这样的代码时,它会检查常量池。
  2. "abc" 不在池中,就将其加入池中,并将 s 的引用指向池中的 "abc"。
  3. "abc已经在池中,就直接将s` 的引用指向池中已有的 "abc"。

new String() vs. 字面量的区别:

String s1 = "Hello";       // 1. 检查常量池,"Hello" 不存在,则创建并放入池中,s1 指向池中的 "Hello"。
String s2 = "Hello";       // 2. 检查常量池,"Hello" 已存在,s2 直接指向池中的同一个 "Hello"。
String s3 = new String("Hello"); // 3. 总是在堆内存中创建一个新的 "Hello" 对象,然后检查常量池,"Hello" 不存在,则将其放入池中(但 s3 仍然指向堆中的对象)。
System.out.println(s1 == s2);   // true (比较引用,指向同一个池中对象)
System.out.println(s1 == s3);   // false (比较引用,s1指向池中,s3指向堆中)

StringStringBuilderStringBuffer

由于 String 的不可变性,频繁进行字符串拼接(如在循环中)会创建大量临时对象,导致性能下降和内存浪费,为了解决这个问题,Java 提供了两个可变的工具类。

特性 String StringBuilder StringBuffer
可变性 不可变 可变 可变
线程安全 安全 (因为不可变) 不安全 安全 (方法有 synchronized 关键字)
性能 拼接性能差 性能最好 (无同步开销) 性能比 StringBuilder 差 (有同步开销)
使用场景 固定不变的字符串内容 单线程环境下的字符串操作,如循环拼接 多线程环境下的字符串操作

示例:

// 高效的单线程拼接
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append("i"); // 在同一个对象上进行修改,不创建新对象
}
String result = sb.toString();

  1. 引用类型本质String 变量存储的是对象的引用(地址),而不是字符序列本身。String s = "abc"; 中,s 是引用,"abc" 是对象。
  2. 不可变性String 对象一旦创建,内容就不能被修改,任何修改操作都会返回一个新的 String 对象。
  3. 字符串常量池:为了优化性能和节省内存,JVM 维护了一个字符串常量池,使用字面量创建的字符串会被放入池中,实现复用。new String() 总是在堆上创建新对象。
  4. vs. equals()
    • 比较两个引用是否指向同一个对象(内存地址是否相同)。
    • equals()String 重写了 equals() 方法,它比较的是两个字符串的是否相同。
  5. 性能考量:对于需要频繁修改的字符串,在单线程环境下应优先使用 StringBuilder,在多线程环境下使用 StringBuffer

理解了 String 作为引用类型的这些特性和优化机制,你就能写出更高效、更健壮的 Java 代码。

分享:
扫描分享到社交APP
上一篇
下一篇