1. 初始化
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
char[] chars = {
'h', 'e', 'l', 'l', 'o'};
String str3 = new String(chars);
}
仔细观察 str3 的创建过程
分析 new String(chars) 源码可得
会有一个 Arrays.copyOf(arr, length)
的拷贝工作。这就是 str3 的来历
总结
- 所有双引号内容都在 字符串常量池
- new 对象都在 堆
- new 对象首先会在 字符串常量池 中查找有无 字符串,没有的话就会自己创建,有的话就存储指向 字符串常量池 的引用值
2.比较
1. 直接赋值和引用赋值
了解了上述初始化的部分过程,可以看看常见字符串的比较的 true 与 false
public static void main(String[] args) {
String str1 = "hello";// 所有双引号内容都在 字符串常量池
String str2 = new String("hello");// new 对象都在 堆
System.out.println(str1 == str2);// 比较的还是值相不相同,但是这个值不是字符串而是 引用值
System.out.println(str1.equals(str2));// 比较的是内容是否相同
}
false
true
2. 字符串拼接赋值
public static void main(String[] args) {
String str1 = "hello";
String str2 = "hel" + "lo";// 拼接完后,依旧在常量池中
System.out.println(str1 == str2);
System.out.println(str1.equals(str2));
}
true
true
为什么双引号的拼接操作也是在常量池中完成的呢?
cxf@cxfdeMBP bit % javap -c test
警告: 二进制文件test包含bit.test
Compiled from "test.java"
public class bit.test {
public bit.test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hello
2: astore_1
3: ldc #2 // String hello
5: astore_2
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: aload_2
11: if_acmpne 18
14: iconst_1
15: goto 19
18: iconst_0
19: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V
22: return
}
查看 main 函数中的 ldc 发现 无论是直接赋值也好还是拼接操作,都是直接形成一个整体字符串
检验数字计算操作是否也和字符串拼接一样
public static void main(String[] args) {
int a= 10;
int b = 5 + 5;
}
反汇编如下:
cxf@cxfdeMBP bit % javap -c test
警告: 二进制文件test包含bit.test
Compiled from "test.java"
public class bit.test {
public bit.test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: istore_1
3: bipush 10
5: istore_2
6: return
}
我们发现 5+5操作被直接形成 10 这个整体
3. 字符串和变量的拼接赋值
public static void main(String[] args) {
String str1 = "hello";
String str2 = "hel";
String str3 = str2 + "lo";
System.out.println(str3 == str1);
String str4 = "world";
final String str5 = "wor";
String str6 = str5+"ld";
System.out.println(str6 == str4);
}
false
true
str4, str5, str6全在字符串常量池中完成操作,图中为了简化工作量而省略【和2.字符串拼接赋值的图一样】
反汇编如下:
cxf@cxfdeMBP bit % javap -c test
警告: 二进制文件test包含bit.test
Compiled from "test.java"
public class bit.test {
public bit.test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hello
2: astore_1
3: ldc #3 // String hel
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: aload_2
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: ldc #7 // String lo
19: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: astore_3
26: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
29: aload_3
30: aload_1
31: if_acmpne 38
34: iconst_1
35: goto 39
38: iconst_0
39: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
42: ldc #11 // String world
44: astore 4
46: ldc #12 // String wor
48: astore 5
50: ldc #11 // String world
52: astore 6
54: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
57: aload 6
59: aload 4
61: if_acmpne 68
64: iconst_1
65: goto 69
68: iconst_0
69: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
72: return
}
反汇编分析
- 0: ldc #2 // String hello
2: astore_1
3: ldc #3 // String hel
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."": ()V
13: aload_2
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: ldc #7 // String lo
19: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: astore_3
26: getstatic #9 // Field
发现未加final
修饰的字符串拼接操作被StringBuilder
优化
- 42: ldc #11 // String world
44: astore 4
46: ldc #12 // String wor
48: astore 5
50: ldc #11 // String world
52: astore 6
54: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
加final
修饰的字符串拼接操作直接在字符串常量池中完成拼接
final 修饰原因:
变量:就是在编译的时候不知道里边的值,在运行的时候才知道里边的值
常量:编译的时候就已经确定里边的值
4. 字符串和引用拼接赋值
public static void main(String[] args) {
String str1 = "hello";
String str2 = "hel" + new String("lo");
System.out.println(str2 == str1);
}
false
str2 指向 字符串常量池 中的 “hel” 的引用和 堆 中的字符串 “lo” 完成拼接后自然而然的就处于堆中【堆引用了常量池中的字符串而非是在堆中新建了一个字符串常量池中的字符串,这样JVM可以节约空间】
5. 引用拼接赋值
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hel") + new String("lo");
System.out.println(str2 == str1);
}
false
6. 先创建字符串后引用入池
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hel") + new String("lo");
str2.intern();
System.out.println(str2 == str1);
}
false
- intern: 首先会检查字符串常量池中是否含有需要入池的字符串,如果有则不入池;如果没有则入池
- 由于 str1 的先创建导致字符串常量池已经有 “hello” 导致 str2的
intern
入池操作失败。所以 str2 会继续保存 堆 中的引用值而不是字符串常量池中的引用值
7. 先引用入池后创建字符串
public static void main(String[] args) {
String str2 = new String("hello");
str2.intern();
String str1 = "hello";
System.out.println(str2 == str1);
}
true
str1 在堆中创建的 “hello” 经过
intern
操作入池后,后续的 str2 不会创建相同的 “hello” 字符串而直接指向字符串常量池中的 “hello” 地址。
8. 先引用拼接入池后创建字符串
public static void main(String[] args) {
String str2 = new String("hel") + new String("lo");
str2.intern();
String str1 = "hello";
System.out.println(str2 == str1);
}
true
堆中拼接好,直接指向字符串常量池中,所以 str1 和 str2 的引用相同
9. 先字符串入池在引用拼接入池
public static void main(String[] args) {
String str2 = new String("hel") + new String("lo");
String str1 = "hello";
str2.intern();
System.out.println(str2 == str1);
}
false
10. 变量赋值
public static void main(String[] args) {
String str1 = "hello";
String str2 = str1;
str1 = "world";
System.out.println(str2 == str1);
}
false
str1=“world” 会改变 str1 的指向
3. 不是传引用就能改变值
private static void func(String str){
str = "world";
}
public static void main(String[] args) {
String str1 = "hello";
func(str1);
System.out.println(str1);
}
hello
func(String str) 传进去的是引用,但是并未改变 main 函数中的 str 引用,所以 str 值不变
4. 理解 String 不可变对象
public static void main(String[] args) {
String str1 = "hello";
str1 += "world";
str1 += "!!!";
System.out.println(str1);
}
helloworld!!!
也说明 +
拼接的字符串操作非常低效,会产生很多垃圾
这算是改变字符串吗?
public static void main(String[] args) {
String str = new String("H");
String str1 = "hello";
str1 = "H"+str1.substring(1);// 还是产生了一个新对象,并没有把原来的 "hello" 改为 "Hello"
System.out.println(str1);
}
Hello
其实并不是,操作本质和
+
拼接类似
其实
String str1 = "hello";
也是被 new 过的。查看 new String 的源码会发现:
存储的是一个被 private, final 修饰的 value[] 字段数组。第一反应是无法修改的,但是如果拿到这个类,也就可以修改了。
5. 反射修改字符串
反射:Java 类的一种自省方法。通常情况下:类的细节有时候在类外是看不到的,但是通过反射就可以看到
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str1 = "hello";
Field valueField = String.class.getDeclaredField("value");// // 获取 String 类中的 value 字段. 这个 value 和 String 源码中的 value 是匹配的.
valueField.setAccessible(true);// 将这个字段的访问属性设为 true
char[] vals = (char[]) valueField.get(str1);//把 str1 中的 value 属性获取到.
vals[0] = 'H';//修改 value 的值
System.out.println(str1);
}
Hello