类声明

final 修饰符决定String是一个常量,是不可继承并且不可变的。String同时实现了 SerializableComparableCharSequence 三个接口。

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

属性字段

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
// 0代表LATIN1编码,1代表UTF16编码,navite注解表示该值可能来自实现JVM的C/C++代码
@Native static final byte LATIN1 = 0;
@Native static final byte UTF16 = 1;

// 字符串的值,核心属性,Stable和final修饰符保证稳定不可变(但是可以通过反射修改)。
// 在jdk8之前使用char[]存储字符串的值
@Stable
private final byte[] value;

// 字节编码标识符,值为LATIN1或UTF16
// 当全部字符在ASCII编码范围内,coder = LATIN1
// 当全部字符不能使用ASCII编码,coder = UTF16
private final byte coder;

// 字符串hash值,默认为0
private int hash;

// 序列化和反序列化使用
private static final long serialVersionUID = -6849794470754667710L;

// 压缩标识符,默认为ture(开启)
// COMPACT_STRINGS = false表示使用UTF16编码
static final boolean COMPACT_STRINGS;
static {COMPACT_STRINGS = true;}

// 只有serialPersistentFields中的字符字段会被序列化,优先级高于transient,默认为空
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];

构造方法

package 方法

char[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
String(char[] value, int off, int len, Void sig) {
// 如果数组长度为0,初始化为空字符串
if (len == 0) {
this.value = "".value;
this.coder = "".coder;
return;
}
// LATIN1:每个byte表示对应char的8个低位,每个char对应一个byte
// UTF16:每个char对应两个byte
// 是否开启压缩
if (COMPACT_STRINGS) {
byte[] val = StringUTF16.compress(value, off, len);
if (val != null) {
this.value = val;
this.coder = LATIN1;
return;
}
}
this.coder = UTF16;
this.value = StringUTF16.toBytes(value, off, len);
}

AbstractStringBuilder

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
String(AbstractStringBuilder asb, Void sig) {
// get value
byte[] val = asb.getValue();
// get length
int length = asb.length();
// 判断asb中是否开启压缩
if (asb.isLatin1()) {
this.coder = LATIN1;
// 复制asb中的值
// Arrays.copyOfRange()底层是通过arraycopy()实现
this.value = Arrays.copyOfRange(val, 0, length);
} else {
// 判断本类是否开启压缩
if (COMPACT_STRINGS) {
byte[] buf = StringUTF16.compress(val, 0, length);
if (buf != null) {
this.coder = LATIN1;
this.value = buf;
return;
}
}
this.coder = UTF16;
this.value = Arrays.copyOfRange(val, 0, length << 1);
}
}

byte[]

1
2
3
4
5
String(byte[] value, byte coder) {
// 直接给value和coder赋值
this.value = value;
this.coder = coder;
}

public 方法

String

1
2
3
4
5
6
7
8
9
10
11
12
13
// 空参数
public String() {
this.value = "".value;
this.coder = "".coder;
}

// 创建一个参数字符串的副本字符串
@HotSpotIntrinsicCandidate
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
  • 由于字符串不可变,所以不推荐使用空参数的构造方法。同样,除非需要 original 的显式副本,否则不要通过复制来新建字符串。

char[]

调用 package 方法将 char[] 转换为字符串,第二个方法检查了数组是否越界。

char[] 再进行修改不会影响到新创建的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// package method: 
// String(char[] value, int off, int len, Void sig) {...}

public String(char value[]) {
this(value, 0, value.length, null);
}

public String(char value[], int offset, int count) {
this(value, offset, count, rangeCheck(value, offset, count));
}

private static Void rangeCheck(char[] value, int offset, int count) {
checkBoundsOffCount(offset, count, value.length);
return null;
}

int[] codePoints

  • 代码点是一个整数,代表是 Unicode 字符集里的位置。Unicode目前的代码点范围是 0x0000-0x10FFFF。目前 Unicode11.0 只有 137374 个字符,还有将近 100 万个空余地址用于添加新字符,每年 Unicode 的字符集都会增加新字符。
  • 如果超出了代码点的有效值范围,会抛出java.lang.IllegalArgumentException,修改代码点数组不会影响创建的新字符串。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /**
    * codePoints: 代码点源数组
    * offset: 子数组的第一个代码点索引
    * count: 子数组的长度
    */

    public String(int[] codePoints, int offset, int count) {
    checkBoundsOffCount(offset, count, codePoints.length);
    if (count == 0) {
    this.value = "".value;
    this.coder = "".coder;
    return;
    }
    if (COMPACT_STRINGS) {
    byte[] val = StringLatin1.toBytes(codePoints, offset, count);
    if (val != null) {
    this.coder = LATIN1;
    this.value = val;
    return;
    }
    }
    this.coder = UTF16;
    this.value = StringUTF16.toBytes(codePoints, offset, count);
    }

bytes[]

一共有 6 个使用bytes[]的构造方法,本质上都是使用StringCoding.decode()

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
/**
* bytes[]: 字节数组
* offset: 开始解码的第一个字节索引
* length: 解码字节个数
* charsetName or charset: 支持的字符集名称,默认使用默认字符集
*/

public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBoundsOffCount(offset, length, bytes.length);
StringCoding.Result ret =
StringCoding.decode(charsetName, bytes, offset, length);
this.value = ret.value;
this.coder = ret.coder;
}


