一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第23天,点击查看活动详情。
String被final修饰有啥好处
对于final而言,可以用来修饰方法表示方法不能被重写,用来修饰类表示类不能被继承你只能使用组合去复用它(比如这里的String),用来修饰基本类型表示基本类型不能被重新赋值,用来修饰引用类型表示这个引用的内存地址不能被改变。此外的话就是重排序,final修饰的变量成员需要在初始化完成之后才能被其他线程可见。
注意:针对于final修饰的引用类型,只是引用地址不能被修改,但是对象内部的值是可以修改的,如果你不想被修改需要吧对象内的字段使用final修饰,比如下面要讲的String。
可以看到,String不仅类被final修饰,内部用于存放字符的类成员都被final修饰,也就是说你想要复用String只能去组合使用不能通过继承(保持纯正,比如避免多态带来一些不易发现的问题),同时String一经初始化后此对象就不能修改其内部数据。
那么为啥要这么设计呢?主要从两个方面:缓存、线程安全。
缓存
大家都知道JVM堆内存元空间中存放了字符串常量池(jdk1.8),就是用来存放String的字面变量的,如果String new出来的对象想放入常量池可以通过调用intern()。存放在常量池中的目的就是为了避免重复创建对象被共享使用,不但避免了重复创建带来的内存占用还避免了因为它的创建触发GC,同时因为它是不变的所以它的相关东西比如hashCode不用重新计算。
总之是就是节省内存、提升应用程序整体性能。
线程安全
因为String是不可变的,所以你要对它内容进行修改引用就会变化,如果你把String变量传递给其他线程用不用担心会被其他线程修改产生不一致的问题,因为你传递过去的引用不会发生变化它始终指向的是传入时指向的字符串。
import lombok.SneakyThrows;
public class Demon {
@SneakyThrows
public static void main(String[] args) {
String s = "sd";
test(s);
Thread.sleep(500);
s = "dd";
System.out.println(System.identityHashCode(s) + ":" + s.hashCode());
}
private static void test(String s) {
Thread thread = new Thread(new Runnable() {
@Override
@SneakyThrows
public void run() {
System.out.println(System.identityHashCode(s) + ":" + s.hashCode() + ":" + s);
Thread.sleep(1000);
System.out.println(System.identityHashCode(s) + ":" + s.hashCode() + ":" + s);
}
});
thread.start();
}
}
其实你也会发现Integer、Long等基础类型的封装类都是这样,不允许你改变对象内部数据。
StringBuffer、StringBuilder
前面有说到String被final修饰的一些好处,凡事有利就有弊,就比如经常需要用到字符串拼接,因为String的不可变,导致创建很多中间的字符串常量,但是中间产生的变量没必要一直存放在字符串常量池中,因为很可能拼接拿到结果后中间产生的其实就用不上了。所以就出现了StringBuffer和StringBuilder,它们两个都是可变长的,当然还提供了一些经常处理字符串使用到的API比如字符串翻转等,它们的主要区别是StringBuffer是线程安全的(使用synchronized进行同步),StringBuilder则没有。
String长度有限制吗?是多少?
首先字符串的内容是由一个字符数组 char[] 来存储的,由于数组的长度及索引是整数,且String类中返回字符串长度的方法length() 的返回值也是int ,所以通过查看java源码中的类Integer我们可以看到Integer的最大范围是2^31 -1,由于数组是从0开始的,所以数组的最大长度可以使【0~2^31-1】通过计算是大概4GB。
但是通过翻阅java虚拟机手册对class文件格式的定义以及常量池中对String类型的结构体定义我们可以知道对于索引定义了u2,就是无符号占2个字节,2个字节可以表示的最大范围是2^16 -1 = 65535。其实是65535,但是由于JVM需要1个字节表示结束指令,所以这个范围就为65534了。超出这个范围在编译时期是会报错的,但是运行时拼接或者赋值的话范围是在整形的最大范围。
String常见的面试题及解释
题一
public static void main(String[] args) {
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
System.out.println(str3 == str4);
String str5 = "string";
System.out.println(str3 == str5);
}
执行一下就知道了,第一个打印是false,第二个为true这是为啥呢?可以通过字节码解释,如下:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=6, args_size=1
0: ldc #3 // String str
2: astore_1
3: ldc #4 // String ing
5: astore_2
6: ldc #5 // String string
8: astore_3
9: new #6 // class java/lang/StringBuilder
12: dup
13: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
32: aload_3
33: aload 4
35: if_acmpne 42
38: iconst_1
39: goto 43
42: iconst_0
43: invokevirtual #11 // Method java/io/PrintStream.println:(Z)V
46: ldc #5 // String string
48: astore 5
50: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
53: aload_3
54: aload 5
56: if_acmpne 63
59: iconst_1
60: goto 64
63: iconst_0
64: invokevirtual #11 // Method java/io/PrintStream.println:(Z)V
67: return
LineNumberTable:
line 26: 0
line 27: 3
line 29: 6
line 30: 9
line 31: 29
line 33: 46
line 34: 50
line 35: 67
LocalVariableTable:
Start Length Slot Name Signature
0 68 0 args [Ljava/lang/String;
3 65 1 str1 Ljava/lang/String;
6 62 2 str2 Ljava/lang/String;
9 59 3 str3 Ljava/lang/String;
29 39 4 str4 Ljava/lang/String;
50 18 5 str5 Ljava/lang/String;
因为两个字符串引用+就会转换成StringBuilder进行append,所以地址肯定不一样。"str" + "ing"这样+只是形式上不一样,字节码就是"string"。
题二
import lombok.SneakyThrows;
public class Demon {
public static final String A; // 常量A
public static final String B; // 常量B
static {
A = "ab";
B = "cd";
}
public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
}
是false,原因看字节码也能看出来,static是运行时加载类执行的,所以编译后A和B就只是一个引用,所以同上,是StringBuilder进行append。
题三
public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
String s = A + B; // 将两个常量用+连接对s进行初始化
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
是tru,因为编译时发现A和B已经确定了,所以相当于"ab"+"cd"所以就相当于"abcd"所以就相等。
题四
String hello = "Hello", lo = "lo";
System.out.println((hello == ("Hel" + lo)));
是fasle,lo为引用所以还是使用StringBuilder进行append。
结论
当引用+引用或者引用+字面变量的时候就会处理成StringBuilder append,所以地址肯定不一样。但是如果引用是final有直接初始值赋值的话编译期就能确定字面变量,就相当于字面变量进行+所以会相等。