Java的String学习总结

1 String是不可变的

1.1 首先String是不可变的,具体原因主要有以下两点

  1. String中保存字符串的数组被final修饰且是私有属性,而且String没有暴露给外界任何可以修改该字符串的方法;
  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
    1
    2
    3
    4
    5
    6
    public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    ...
    }
    注意:JDK9及后将保存字符串的数组由char改为了byte

详细参见:如何理解 String 类型值的不可变?

1.2 既然String不可变那它是怎样实现修改的呢?

1
2
String s = "1";
s = "2";

上述代码转换为字节码后

上述字节码表示会在字符串常量池中新建一个字符串并加载

2 String拼接

2.1 拼接方式

String有两种拼接字符串方法

  • ++=拼接
  • StringBuilderStringBufferappend(String str)方法,拼接完成后调用他们的toString()方法获得String对象

注意

1
2
3
String 中的对象是不可变的,也就可以理解为常量,线程安全;
StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加
同步锁,所以是非线程安全的。

2.2 ++=拼接的实质

实际上++=是Java专门为String重载的运算符,java本身是不支持运算符重载的,++=也是 Java 中仅有的两个重载过的运算符

1
2
3
String str1 = "s";
String str2 = "zh";
String str4 = str1 + str2;

上述代码对应的字节码为:

1
2
3
String str1 = "s";
String str2 = "zh";
(new StringBuilder()).append(str1).append(str2).toString();

2.png
由字节码看出实际上是由StringBuilder实现拼接的

2.3 注意

有一点注意的是:在循环内最好不要用++=来拼接字符串,因为不会在循环外部创建一个StringBuild对象来进行复用,而是在每次循环是都会创建一个StringBuild对象,造成资源的浪费

2.3.1 +=拼接
1
2
3
4
5
String[] arr = {"he", "llo", "world"};
String str = null;
for (String value : arr) {
str += value;
}

字节码在循环体内部创建StringBuild对象:
Snipaste_2022-10-02_14-49-03.png

2.3.2 循环外部创建StringBuild对象
1
2
3
4
5
String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder();
for (String value : arr) {
s.append(value);
}

字节码只创建了一个StringBuild对象
3.png

3 符串常量池

3.1 什么是字符串常量池

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

1
2
3
4
5
6
7
String str1 = "111";
String str2 = "111";
String str3 = new String("111");
//str1和str2都是常量池中的地址,所以他们是相同的
System.out.println(str1 == str2); //true
//str1是常量池中的地址,str3是堆内存中的地址
System.out.println(str1 == str3); //false

注意:在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生java.lang.OutOfMemoryError: PermGen space错误的。 所以在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。为什么要移动,Perm 区域太小是一个主要原因,当然据消息称 jdk8 已经直接取消了 Perm 区域,而新建立了一个元区域。应该是 jdk 开发者认为 Perm 区域已经不适合现在 JAVA 的发展了。

3.2 String s1 = new String(“111”);这句话创建了几个字符串对象?

答:会创建1个或两个对象

3.2.1 如果常量池中不存在111字符串,那么该句子会创建两个对象,在堆区创建一个字符串对象,在常量池中创建一个对象
1
String str3 = new String("111");

对应的字节码:
5.png
ldc 命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中。

3.2.2 如果常量池中存在111字符串,那么该句子指挥创建一个对象,即在堆区创建一个字符串对象
1
2
String str1 = "111";
String str3 = new String("111");

对应的字节码:
4.png

第7行已经存在就不会再添加了

3.3 Stringintern方法

String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:

  • 如果字符串常量池中保存了对应的字符串,就直接返回该引用
  • 如果字符串常量池中没有保存对应的字符串,那就在常量池中创建一个指向该字符串对象的引用并返回

示例代码(JDK 8) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true

摘自:Java Guide intern 方法有什么作用

更加详细的解释:深入解析String#intern

4 常量折叠

4.1 无final修饰

1
2
3
4
String str1 = "s";
String str2 = "zh";
String str3 = "s" + "zh";
String str4 = str1 + str2;

字节码

1
2
3
4
5
String str1 = "s";
String str2 = "zh";
//直接拼接了
String str3 = "szh";
(new StringBuilder()).append(str1).append(str2).toString();

常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。

并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:

  • 基本数据类型( bytebooleanshortcharintfloatlongdouble)以及字符串常量。
  • final 修饰的基本数据类型和字符串变量
  • 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )

引用的值在程序编译期是无法确定的,编译器无法对其进行优化。

4.2 与final修饰

1
2
3
4
final String str1 = "s";
final String str2 = "zh";
String str3 = "s" + "zh";
String str4 = str1 + str2;

字节码:

1
2
3
4
5
String str1 = "s";
String str2 = "zh";
String str3 = "szh";
String str4 = "szh";
//str3和str4都进行了折叠

final 关键字修改之后的 String 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。

详细介绍:常量折叠

参考


Java的String学习总结
https://huajframe.github.io/2020/09/28/Java基础/Java的String学习总结/
作者
HuaJFrame
发布于
2020年9月28日
许可协议