// 判断是否越界
static void checkBoundsOffCount(int offset, int count, int length) {
if (offset < 0 || count < 0 || offset > length - count) {
throw new StringIndexOutOfBoundsException(
"offset " + offset + ", count " + count + ", length " + length);
}
}

public String(byte bytes[], int offset, int length, Charset charset)
public String(byte bytes[], String charsetName)
public String(byte bytes[], Charset charset)
public String(byte bytes[], int offset, int length)
public String(byte[] bytes)

StringBuffer & StringBuilder

1
2
3
4
5
6
public String(StringBuffer buffer) {
this(buffer.toString());
}

// 每次调用toString方法都会更新toStringCache的值,等价于缓存了最后一次的修改值
private transient String toStringCache;
1
2
3
4
5
6
// package method: 
// String(AbstractStringBuilder asb, Void sig) {...}

public String(StringBuilder builder) {
this(builder, null);
}

其他方法

charSequence 接口方法

length()

根据字符串是否压缩来计算字符串的长度。

1
2
3
4
5
6
7
8
9
10
11
public int length() {
// 右移操作
return value.length >> coder();
}

// 判断是否压缩
byte coder() {
// 压缩,则返回0,长度不变
// 不压缩返回1,因为UTF16中一个字符2个byte,所以长度要减半
return COMPACT_STRINGS ? coder : UTF16;
}

charAt()

根据索引获取相对应字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
public char charAt(int index) {
// 根据编码标识符使用不同的方法
if (isLatin1()) {
return StringLatin1.charAt(value, index);
} else {
return StringUTF16.charAt(value, index);
}
}

// 判断编码标识符
private boolean isLatin1() {
return COMPACT_STRINGS && coder == LATIN1;
}

isEmpty()

判断字符串是否为空。

1
2
3
4
public boolean isEmpty() {
// 通过长度判断
return value.length == 0;
}

比较方法

compareTo()

实现 Comparable 接口的 compareTo()。

1
2
3
4
5
6
7
8
9
10
11
12
public int compareTo(String anotherString) {
byte v1[] = value;
byte v2[] = anotherString.value;
// 编码表示相同时
if (coder() == anotherString.coder()) {
return isLatin1() ? StringLatin1.compareTo(v1, v2)
: StringUTF16.compareTo(v1, v2);
}
// 编码标识符不同时
return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
: StringUTF16.compareToLatin1(v1, v2);
}

