聊聊Java中的常用类String
# 写在文章开头
String类算是java校招面试题的重灾区,而本文将针对笔者过去的String稿件进行梳理,对一些比较核心的常见的知识点进行梳理分析,希望对你有帮助。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的技术人,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 详解java中的String类
# String、StringBuffer、StringBuilder 的区别
这个问题,我们可以从以下三个角度考虑:
我们从可变性分析角度分析来说,String不可变。而 StringBuffer、StringBuilder都继承自AbstractStringBuilder ,两者的底层的数组value并没有使用private和final修饰,这也就意为着底层存储字符串的容器是可以发生变化的。
对此我们也给出AbstractStringBuilder 源码:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//确定数组空间是否充足,若不充足则动态扩容
ensureCapacityInternal(count + len);
//这里会进行数组拷贝将新字符串存到数组中
str.getChars(0, len, value, count);
count += len;
return this;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
然后我们再从从线程安全性考虑: String的底层的字符串容器是通过final进行修饰,通过不可变保证了操作的线程安全:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
//......
}
2
3
4
5
6
StringBuilder 在进行字符串拼接操作时,调用append方法并没有针对临界资源进行上锁,这意味它的操作是线程不安全的:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//确定数组空间是否充足,若不充足则动态扩容
ensureCapacityInternal(count + len);
//这里会进行数组拷贝将新字符串存到数组中
str.getChars(0, len, value, count);
count += len;
return this;
}
2
3
4
5
6
7
8
9
10
11
而StringBuffer则在append方法上增加synchronized修饰保证了线程安全:
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
2
3
4
5
6
从性能角度来说String是常量每次添加字符串都会将引用指向新的字符串,对应我们给出一段示例代码:
public static void main(String[] args) {
String str = "";
for (int i = 0; i < 100; i++) {
str += i;
}
System.out.printf(str);
}
2
3
4
5
6
7
8
查看编译后的字节码可以看到,其底层本质就是创建StringBuilder然后不断的append完成字符串拼接,最后toString 赋值给String,正是因为这种反复创建对象的开销,导致循环的字符串拼接性能下降:
11 new #3 <java/lang/StringBuilder>
14 dup
15 invokespecial #4 <java/lang/StringBuilder.<init> : ()V>
18 aload_1
19 invokevirtual #5 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
22 iload_2
23 invokevirtual #6 <java/lang/StringBuilder.append : (I)Ljava/lang/StringBuilder;>
26 invokevirtual #7 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
29 astore_1
2
3
4
5
6
7
8
9
而 StringBuilder 和StringBuffer都是基于原有的char数组进行字符串拼接,但前者非线程安全所以没有锁住临界资源,这也就意味着StringBuilder性能上相较于StringBuffer会快10%-15%。
在三者使用场景建议上:
- 操作少量数据拼接,使用
String即可,因为JIT会对这些简单的拼接操作做优化。 - 若是单线程操作大量不可预知的循环或者其他操作的拼接字符串,建议使用
StringBuilder。 - 多线程用
StringBuffer。
# 为什么String 是不可变的?这样设计的原因是什么
从源码可以看到String底层是使用字符数组存储值的,之所以不可变是因为:
value私有且final也没有对外提供字符串操作的方法。- 类设置为
final,子类也无法继承该类对其进行修改。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
2
3
4
5
String为什么设计成不可变的原因:
- 安全:字符串可能会用于存储一些敏感信息,因此保证
String可以保证在常量池中的字符串内容不可被修改,使用时可以无条件信任。 - 性能:将
String设计成不可变时,对于常量字符串我们就可以直接缓存,直接复用从而提升程序整体执行性能。 - 线程安全:设计成不可变可以保证字符串在并发操作时不会修改原有字符串,由此保证线程安全。
# 字符串拼接用“+” 的底层工作机制是什么?
我们给出一段示例代码:
public class StringTest {
@Test
public void addTest() {
String s1 = "hello";
String s2 = "world";
String s3 = "guy";
String s4 = s1 + s2 + s3;
}
}
2
3
4
5
6
7
8
9
10
11
查看其字节码可以看到JIT为了避免大量常量创建,对拼接操作进行了优化,改用StringBuilder进行拼接后toString得到最终的结果:

但是在循环体内使用+=的情况下很可能造成性能灾难。
@Test
public void addTest2() {
String[] arr = {"hello", "world", "guys"};
String string = "";
for (int i = 0; i < arr.length; i++) {
string += arr[i];
}
}
2
3
4
5
6
7
8
9
可以看到在循环体内会不断创建StringBuilder然后通过toString生成新的字符串,然后通过astore_2存入局部变量string中:

