[java复习] 字符串

概要

这一章的复习主要涵盖 String 的属性原理String常量池反编译探究字符串拼接,以及 AbstractStringBuilder 的核心方法实现。这里整理的都是个人觉得比较重要重新认真分析的东西,不一定全部为基础,如果有不准确的地方,还望大佬从评论区指出,感激不尽。

文章中的所有的自定义的测试都在自己的 java-review 仓库中,本文对应的链接:

String

String 的创建

new String()

String s = new String("abc") 执行过程中创建了两个对象

  1. 第一个对象是 "abc" 字符串存储在 常量池
  2. 第二个对象在 JAVA Heap 中的 String 对象

StringBuildler # append

编译器会自动优化,使用 StringBuilder 来拼接创建字符串

使用 StringBuilder 多字符串拼接,来创建字符串的时候只在堆中创建 String 对象,不会直接放到字符串常量池中,调用对象实例的 String # intern 方法

  1. 这个字符串不在字符串常量池中时,才会将这个在堆中的 Stirng 对象引用保存到常量池中并返回这个引用
  2. 常量池中存在这个 String 对象(直接创建的 String 对象或者之前通过 String # intern 保存过来的相同的字符串的引用),直接返回

空串 “” 和 null

  • "" 代表的是一个字符长度为 0 的字符串对象,而不是 null
  • 印证了 equals() 的比较原则,一个对象跟 null 进行比较的的结果永远是 null
1
2
3
4
5
6
7
8
9
10
11
12
13
// test

private static void emptyNull() {
String str = "";
log("str == null: " + (str == null));
log("str.equals(null): " + str.equals(null));

/* Output:

str == null: false
str.equals(null): false
*///:~
}

java 8-9 源码的变化

Java 8 中,String 内部使用 char[] 数组存储数据。

1
2
3
4
5
6
7
8
// java 8 source

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {

/** The value is used for character storage. */
private final char value[];
}

Java 9 之后,String 类的实现改用 byte[] 数组存储字符串,同时使用 byte coder 来标识使用了哪种编码。

1
2
3
4
5
6
7
8
9
10
11
12
// java 12 source

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {

/** The value is used for character storage. */
private final byte[] value;

/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;

}

String 常量池

何时将字符串保存到常量池中以及保存的格式(假设此时常量池中不存在 “123” 字符串)

  1. String str = "123"; 会直接在常量池中创建一个对象
  2. String str = new String("123"); 堆中创建一个对象,常量池中创建一个对象

jdk 6 ~ 7 中的变化

jkd 6jdk 7

  1. String 常量池 从 Perm 区移动到了 Java Heap
  2. String # intern 方法时,如果存在堆中的对象,会直接保存对象的引用,不会在常量池汇中重新创建对象。

String 和 常量池的关系

关于 str1 = new String("123")str = "123" 的两种情况分析

  • str = new String("123"); 分析

    1. 在堆中创建一个 String 对象
    2. 字符串常量池中 没有 对应的字符串,就会将它的 引用放到常量池中
  • str = "123"; 分析

    • 常量池中没有这个 String,直接在常量池中 创建这个字符串对象,并将引用返回
    • 缓存池中有这个 String,直接将引用返回,引用可能为
      1. 常量池中的对象引用
      2. 常量池中保存的 指向堆中 String 对象的引用

关于 StringBuilder 创建字符串的情况分析

从反编译的字节码中可以看到,使用 StringBuilder 来拼接 String 对象引用的时候,不会在常量池中创建对象,调用 String # inter 方法的时候就是将自身放进了常量池中,需要注意的是

StringBuilder 创建字符串测试测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// test

private static void stringBuilderCreateString1() {
/* 只有一个字符串进行拼接 */
String a = new StringBuilder("abc").toString();
String b = new StringBuilder().append("789").toString();

/* 多个字符串进行拼接 */
String c = new StringBuilder("abc").append("def").toString();
String d = new StringBuilder().append("123").append("456").toString();

System.out.println("a == a.intern(): " + (a == a.intern()));
System.out.println("b == b.intern(): " + (b == b.intern()));
System.out.println("c == c.intern(): " + (c == c.intern()));
System.out.println("d == d.intern(): " + (d == d.intern()));

/*///:~

a == a.intern(): false
b == b.intern(): false
c == c.intern(): true
d == d.intern(): true
*/
}

StringBuilder 是否在常量池中创建对象,受其拼接的字符换长度影响

  • 只有一个字符串进行拼接的时候,就会常量池中创建一个对象,堆中创建一个 String 对象,这时候调用 str.intern() 的时候返回的就是常量池创建的那个对象,所以不同 str != str.intern()
    1. new StringBuilder("123").toString();
    2. new StringBuilder().append("123").toString();
  • 多个字符串拼接,不会在常量池中创建对象,这时候调用 str.intern() 的时候,就会将自身的引用放到常量池中并返回,所以相同 str == str.intern()
    1. new StringBuilder("123").append("456").toString();
    2. new StringBuilder().appned("123").append("456").toString();

String intern()

1
2
3
// java 12 source

public native String intern();
  • String # intern 方法的作用会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中并将引用返回
  • 任何 String 对象的可以调用 String # inter 方法,只是对于 str = new String("")str = "" 没有意义,因为已经被放入了常量池中

几个重要的点

  1. + 拼接 String 对象引用 来得到的 String 不会直接放到到常量池中!
  2. String 对象引用可以通过 String # intern 方法将 String 放到常量池中
  3. jdk 7 以后,常量池中保存的为 String 的引用,而非直接在常量池中生成一个对象
  4. 如果使用了 StringBuilder 来创建字符串
    • 只有一个字符串的时候的时候 (new StringBuilder("123") 或者 new StringBuilder().append("123").toString()),就会常量池中创建一个对象,堆中创建一个 String 对象,这时候调用 str.intern() 的时候返回的就是常量池创建的那个对象,所以不同
    • 有多个 appned("") 的时候,不会在常量池中创建对象,这时候调用 str.intern() 的时候,就会将自身的引用放到常量池中并返回,所以相同
1
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
27
28
29
30
31
32
33
34
35
36
37
38
39
// test source

public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);

