0. 前言
本文旨在介绍 String 类的相关特性以及源码分析,同时还列出了常见的面试题以及示例回答。
相信学习过 Java 的同学都见过这样一段代码:
public static void main(String[] args){
System.out.println("Hello, World!");
}
复制代码
这几乎是每种语言必写的一个示例代码,而在这段 Java 代码中的 Hello, World! 就属于接下来要聊的 String 类。
本文 JDK 源代码版本为 1.8.0_221
1. String 的不可变性
正如 String 类源码文档中所说的那样:String 是常量,它的值在创建后就不能改变。而 String 类的不可变性是由于它被 final 关键字所修饰。
public final class String
implements java.io.Serializable, Comparable, CharSequence{
}
复制代码
1.1 final 关键字
顺便复习一下 Java 基础—— final 关键字的作用:
final 关键字可以被修饰于类、方法和变量
final 关键字强调的是一种不可变性,对于上述三种情况有不同的具体含义:
当 final 被修饰于类时,表明该类不能被继承
当 final 被修饰于方法时,表明该方法不能被重写
当 final 被修饰于变量时,又分两种情况:
若变量属于基本数据类型,那么它的值在初始化之后就不能再更改
若变量是引用类型,那么在初始化之后它就不能再指向另外的对象
所以在 String 类的源码上使用 final 关键字修饰,保证了 String 类不能被扩展。
1.2 String 底层结构
在 String 类中有一个 char 类型的数组,它是用来存储字符串的。
/** The value is used for character storage. */
private final char value[];
复制代码
这个 char 类型的变量也是被 final 关键字修饰,而数组变量属于引用类型,它的值其实就是数组的地址,也就是说 value 数组在初始化之后就不能再指向另外的对象,这也保证了 String 类的不可变性。
但是这里有一点需要额外提到的就是,虽然 value 数组在初始化之后就不能再指向另外的对象,但是它原本指向的地址的内容是可以改变的。
下面是两个直接修改被 final 修饰的变量的示例。
final char[] str = {'1','2','3'};
// 直接赋值将 str 数组的内容修改为{'1','2','4'}
str[2] = '4';
// 通过反射将 str 数组的内容修改为{'1','2','5'}
java.lang.reflect.Array.set(str, 2, '5');
复制代码
第一个方式是直接对 str 数组变量中元素进行赋值,第二个方式是通过反射修改数组内容。所以,要记得的是:被 final 修饰的引用类型变量只是引用地址不能改变。
而为了保证 String 对象的内容,也就是 value 数组的元素不被修改,源码中也没有提供任何修改 value 数组的方法,这也是保证 String 类的不可变性的一种方式。
1.3 使用 final 修饰的原因
首先第一原因是高效,就拿常量池来说,只有变量是不可修改的,才能够被缓存起来,从而实现常量池的功能。
第二个原因是安全, Java 之父 James Gosling 解释过,迫使 String 类设计成不可变的另一个原因是安全,当你在调用其他方法时,比如调用一些系统级操作指令之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,它的内部的值又被改变了,这样有可能会引起严重的系统崩溃问题。
2. String 的创建流程
通常 String 类有两种创建方式,直接赋值和new。
String a = "abc";
String b = new String("abc");
复制代码
2.1 直接赋值
首先来说直接赋值,首先会去常量池中寻找 abc 字符串是否存在,若已存在会将 a 变量直接指向常量池中的值。如果不存在,会在常量池中先创建一个 abc 字符串,然后把 str 指向刚刚创建出来的 abc 字符串。
2.2 new String()
对于使用 new 关键词来创建一个 String 对象的情况,首先虚拟机会在 Java 堆中创建一个 String 对象,然后再去常量池中寻找 abc 字符串是否存在,如果存在,将创建的对象的值指向常量池中已存在的字符串;如果不存在会在常量池中创建一个 abc 字符串,然后把 Java 堆中的对象引用的值指向在常量池中创建的 abc 字符串。
2.3 代码示例
我们写个例子验证一下上面的结论。
public static void main(String[] args){
String a = "abc";
String b = new String("abc");
System.out.println(a==b);
}
复制代码
下面这张图描述了示例代码中对象之间的关系。
第1行代码中使用直接赋值的方式,由于常量池中 abc 字符串已经存在(虚拟机在类加载期间在常量池中创建该字符串),所以 a 变量会直接指向常量池中的 abc 字符串。
第2行代码中使用 new String("abc") 创建了一个对象,所以会在堆中创建一个 value[] 数组对象,而此时常量池已存在 abc 字符串,所以 b 对象的 value[] 数组的引用值会指向常量池中的 abc 字符串。
最后,由于 a 对象的地址相当于常量池中 abc 字符串的地址,而 b 对象的地址相当于 value[] 对象的地址,这两者地址不相同,所以 a 与 b 两个对象不相等。
3. String#intern
除了上述的两种创建方式之外,还有一种方式可以创建 String 对象,那就是 String#intern 方法。
intern 是一个本地方法,它返回的是一个字符串对象,如果常量池已经包含了字符串,那么直接返回该字符串;否则,会将该字符串添加到常量池,并返回该字符串。
在上述示例代码的基础上,再加上一个变量。
public static void main(String[] args){
String a = "abc";
String b = new String("abc");
String c = b.intern();
System.out.println(a==b);
System.out.println(a==c);
System.out.println(b==c);
}
复制代码
a 和 b 的关系我们已经知道了,那么对于使用 intern 方法返回的 c 字符串,会是怎样的情况呢?执行这个方法,我们会发现输出结果是:
false // a!=b
true // a==c
false // b!=c
复制代码
对照 intern 方法的作用,其实也能够知道这个结果:当调用 intern 方法时,如果常量池中存在 abc 字符串,那么直接返回。
在示例代码的场景中,此时 abc 字符串肯定已经存在于常量池中,而且这个字符串是在类加载期间创建的,同时又被赋值给 a 变量,所以 a==c 的判断结果是 true。
4. String、StringBuilder 和 StringBuffer 的区别
这个问题真的被问吐了,网上资料也非常多了,这里就贴几个链接好了。
参考资料