# 类声明
final 修饰符决定String是一个常量,是不可继承并且不可变的。String同时实现了 Serializable,Comparable,CharSequence 三个接口。
public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence
# 属性字段
// 0代表LATIN1编码,1代表UTF16编码,navite注解表示该值可能来自实现JVM的C/C++代码@Native static final byte LATIN1 = 0;@Native static final byte UTF16 = 1;// 字符串的值,核心属性,Stable和final修饰符保证稳定不可变(但是可以通过反射修改)。// 在jdk8之前使用char[]存储字符串的值@Stableprivate final byte[] value;// 字节编码标识符,值为LATIN1或UTF16// 当全部字符在ASCII编码范围内,coder = LATIN1// 当全部字符不能使用ASCII编码,coder = UTF16private final byte coder;// 字符串hash值,默认为0private 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[]
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
String(AbstractStringBuilder asb, Void sig) {// get valuebyte[] val = asb.getValue();// get lengthint 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[]
String(byte[] value, byte coder) {// 直接给value和coder赋值this.value = value;this.coder = coder;}
| public 方法
* String
// 空参数public String() {this.value = "".value;this.coder = "".coder;}// 创建一个参数字符串的副本字符串@HotSpotIntrinsicCandidatepublic String(String original) {this.value = original.value;this.coder = original.coder;this.hash = original.hash;}
- 由于字符串不可变,所以不推荐使用空参数的构造方法。同样,除非需要 original 的显式副本,否则不要通过复制来新建字符串。
* char[]
调用 package 方法将 char[] 转换为字符串,第二个方法检查了数组是否越界。
对
char[]再进行修改不会影响到新创建的字符串
// 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,修改代码点数组不会影响创建的新字符串。 ```java /**- 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); }
<a name="VZxGQ"></a>### * bytes[]一共有 6 个使用`bytes[]`的构造方法,本质上都是使用`StringCoding.decode()`。```java/*** 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
public String(StringBuffer buffer) {this(buffer.toString());}// 每次调用toString方法都会更新toStringCache的值,等价于缓存了最后一次的修改值private transient String toStringCache;
// package method:// String(AbstractStringBuilder asb, Void sig) {...}public String(StringBuilder builder) {this(builder, null);}
# 其他方法
| charSequence 接口方法
* 1 length()
根据字符串是否压缩来计算字符串的长度。
public int length() {// 右移操作return value.length >> coder();}// 判断是否压缩byte coder() {// 压缩,则返回0,长度不变// 不压缩返回1,因为UTF16中一个字符2个byte,所以长度要减半return COMPACT_STRINGS ? coder : UTF16;}
* 2 charAt()
根据索引获取相对应字符。
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;}
* 3 isEmpty()
判断字符串是否为空。
public boolean isEmpty() {// 通过长度判断return value.length == 0;}
| 比较方法
* 1 compareTo()
实现 Comparable 接口的 compareTo()。
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);}
* 2 equals()
public boolean equals(Object anObject) {// 如果两个比较对象,直接返回trueif (this == anObject) {return true;}// 先判断是否都是Stringif (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()@HotSpotIntrinsicCandidatepublic 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()@HotSpotIntrinsicCandidatepublic 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;}
* 3 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 虚拟机可以自动的完成这个优化。
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对象。
String str1 = "he";String str2 = "llo";String str3 = "world";String str4 = str1 + str2 + str3;
上面代码对应的字节码如下:
Java8 在 for 循环中拼接字符串时,每拼接一次就会创建一个新的StringBuilder对象。为了避免频繁创建新对象,可以在循环开始前创建StringBuilder对象用于在循环当中拼接字符串:
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){// 在循环开始前创建StringBuilderStringBuilder 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修饰的基本数据类型和字符串变量;- 字符串通过“+”拼接得到的字符串、基本数据类型之间的算术运算、基本数据类型的位运算; ```java String str1 = “str”; String str2 = “ing”;
String str3 = “str” + “ing”; // 常量池中的对象 String str4 = str1 + str2; // 堆上的新对象 String str5 = “string”; // 常量池中的对象
System.out.println(str3 == str4); // false System.out.println(str4 == str5); // false System.out.println(str3 == str5); // true
例如上面的代码,`str3`和`str4`的内存地址并不相同,因为在字符串拼接时没有进行优化。如果将`str1`和`str2`使用 final 修饰符修饰,那么编译器会自动进行常量折叠,`str3`和`str4`在内存中的地址相同。```javafinal String str1 = "str";final String str2 = "ing";String str3 = "str" + "ing"; // 常量池中的对象String str4 = str1 + str2; // 常量池中的对象System.out.println(str3 == str4); // true