String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);

/*///:~

jdk6 下: false false
jdk7 下: false true
*/
}


public static void main(String[] args) {
String s1 = new String("2") + new String("3");
s1.intern();
String s2 = "23";
//! s2.intern();
System.out.println("s1 == s2: " + (s1 == s2));

String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);

/*///:~

jdk6 下: false false
jdk7 下: false false
*/
}

jdk 6 中的常量池是放在 Perm 区中的,Perm 区和正常的 JAVA Heap 区域是完全分开的,所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用 String # intern 方法也是没有任何关系的

使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域

String 的不变性

不变性的优点

从上面的 Stirng 的属性中可以知道 hash

不变性的分析

  1. value 数组被声明为 private final,将数组引用锁定,并且 String 中没有对 value 数组进行修改的地方,所以 String 为不可变对象

  2. String 只要发生了修改,就会 new 一个新的 String 对象,这里以 String # toLowerCase 方法为例,这个方法的返回值重新 new 了一个新的对象作为返回值

1
2
3
4
5
6
7
8
// java 12 source

public static String toLowerCase(String str, byte[] value, Locale locale) {

/// ... 忽略以上代码

return new String(result, LATIN1);
}

重载 “+” 和 “+=”

java 不允许操作符重载,但是为 String 对象重载了 + 以及 +=操作符。

String 类型提升

就如同前面的 算数运算 会自动将运算数的类型提高为 运算数的最高类型 一样,只要根据结合律执行遇到了 String 就换转换为 String 类型,在这个环境中,对象引用会自动调用 String toString() 方法返回一个字符串用于拼接。

表达式会根据操作符的结合律来进行执行,当遇到第一个 String 类型,就将当前的类型直接提升到 String,即当前变为 String 环境

  • String 环境中的对象引用都会调用被引用对象的 Object # toString 来得到一个 String 用于拼接字符串

  • str += obj + a * b;

    1. 先执行 a * b 此时的结果的为 int 类型
    2. 执行 obj + ret,此时 obj对象引用a * b 的结果为 intString 环境报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// test source

private static void addOperator() {
int a = 3, b = 4;
Object obj = new Object();
String str = "";
str += a * b;
str += obj;
//! str += obj + a * b; // 优先级: * > + > +=
str += a * b + "" + obj;
System.out.println("str: " + str);

///:~ str: 12
}

“+” 字符串拼接分析

拼接 字面量字符串String 对象引用 在反编译的结果中,并不是完全相同

  • 连续的字面量字符串 会如同反编译代码的 33 行,将连续的字面量字符串使用 ldc 一次性完成加载
  • 存在 String 对象引用的拼接都会先 new 一个 StringBuilder 对象并完成初始化,使用 StringBuilder # append 来拼接单个 String 或者是连续的字面量字符串
  • 需要注意的是,编译器会为 每个拼字符串式 创建一个 StringBuilder 对象,如果将 += 操作符用于了 for 循环中就会每经过一次循环体,就会创建新的 StringBuilder 对象,

