String 类在 Java 中非常特殊,它被 final 修饰,意味着它不能被继承,Java 为了提高字符串的性能和内存使用,引入了字符串常量池的概念,这使得 String 的构造方式与其他普通对象有所不同。

String 类提供了非常多的构造函数,以满足不同场景下创建字符串对象的需求,下面我将它们分类进行详细解释。
从字符数组构造
这是最常见的一类构造函数,允许你将一个字符数组转换成字符串。
String(char[] value)
这是最基础的构造函数,它将整个字符数组的内容复制到一个新的 String 对象中。
char[] chars = {'H', 'e', 'l', 'l', 'o'};
String str1 = new String(chars);
System.out.println(str1); // 输出: Hello
String(char[] value, int offset, int count)
这个构造函数允许你从字符数组的指定 offset(偏移量)位置开始,取 count 个字符来构造字符串。

char[] chars = {'W', 'o', 'r', 'l', 'd', '!', 'J', 'a', 'v', 'a'};
String str2 = new String(chars, 6, 4); // 从索引6开始,取4个字符
System.out.println(str2); // 输出: Java
从字节数组构造
这类构造函数在处理 I/O 流、网络传输或文件读写时非常有用,因为数据通常以字节数组的形式存在,这里涉及到字符编码的问题,非常重要。
String(byte[] bytes)
使用平台默认的字符集(charset,通常是 UTF-8 或系统默认编码)将字节数组解码成字符串。
byte[] bytes = {72, 101, 108, 108, 111}; // "Hello" 的 UTF-8 编码
String str3 = new String(bytes);
System.out.println(str3); // 输出: Hello
String(byte[] bytes, int offset, int length)
与上面类似,但只处理字节数组中从 offset 开始的 length 个字节。
byte[] bytes = {87, 111, 114, 108, 100, 33};
String str4 = new String(bytes, 0, 5); // 取前5个字节
System.out.println(str4); // 输出: World
String(byte[] bytes, String charsetName)
使用指定的字符集(如 "UTF-8", "GBK", "ISO-8859-1")将整个字节数组解码成字符串。这是处理编码问题最推荐的方式。

byte[] gbkBytes = {-60, -29, -70, -61}; // "你好" 在 GBK 编码下的字节数组
// 错误示范:用错误的编码解码
String wrongStr = new String(gbkBytes, "UTF-8"); // 可能会乱码,输出类似: æ¨å¥½
System.out.println("错误解码: " + wrongStr);
// 正确示范:用正确的编码解码
String correctStr = new String(gbkBytes, "GBK"); // 输出: 你好
System.out.println("正确解码: " + correctStr);
String(byte[] bytes, int offset, int length, String charsetName)
结合了以上两个功能,指定范围并使用指定字符集。
从字符串或其部分构造
String(String original)
这个构造函数看起来有点奇怪,因为它接收一个 String 对象作为参数,然后创建一个新的 String 对象,新对象的值与原始对象相同。
重要知识点:字符串不可变性与 intern() 方法
-
不可变性:
String对象一旦创建,其内容就不能被修改,这个构造函数会创建一个新的String实例,即使它的内容与另一个实例完全相同。 -
字符串常量池:为了优化内存,JVM 维护了一个字符串常量池,当你使用字面量(如
String s = "hello";)创建字符串时,JVM 会先检查池中是否存在该字符串,如果存在则直接引用,不存在则创建并存入池中。 -
new String()vs. 字面量:String s1 = "hello";// s1 直接指向字符串常量池中的 "hello"String s2 = new String("hello");// JVM 会在堆上创建一个新的 String 对象,其内容是 "hello"。"hello" 这个字面量会被放入常量池(如果还没有的话),s2 指向堆中的新对象。
s1 == s2的结果是false,因为它们是两个不同的对象(一个在池中,一个在堆中)。s1.equals(s2)的结果是true,因为它们的内容相同。
String original = "hello"; String str5 = new String(original); // 在堆上创建了一个新的 "hello" 对象 System.out.println(original == str5); // false System.out.println(original.equals(str5)); // true
String(StringBuffer buffer)
使用 StringBuffer 对象的内容创建一个不可变的 String 对象。
StringBuffer sb = new StringBuffer("Java");
String str6 = new String(sb);
System.out.println(str6); // 输出: Java
String(StringBuilder builder)
与 StringBuffer 类似,但使用 StringBuilder,这是 Java 5 引入的、非线程安全的版本,性能更高。
StringBuilder sb = new StringBuilder("StringBuilder");
String str7 = new String(sb);
System.out.println(str7); // 输出: StringBuilder
其他构造函数
String(int[] codePoints, int offset, int count)
这是一个比较特殊的构造函数,它基于 Unicode 代码点数组来创建字符串,代码点可以表示一个字符,也可以表示一个代理对(用于表示某些特殊的 Unicode 字符,如某些表情符号)。
// 'A' 的代码点是 65, 'B' 是 66, '€' (欧元符号) 的代码点是 8364
int[] codePoints = {65, 66, 8364};
String str8 = new String(codePoints, 0, 3);
System.out.println(str8); // 输出: AB€
重要知识点总结
| 构造函数示例 | 描述 | 关键点 |
|---|---|---|
new String(char[] value) |
从整个字符数组创建。 | 复制字符数组内容到新 String 对象。 |
new String(byte[] bytes) |
使用默认字符集从字节数组创建。 | 依赖平台默认编码,可能产生乱码。 |
new String(byte[] bytes, "UTF-8") |
使用指定字符集从字节数组创建。 | 推荐使用,明确编码,避免乱码。 |
new String(String original) |
从另一个 String 对象创建。 |
创建一个新相同的不可变对象。 |
String s = "literal"; |
使用字符串字面量创建。 | 会检查字符串常量池,优化内存。 |
new String() vs. "literal" |
new String() 创建一个空字符串对象; 也是空字符串,但可能指向池中的同一个实例。 |
注意空字符串字面量 的池化行为。 |
最佳实践
- 优先使用字面量:对于常规字符串,直接使用
String s = "hello";,这是最简洁、最高效的方式,并且能充分利用字符串常量池的优化。 - 处理 I/O 时指定编码:当从文件、网络或数据库等来源获取字节数据并转换为字符串时,务必使用
String(byte[] bytes, String charsetName)构造函数并明确指定正确的字符集(如 "UTF-8"),以避免乱码问题。 - 理解
new String()的开销:除非你有特殊需求(你需要一个独立于常量池的String实例,或者你想确保字符串不被外部修改),否则尽量避免使用new String("literal")这种方式,因为它会多创建一个不必要的对象。 - 区分 和
equals():永远记住, 比较的是对象的内存地址(引用),而String.equals()比较的是字符串的内容,对于字符串,内容比较才是最常见的。
