on
为什么java中的String是Final或不可变的
该问题在面试过程中经常被提及,考察对String的设计和原理的理解。主要是因为线程安全、性能和安全原因,通过将String修饰为final,不允许被继承,也就避免了对内部实现进行修改的机会。下面对各方面进行描述:
线程安全
因为字符串是不可变的,所以是线程安全的,同一个字符串可以被多个线程共享,不用考虑线程安全而进行同步加锁操作
性能
-
因为字符串使用的非常频繁,如果每次创建一个字符串都在堆上创建一个对象,将对性能和内存造成一定的影响。所以,字符串是分配在字符串常量池(String pool)中,而且是全局共享的,这里常量的意思很明显,分配之后就不可修改了。如果能被随意修改,当定义一个字符串"someString",然后修改这个字符串,将会改变所有引用的值,这样明显将会导致错误。
-
使用HashMap时,使用字符串作为key的几率非常高,而HashMap是基于hash算法的,字符串不可变的话,在分配字符串时,就可以计算出hashcode,以后使用时就不用重复计算了。如果字符串可以被修改,当把字符串放入到HashMap后,然后再对字符串进行修改,将出现不同的hashcode,导致无法获取正确的值。
安全性
-
jdk提供的api很多都使用String作为参数,比如:网络连接、文件操作、数据库url等,如果字符串可修改,在打开某个文件后,再将文件路径修改为其它没有权限可访问的文件就可以绕过安全限制
-
类加载安全。java在加载类时需要指定class的访问路径,如:“java.io.File”,如果字符串可以修改,那么在类加载的时就可以将其修改为自定义的类了,这样将造成很大的漏洞
下面通过代码示例来加深对字符串分配的理解:
String a = "aa";
String b = "aa";
/* 1: */ System.out.println(a == b); // true
String aa = new String(a);
String bb = new String(b);
/* 2: */ System.out.println(aa == bb); // false
/* 3: */ System.out.println(aa.intern() == bb.intern()); // true
/* 4: */ System.out.println(a == aa.intern()); // true
String a3 = a + b;
String a4 = a + b;
/* 5: */ System.out.println(a3 == a4); // false
String a5 = "aa" + "aa";
String a6 = "aa" + "aa";
/* 6: */ System.out.println(a5 == a6); // true
String a7 = a + "1";
String a8 = a + "1";
/* 7: */ System.out.println(a7 == a8); // false
-
因为是字面量的字符串,在分配时就直接存在常量池中了,所以a和b指向相同的常量池地址
-
这里比较好理解,因为是new的两2不同的对象
-
在调用intern时,会先看常量池中是否已经有相同的字符串,判断是使用的
equals(Object)方法,有则直接返回,没有就加到常量池中,然后返回一个指向到分配的常量池地址的引用。所以这里是true,因为"aa"已经在常量池中了 -
这里和第3个操作是一样的,都指向同一个引用地址
-
这里看似相等,其实是不相等的,因为jvm在这里使用的StringBuilder构造新的字符串,结合javap查看字节码就很清晰了
Code:
0: ldc #2 // String aa
2: astore_1
3: ldc #2 // String aa
5: astore_2
6: new #3 // class java/lang/StringBuilder
9: dup
10: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
25: new #3 // class java/lang/StringBuilder
28: dup
29: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
32: aload_1
33: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: aload_2
37: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
40: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
43: astore 4
45: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
48: aload_3
49: aload 4
51: if_acmpne 58
54: iconst_1
55: goto 59
58: iconst_0
59: invokevirtual #8 // Method java/io/PrintStream.println:(Z)V
-
和第一步相同,都是字面量的字符串
-
这里和第5步是一样的,只是这里常量池中有2个,一个是"aa",另外一个是"1",在构造字符串时,使用的是StringBuilder,如下面的字节码
Code:
0: ldc #2 // String aa
2: astore_1
3: ldc #2 // String aa
5: astore_2
6: new #3 // class java/lang/StringBuilder
9: dup
10: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: ldc #6 // String 1
19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: astore_3
26: new #3 // class java/lang/StringBuilder
29: dup
30: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
33: aload_1
34: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
37: ldc #6 // String 1
39: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
42: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
45: astore 4
47: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
50: aload_3
51: aload 4
53: if_acmpne 60
56: iconst_1
57: goto 61
60: iconst_0
61: invokevirtual #9 // Method java/io/PrintStream.println:(Z)V