所以对于 for 循环拼字符串就不能直接使用 “+=”,因为这样还是需要使用 StringBuilder 来代替 String + +/+=

测试代码及反编译字节码

1
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
String a = "who";
String b = " are";
String c = " you?";

String str1 = a + b + c;

String str2 = "who" + " are" + " you";
/* "who" + " are" + " you" 拼接的反编译字节码 */
33: ldc #20 // String who are you /* 连续的字面量字符串 会通过 ldc 一次性装载 */
35: astore 5


String str3 = a + " are" + " you";
/* a + " are" + " you" 拼接的反编译字节码 */
37: new #2 // class java/lang/StringBuilder /* 拼接串中存在 String 引用,就会先创建一个 StringBuilder 提高效率 */
40: dup
41: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
44: aload_1
45: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
48: ldc #21 // String are you
50: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
53: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
56: astore 6


String str4 = "who" + b + " you";
/* */
58: new #2 // class java/lang/StringBuilder
61: dup
62: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
65: ldc #17 // String who
67: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
70: aload_2
71: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
74: ldc #22 // String you
76: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
79: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
82: astore 7


String str5 = "who" + " are" + c;

测试什么时候创建新的 StringBuilder 对象

这里用来测试字符串的拼接那种情况会触发创建 StringBuilder 对象,以及连续的 str += "" 调用是使用同一个 StringBuilder 对象? 还是会每条语句都会创建新的 StringBuilder 对象?

  1. 对于纯字面量的字符串赋值表达式 (= 赋值),不会创建 StringBuilder 对象,字面量字符串直接装载,String 会创建 String 对象
  2. 对于存在了 String 对象引用的表达式中(如 str += "", 或者 str = str1 + "") 都会创建新的 StringBuilder 对象,一条表达式一个 StringBuilder 对象,也就证明了为什么 for 循环不推荐使用 str += "" 来进行延长字符串
1
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// test source

public class ByteCode1 {

public static void main(String[] args) {
String a = " you?"; // a 拼接 (1)=> 不创建 StringBuilder 对象

String str = "who" + " are"; // str 拼接 (1)=> 不创建新的 StringBuilder 对象

str += a; // str 拼接 (2)=> += 为一条字符串拼接表达式,且 str 为 String 类型,创建第 1 个 StringBuilder

a += "How?"; // a 拼接(2)=> 创建第 2 个 StringBuilder

str += " And"; // str 拼接 (3)=> 创建第 3 个 StringBuilder

str += " I am ok!"; // str 拼接 (4) => 创建第 4 个 StringBuilder

str = new String("new String()"); // 使用 new 创建是否使用 StringBuilder? x,非拼接,不使用 StringBuilder

System.out.println(str);
}
}

// Decompile source

Compiled from "ByteCode1.java"
public class com.example.review.string.ByteCode1 {
public com.example.review.string.ByteCode1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
//=> a 拼接 (1)
/* 1. 将 "you?" 和 "who are" 保存到了 String 常量池 中 */
0: ldc #2 // String you?
2: astore_1

//=> str 拼接 (1)
3: ldc #3 // String who are
5: astore_2


//=> str 拼接 (2)
/* 2. 创建第 1 个 StringBuilder 对象来拼接 str,连接 apppend("who are")
.append(" you?") */
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: aload_2
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_1
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_2


//=> a 拼接(2)
/* 2. 创建第 2 个 StringBuilder 对象来拼接 a, append(" you?")
.append(" How?") */
25: new #4 // class java/lang/StringBuilder
28: dup
29: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
32: aload_1
33: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: ldc #8 // String How?
38: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
41: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
44: astore_1

//=> str 拼接 (3)
/* 3. 创建第 3 个 StringBuilder 对象来拼接 str, append("who are you?")
.append(" And") */
45: new #4 // class java/lang/StringBuilder
48: dup
49: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
52: aload_2
53: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
56: ldc #9 // String And
58: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
61: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
64: astore_2

//=> str 拼接 (4)
/* 4. 创建第 4 StringBuilder 对象来拼接 str,append("who are you? And")
.append(" I am ok!") */
65: new #4 // class java/lang/StringBuilder
68: dup
69: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
72: aload_2
73: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
76: ldc #10 // String I am ok!
78: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
81: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
84: astore_2

/* 5. 通过 new String() 来创建字符串,会直接建立 String 对象 */
85: new #11 // class java/lang/String
88: dup
89: ldc #12 // String new String()
91: invokespecial #13 // Method java/lang/String."<init>":(Ljava/lang/String;)V
94: astore_2
95: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
98: aload_2
99: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
102: return
}

