Java 常用类之字符串
1、字符串的共享与常量折叠
String
类的对象是不可变的,我们只能改变 String 类型的对象变量的值(即指针的指向),而不能改变对象本身。
字符串字面量
是共享的,而用+
拼接 String 对象变量或执行substring
等操作得到的字符串不共享,验证一下:
public class Main {
public static void main(String[] args) {
String s1 = "He", s2 = "llo";
System.out.println(s1 == "He" && s2 == "llo"); // true
System.out.println("He" + "llo" == "Hello"); // true
System.out.println("----------");
System.out.println(s1 + "llo" == "Hello"); // false
System.out.println(s1 + s2 == "Hello"); // false
System.out.println("Hello World!".substring(0, 5) == "Hello"); // false
}
}
没问题!但是为什么用 + 符号拼接的 String 对象常量是共享的呢?
我们接着看。如果为s1
和s2
加上final
修饰符,结果会有哪些变化:
public class Main {
public static void main(String[] args) {
final String s1 = "He", s2 = "llo";
System.out.println(s1 == "He" && s2 == "llo"); // true
System.out.println("He" + "llo" == "Hello"); // true
System.out.println("----------");
System.out.println(s1 + "llo" == "Hello"); // true
System.out.println(s1 + s2 == "Hello"); // true
System.out.println("Hello World!".substring(0, 5) == "Hello"); // false
}
}
如上所示,第三和第四个判断语句的结果与之前不同,结果为true
。为什么会这样呢?
这是因为编译器有一种称为常量折叠
的优化技术:它会在编译期间,完成常量之间的计算,而不需要等到运行时计算。
我们简化一下上述代码,再使用反编译命令javap -v Main.class
,验证一下。
简化代码如下:
public class Main {
public static void main(String[] args) {
final String s1 = "He", s2 = "llo";
String a = s1 + "llo";
String b = s1 + s2;
}
}
反编译如下:
Code:
stack=1, locals=5, args_size=1
0: ldc #7 // String He
2: astore_1
3: ldc #9 // String llo
5: astore_2
6: ldc #11 // String Hello
8: astore_3
9: ldc #11 // String Hello
11: astore 4
13: return
根据ldc
指令可以知道,字符串Hello
都是从常量池中取的,没有进行+
拼接操作,这就验证了编译器的常量折叠。
2、码点与代码单元
区分码点
与代码单元
:一个 UTF-16 编码的 Unicode 字符对应一个码点,对应 1 ~ 2 个 char 值;一个 char 值对应一个代码单元。
如下所示,Unicode 字符🍺
(啤酒杯)的码点为 U+1F37A;它对应两个代码单元,分别是 U+D83C 和 U+DF7A;它也对应两个 char 值,分别是 \uD83C 和 \uDF7A。
public class Main {
public static void main(String[] args) {
String s = "\uD83C\uDF7A"; // 🍺(啤酒杯)
System.out.println(s); // 🍺
System.out.println(Arrays.toString(s.toCharArray())); // [?, ?]
}
}
平时我们都是对代码单元
进行遍历,那么如何对码点
进行遍历?如下所示。
public class Main {
public static void main(String[] args) {
String str = "I Love \uD83C\uDF7A!";
int[] codePoints = str.codePoints().toArray();
for (int v : codePoints) {
System.out.print(v + " ");
}
System.out.println();
System.out.println(new String(codePoints, 0, codePoints.length));
}
}
3、构建字符串
我们之前说过,用+
拼接 String 对象变量或执行substring
等操作得到的字符串不共享。当需要频繁地拼接字符串时,每次执行 + 操作都会构建一个新的 String 对象,既耗时又浪费空间。
这时,我们可以使用StringBuffer
或者StringBuilder
类,来避免这个问题发生。
StringBuffer:线程安全、效率低;StringBuilder(Java5):线程不安全、效率高。
示例代码:
public class Main {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder();
for(int i = 0; i < 10; i++) {
builder.append(i);
}
String str = builder.toString();
System.out.println(str);
}
}
如有错误,欢迎指正。.... .- ...- . .- -. .. -.-. . -.. .- -.-- -.-.--