来看看我们手动创建StringBuilder 进行拼接和+=由JVM优化后的性能差距:
@Test
public void addTest2() {
String[] arr = new String[1000];
for (int i = 0; i < arr.length; i++) {
arr[i] = String.valueOf(i);
}
long start = System.currentTimeMillis();
String string = "";
for (int i = 0; i < arr.length; i++) {
string += arr[i];
}
long end = System.currentTimeMillis();
System.out.println("使用+=耗时:" + (end - start));
start = System.currentTimeMillis();
StringBuilder builder =new StringBuilder();
for (int i = 0; i < arr.length; i++) {
builder.append(arr[i]);
}
end = System.currentTimeMillis();
System.out.println("使用StringBuilder耗时:" + (end - start));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
输出结果,可以看到StringBuilder比+=快了将近6倍:
使用+=耗时:6
使用StringBuilder耗时:1
2
# String和Object的equals() 有什么区别
String对equals进行了重写,String比较的是字符串的值是否一致:
public boolean equals(Object anObject) {
//若地址一样则直接返回true
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
//遍历并判断字符是否一致
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
而Object比较的则是两者的引用地址是否一致:
public boolean equals(Object obj) {
return (this == obj);
}
2
3
# 字符串常量池是什么,它有什么用?
如下代码所示,Java会将字符串存放在方法区的字符串常量池,后续如有变量需要可以直接复用,关于字符串常量池后文会介绍:
@Test
public void stringConst(){
String s1="s";
String s2="s";
System.out.println(s1==s2);//true
}
2
3
4
5
6
# String str= new String("abc");创建了几个对象
这个问题需要分两种情况,如果字符串池中没有abc字符串,那么就会创建两个对象:
- 创建
String对象s1指向堆区的String对象。 - 在字符串常量池中创建字符串
abc,并让str引用:

如果字符串常量池中有字符串abc,则创建一个String对象然后该对象再引用字符串中的abc:

# intern 方法是什么?有什么用?
大体来说该方法会将字符串值存放到字符串常量池中,并返回该引用。但我们需要针对两个情况来讨论:
- 如果常量池若存在该字面量则直接返回引用。
- 若字面量不存在的时候才会创建并返回新引用。

# 常量intern之间的对比
如下代码所示,我们通过不同方式创建字符串,其中s1、s2还有s4是相等的:
String s1 = "s";
String s2 = s1.intern();
String s3 = new String("s");
String s4 = s3.intern();
System.out.println(s1 == s2);//true
System.out.println(s3 == s4);//false
System.out.println(s1 == s4);//true
2
3
4
5
6
7
8
9
再补充一个神奇的现象,常量字符串进行+=时会被JVM在编译自动优化,例如String s1="a"+"b"实际上会被优化为String s1="ab",所以下面这段intern就会出现下面的结果:
原因也很简单:对于编译期可以确定值的字符串,也就是常量字符串 ,JVM 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
在编译过程中,Javac 编译器会进行一个叫做 常量折叠(Constant Folding) 的代码优化。
这里说到一个叫常量折叠的概念,常量折叠就是将常量表达式计算求值,并用求得的值来替换表达式,然后放到常量表中的一种机制。
String s1 = "s"+"tring";
String s2 = "string";
String s3 = new String("string");
String s4 = s3.intern();
System.out.println(s1 == s2);//true
System.out.println(s3 == s4);//false
System.out.println(s1 == s4);//true
2
3
4
5
6
7
8
9
这一点我们查看字节码文件就得以印证

# final+=的intern操作的比对
final字符串会被JVM优化为常量,所以下面这段代码也会返回true:
final String s1 = "hello";
final String s2 = "world";
String s3 = s1 + s2;
String s4 = "helloworld";
System.out.println(s3 == s4);//true
2
3
4
5
6
7
8
查看字节码得以印证。

# 引用或者函数获取的+=
注意JVM不会对引用和方法这种动态变化的情况进行优化,所以下面这段代码就会返回false。
@Test
public void internTest3() {
final String s1 = "hello";
final String s2 = getStr();
String s3 = s1 + s2;
String s4 = "helloworld";
System.out.println(s3 == s4);//false
}
private String getStr() {
return "world";
}
2
3
4
5
6
7
8
9
10
11
12
13
# String有长度限制吗?
有的,在编译器和运行期是不一样的,字符串类型在编译时需要用CONSTANT_Utf8_info标识字符串常量的值,这个类型长度限制是65535,运行时从构造方法可知String的长度最大是int,,所以对应的最大长度就是Integer.MAX_VALUE:
public int length() {
return value.length;
}
2
3
# char能存储中文吗
可以,char本身占用2个字节,使用Unicode编码,是支持中文的,这一点我们查看String底层的设计即可看到每一个字符串实际上都是存储在char数组上的。
# 字符串常量是什么时候进入到字符串常量池的
- 字面量在编译时加载到
Class常量池,首次创建该常量的字符串时,ldc指令就会解析并将Class常量池中的字符串将在到字符串池中。 - 运行时调用
new String("abc").intern();若字符串常量池没有,则会将手动将字符串添加到字符串常量池中。
# String中intern的原理是什么
- 字面量会在编译期先进入到
Class常量池,然后再创建时解析加载到字符串池中。 - 调用
intern时,如果字符串池有这个字符串直接获取,若没有同样会添加这个字符串对象到字符串常量池中。
# 字符型常量和字符串常量的区别
- 字符类型在
Java中占用两个字节,字符串若干个字节(你可以理解为末尾有个\0)。 - 字符用单引号,字符串用双引号。
# 小结
自此我们简单的梳理了java中字符串的核心知识点,希望对你有帮助。
我是 sharkchili ,CSDN Java 领域博客专家,mini-redis的作者,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。
同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 参考文献
Java基础常见面试题总结(中):https://javaguide.cn/java/basis/java-basic-questions-02.html#string (opens new window)