类声明
final
修饰符决定String
是一个常量,是不可继承并且不可变的。String
同时实现了 Serializable
,Comparable
,CharSequence
三个接口。
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
| @Native static final byte LATIN1 = 0; @Native static final byte UTF16 = 1;
@Stable private final byte[] value;
private final byte coder;
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
static final boolean COMPACT_STRINGS; static {COMPACT_STRINGS = true;}
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) { if (len == 0) { this.value = "".value; this.coder = "".coder; return; } 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) { byte[] val = asb.getValue(); int length = asb.length(); if (asb.isLatin1()) { this.coder = LATIN1; 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) { 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
|
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
|
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
|
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()); }
private transient String toStringCache;
|
1 2 3 4 5 6
|
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() { 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) { if (this == anObject) { return true; } 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; }
@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; }
@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; }
public static int hashCode(byte[] value) { int h = 0; for (byte v : value) { h = 31 * h + (v & 0xff); } return h; }
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;
|
上面代码对应的字节码如下:
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 result = new StringBuilder();
for(int i = 0; i < strArray.length; i++) result.append(strArray[i]); return result.toString(); }
|
Java 9
Java9 及之后 的 JDK 版本中,JVM 使用动态调用实现字符串之间的拼接。
变量和常量拼接
对于编译期可以确定值的字符串常量,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";
System.out.println(str3 == str4);
System.out.println(str4 == str5);
System.out.println(str3 == str5);
|
例如上面的代码,str3
和str4
的内存地址并不相同,因为在字符串拼接时没有进行优化。如果将str1
和str2
使用 final 修饰符修饰,那么编译器会自动进行常量折叠,str3
和str4
在内存中的地址相同。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;
System.out.println(str3 == str4);
|
参考
- 水木今山的博客
- String hashCode 方法为什么选择数字31作为乘子
- OpenJDK源码阅读解析:Java11的String类源码分析详解
- JavaGuide-String