String

2023/11/5 Java基础

String是字符串类型,String不是基本数据类型,是引用类型。

# 1、概览

String 被声明为 final,因此它不可被继承。

在Java 8中,String 内部使用 char 数组存储字符串。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}
1
2
3
4
5

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final byte[] value;

    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
}
1
2
3
4
5
6
7
8

value 数组被声明为 final,这意味着数据初始化之后不能再更改,并且 String 内部也没提供修改 value 数组的方法,因此可以包装 String 不可变。

# 2、不可变的好处

  • 可以缓存 hash 值

因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

  • String Pool 的需要

如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

  • 安全性

String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。

  • 线程安全

String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

参考:Program Creek : Why String is immutable in Java? (opens new window)

# 3、String、StringBuffer、StringBuilder

  • 可变性

  • String 不可变
  • StringBuffer 和 StringBuilder 可变
  • 线程安全

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

参考:StackOverflow : String, StringBuffer, and StringBuilder (opens new window)

# 4、String Pool

字符串常量池(String Pool)是一种特殊的内存区域,用于存储字符串常量。

字符串常量池有以下特点:

  • 字符串常量池是在堆内存中的一部分,与堆中的其他对象分开管理。
  • 字符串常量池中的字符串对象是不可变的,一旦创建就不能修改。
  • 当创建一个字符串时,Java 首先检查字符串常量池中是否已经存在相同内容的字符串对象。
  • 如果存在相同内容的字符串对象,就返回常量池中的对象的引用,而不会创建新的字符串对象。
  • 如果字符串常量池中不存在相同内容的字符串对象,就在常量池中创建一个新的字符串对象。

这种机制可以减少内存占用,提高字符串的重用性和效率。字符串常量池对于编译器优化、字符串比较和字符串拼接等操作都有重要的作用。

在 Java 中,可以使用字面值或使用 String 类的构造函数(如 new String("example"))创建字符串对象。使用字面值创建的字符串会自动存储在字符串常量池中,而使用构造函数创建的字符串对象则需要手动使用 intern() 方法将其添加到字符串常量池中。

String str1 = "example"; // 字符串常量池中创建一个字符串对象
String str2 = new String("example"); // 堆中创建一个字符串对象
String str3 = str2.intern(); // 将 str2 对象添加到字符串常量池,str3 引用字符串常量池中的对象

System.out.println(str1 == str2); // 输出 false,引用不同的对象
System.out.println(str1 == str3); // 输出 true,引用相同的对象

// 如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6);  // true
1
2
3
4
5
6
7
8
9
10
11

# 5、new String("abc")

使用new字符串的方式,一共会创建两个字符串对象。

public class Hello {  
    public static void main(String[] args) {
        String s = new String("abc");
    }
}
1
2
3
4
5

解析

  • 遇到关键字 new,说明要创建一个新的对象。
  • 关键字 new 后面跟着类名 String,表示要创建一个 String 类型的对象。
  • 使用 String 类的构造函数 String(String original),其中 original 参数是要在新对象中存储的初始值。在这种情况下,初始值是 "abc"
  • 执行 new String("abc") 表达式时,会在堆内存中创建一个新的 String 对象,并以 "abc" 作为初始值。
  • 由于字符串常量池的机制,在字符串常量池中,如果字符串 "abc" 不存在,则创建一个新的字符串对象,并将其添加到常量池中。

new String("abc")表达式会创建一个新的字符串对象,并将 "abc" 作为其初始值。这个表达式在堆内存中创建对象,而不是在字符串常量池中。如果希望将其添加到字符串常量池中,可以使用 intern() 方法,例如 new String("abc").intern()。这个时候s就指向字符串常量池中"abc"的引用了,原来通过 new String("abc") 创建的那个字符串对象,如果没有其他引用指向它的话,它就会被当作垃圾对象进行回收。