equals()

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
public boolean equals(Object anObject) {

// 如果两个比较对象,直接返回true
if (this == anObject) {
return true;
}

// 先判断是否都是String
if (anObject instanceof String) {
String aString = (String) anObject;
// 要求编码方式也相同
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}

// StringLatin1.equals()
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}

// StringUTF16.equals()
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
int len = value.length >> 1;
for (int i = 0; i < len; i++) {
if (getChar(value, i) != getChar(other, i)) {
return false;
}
}
return true;
}
return false;
}

hashcode()

计算公式:s[0] * 31^(n-1) + s[1] * 31^(n-2) + … + s[n-1],s[i] 是字符串中的第 i 个字符,n 是字符串的长度。

选择数字 31 的原因:31是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。同时,数字 31 有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) - i,现代的 Java 虚拟机可以自动的完成这个优化。

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
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
}

// StringLatin1.hashCode()
public static int hashCode(byte[] value) {
int h = 0;
for (byte v : value) {
h = 31 * h + (v & 0xff);
}
return h;
}

// StringUTF16.hashCode()
public static int hashCode(byte[] value) {
int h = 0;
int length = value.length >> 1;
for (int i = 0; i < length; i++) {
h = 31 * h + getChar(value, i);
}
return h;
}

String 拼接

Java8

在 Java8 及之前的 JDK 版本中,”+” 是通过创建StringBuilder对象并调用 append() 实现。拼接完成之后调用toString()得到String对象。

1
2
3
4
String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;

上面代码对应的字节码如下:

image.png

Java8 在 for 循环中拼接字符串时,每拼接一次就会创建一个新的StringBuilder对象。为了避免频繁创建新对象,可以在循环开始前创建StringBuilder对象用于在循环当中拼接字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static String getString1(String[] strArray){
String result = "";

for(int i = 0; i < strArray.length; i++)
result += strArray[i];
return result;
}

public static String getString2(String[] strArray){
// 在循环开始前创建StringBuilder
StringBuilder result = new StringBuilder();

for(int i = 0; i < strArray.length; i++)
result.append(strArray[i]);
return result.toString();
}

Java 9

Java9 及之后 的 JDK 版本中,JVM 使用动态调用实现字符串之间的拼接。

image.png

变量和常量拼接

对于编译期可以确定值的字符串常量,JVM 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段也会被存放到字符串常量池(例如str3),这得益于编译器进行的常量折叠。

常量折叠:把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器对源代码做出的极少量优化措施之一。
编译器无法对引用值进行优化,因为这些值在程序编译期间无法确定。

只有编译器在程序编译期可以确定值的常量才会发生常量折叠:

  • 基本数据类型及字符串常量;
  • final修饰的基本数据类型和字符串变量;
  • 字符串通过“+”拼接得到的字符串、基本数据类型之间的算术运算、基本数据类型的位运算;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    String str1 = "str";
    String str2 = "ing";

    // 常量池中的对象
    String str3 = "str" + "ing";
    // 堆上的新对象
    String str4 = str1 + str2;
    // 常量池中的对象
    String str5 = "string";

    // false
    System.out.println(str3 == str4);
    // false
    System.out.println(str4 == str5);
    // true
    System.out.println(str3 == str5);
    例如上面的代码,str3str4的内存地址并不相同,因为在字符串拼接时没有进行优化。如果将str1str2使用 final 修饰符修饰,那么编译器会自动进行常量折叠,str3str4在内存中的地址相同。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    final String str1 = "str";
    final String str2 = "ing";

    // 常量池中的对象
    String str3 = "str" + "ing";
    // 常量池中的对象
    String str4 = str1 + str2;

    // true
    System.out.println(str3 == str4);

参考

  1. 水木今山的博客
  2. String hashCode 方法为什么选择数字31作为乘子
  3. OpenJDK源码阅读解析:Java11的String类源码分析详解
  4. JavaGuide-String