AbstractStringBuilder

String 是不可修改对象,其内部也没有进行修改的方法,而 AbstractStringBuilder 就是可以修改的 String 的抽象类,他的两个导出类

  • StringBuilder -> 线程不安全

  • StringBuffer -> 线程安全,方法使用了 synchronized 加锁

关于 StringBuilder 来创建的字符串和常量池的关系在上面的 String Pool 中有详细的说明

appned() 方法

StringBuilder # append 方法只是调用父类方法,父类 AbstractStringBuilder 才是核心,这里面的几个核心方法

  1. String # void getBytes(byte dst[], int dstBegin, byte coder) 这个方法的作用没有权限修饰符,所以权限为 default,所以 无法使用该方法

  2. AbstractStringBuilder # ensureCapacityInternal(int minimumCapacity) 确保内部容量(扩容方法) 的作用就是将数组的长度更新,使用的是 Arrays # byte[] copyOf(byte[] original, int newLength) 这个方法的作用就是为了 截断 或者 扩展 数组。

1
2
3
4
5
6
7
8
9
10
11
12
// java 12 source

public class Arrays {

public static byte[] copyOf(byte[] original, int newLength) {
byte[] copy = new byte[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

}
  1. 通过返回 this 实现了串行调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// java 12 source

public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, Comparable<StringBuilder>, CharSequence {

@Override
@HotSpotIntrinsicCandidate
public StringBuilder append(String str) {
super.append(str);
return this; // 返回当前对象引用
}

}

源码分析

1
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// java 12 source

abstract class AbstractStringBuilder implements Appendable, CharSequence {

public AbstractStringBuilder append(String str) {
if (str == null) {
return appendNull();
}
int len = str.length();
ensureCapacityInternal(count + len); // 1. 确认容量
putStringAt(count, str); // 2. 追加上 str
count += len; // 3. 更新长度
return this;
}


/* 1.1 确认内部扩容,即扩容,新的容量大小有 coder 而定 */
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
int oldCapacity = value.length >> coder;
if (minimumCapacity - oldCapacity > 0) {
value = Arrays.copyOf(value, // copyOf()
newCapacity(minimumCapacity) << coder);
}
}

/* 1.2 根据 coder 的值返回新的容量的大小 */
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = value.length >> coder;
int newCapacity = (oldCapacity << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
int SAFE_BOUND = MAX_ARRAY_SIZE >> coder;
return (newCapacity <= 0 || SAFE_BOUND - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}

/* 2.1 调用 String.getBytes(byte dst[], int dstBegin, byte coder),
追加字符串 */
private final void putStringAt(int index, String str) {
if (getCoder() != str.coder()) {
inflate();
}
str.getBytes(value, index, coder);
}

}


public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {

/* 2.2 根据 coder,将当前 String 的值写到 dst[]上,从 dstBegin 位开始,
这个方法没有权限修饰符,为默认的包权限,所以无法测试 */
void getBytes(byte dst[], int dstBegin, byte coder) {
if (coder() == coder) {
System.arraycopy(value, 0, dst, dstBegin << coder, value.length);
} else { // this.coder == LATIN && coder == UTF16
StringLatin1.inflate(value, 0, dst, dstBegin, value.length);
}
}

}

insert() 方法

AbstractStringBuilder # insert 方法比起 AbstractStringBuilder # append 方法就是多了一步: 将插入位置后的字符都向后移动 n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// java 12 source

abstract class AbstractStringBuilder implements Appendable, CharSequence {

public AbstractStringBuilder insert(int offset, String str) {
checkOffset(offset, count);
if (str == null) {
str = "null";
}
int len = str.length();
ensureCapacityInternal(count + len); // 1. 扩容
shift(offset, len); // 2. offset 开始向后移动 n 位
count += len; // 3. 更新长度
putStringAt(offset, str); // 4. 连接字符串
return this;
}

/* 将 offset 偏移量到末尾的所有字符,向后拷贝到 dstBegin == offset + n
将偏移量 offset 后的 n 个位置空出来 */
private void shift(int offset, int n) {
System.arraycopy(value, offset << coder,
value, (offset + n) << coder, (count - offset) << coder);
}

}