String的相关问题都在这里了
本文基于 JDK11。
String 类
// java.util.String
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final byte[] value;
private final byte coder;
private int hash; // Default to 0
static final boolean COMPACT_STRINGS;
static {
COMPACT_STRINGS = true;
}
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
}
value
字节数组存储字符串内容。int hash
缓存字符串的哈希码。
coder
表示字符串编码方式,编码方式有两种,0
表示使用 LATIN1 编码,每个字符占用 1 字节,1
表示使用 UTF16 编码,每个字符占用 2 字节。
用 0/1 表示两种编码,在计算字符串长度时可以直接使用
value.length >> coder
。
COMPACT_STRINGS
表示是否压缩字符串,如果为 false,将只会使用 UTF16 编码方式;JVM 默认该属性为 true,大部分场景使用 1 字节就够了。可以使用 -XX:-CompactStrings 参数来对此功能进行关闭。
CASE_INSENSITIVE_ORDER
是一个 Compactor
,定义了字符串比较的规则(大小写敏感)。
String的不可变性
为什么不可变
- String 中的字节数组被
final
修饰,所以不能修改value
的引用 - 字节数组还是
private
属性而且没有暴露任何修改value
数组的方法 - String 本身是被
final
修饰的,无法被继承,从而避免了子类覆盖父类方法的行为 - String 中对字符串处理的方法(包括
+=
)都会返回新的 String 对象并返回,不会影响原来的字符串
只有当成功地对字符串进行了相关操作,才会返回新的 String 对象。比如 trim()
是去除首尾的空格符,如果字符串的长度为0或首尾没有空格符,会返回原对象(如 code 1)。
// code 1
String str1 = "abc";
String str2 = "";
String str3 = " def ";
System.out.println(str1 == str1.trim()); // true
System.out.println(str2 == str2.trim()); // true
System.out.println(str3 == str3.trim()); // false
注意,通过反射仍然可以修改字节数组的值(如 code 2)。
// code 2
String str = new String("abc");
Field field = str.getClass().getDeclaredField("value");
field.setAccessible(true);
byte[] value = (byte[]) field.get(str);
value[0] = 'd';
System.out.println(str); // sout: dbc
不可变的好处?
- 线程安全;
- 配合字符串常量池,如果 String 可变,那么一个引用改变就会影响其他的引用,常量池也就失去了其复用字符串的作用;
- 缓存哈希码,哈希码只需要计算一次;所以 String 作为
Map
的键可以提高性能。
字符数组改为字节数组的好处
在大多数场景下,1 个字节表示字符就足够了,所以 JDK9 将字符数组改为字节数组,并配合新增的 coder
属性,可以减少 String 的空间占用。
字符串常量池
作用
String 是使用频率很高,为了复用和提高性能,引入了字符串常量池。字符串常量池位于堆中,JDK7 之前在方法区中。
字符串常量池的结构
字符串常量池可以看出一个哈希表,表中每一个Entry包含字符串的 hashCode 和一个指向String对象的指针(_literal
,相当于是String对象的地址)。
所以如果字符串常量池中包含一个字面量时,结构如下图所示。
String的创建
创建 String 有两种方式:字面量
和new
。
当使用字面量时,如果常量池中已存在,直接返回引用(_literal
的指向);如果不存在,就将该字符串加入常量池(即创建一个Entry
)再返回引用。
当使用 new("xxx")
创建时,如果常量池中已经存在,在堆中创建一个 String 对象,并返回该对象的引用;如果常量池中不存在,则先在常量池中国创建该字符串,再创建 String 对象,返回该对象的引用。
所以,使用字面量方式引用同一个字符串,它们的引用一定相等。
String str1 = "abc";
String str2 = "abc";
System.out.println(str1 == str2); // true
使用 new
创建的对象即使字符串是相等的,引用也不相同,与字符串常量池的引用也不相同。
String str1 = "abc";
String str2 = new String("abc");
String str3 = new String("abc");
System.out.println(str1 == str2); // false
System.out.println(str2 == str3); // false
🛎️ 注意 对于多个字面量
+=
的情况,编译时会对其优化,最终只会生成要给字面量。比如String s = "1" + "2"
,优化后等价于String s = "12"
。
除了以上两种主要的创建方式,创建一个String还可以通过其他的写法,这些写法都不包含字面量,不会在常量池中创建对象。这些写法包括不限于如下所示。
// 创建一个内容为"111"的字符串对象
String s1 = new String(new byte[]{49,49,49});
String s2 = String.valueOf(111);
String s3 = String.valueOf(1) + String.valueOf(11);
String s4 = String.format("%d%d%d", 1,1,1);
String s5 = new String("11") + "1";
intern() 方法
调用 intern() 时,如果该字符串在常量池中存在,那么返回常量池中的引用;如果常量池中不存在,则创建并返回对象引用。
🛎️ 注意 实际上,这里说不存在就创建并不准确,这是因为字符串常量池在 JDK7 以后从永久代移到了堆中,位置的变化就会导致 intern() 效果的变化。
JDK7及以后
牢记一点:由于字符串常量池就在堆中,所以会尽可能的避免重复创建,如果字符串常量池中不存在,就将 _literal
指向已经在堆中的 String 对象,而不是重新创建一个String对象。
下面通过几个例子说明:
- 例一
// Example1
String s1 = String.valueOf(1234);
String s2 = s1.intern();
String s3 = "1234";
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
创建 s1 时没有生成 "1234" 的字面量,所以常量池还没有 "1234";所以调用 s1.intern() 时会将 s1 加入常量池(具体就是创建一个Entry
加入常量池,将 _literal
指向 s1)。最终的结构如下:
- 例二
// Example2
String s1 = String.valueOf(1234);
String s3 = "1234";
String s2 = s1.intern();
System.out.println(s1 == s2); // false
System.out.println(s1 == s3); // false
System.out.println(s2 == s3); // true
首先 s1 和 s3 不相等,因为它们和 intern() 无关,在堆中和常量池分别创建不同的对象;调用 s1.intern()
时,常量池中已经存在了 "1234",直接返回它的引用,即 s3。
JDK7之前
由于常量池和堆空间是隔离的区域,就不存在共享同一个String的问题了,调用 intern() 时,如果常量池不存在,会重新创建一个 String 对象。
String/StringBuilder/StringBuffer
区别 | String | StringBuilder | StringBuffer |
---|---|---|---|
线程安全 | 是 | 否 | 是 |
可变性 | 是 | 否 | 否 |
性能 | 低 | 高 | 高 |
String 的线程安全是因为它的不可变性,StringBuffer 线程安全是因为所有方法都是 synchronized
方法。
StringBuilder 和 StringBuffer 中的字节数组不是 final
修饰,对字符串操作时直接修改 value
属性,操作方法返回的都是 this
,所以它们可以使用更方便的链式调用。
以 StringBuilder#append
为例,
public StringBuilder append(String str) {
super.append(str);
return this; // 返回自己
}
由于 String 每次修改都会创建新的对象,所以性能更低。不建议在循环中频繁使用 String 的 +=
或其他操作。
总结,字符串修改较少时,可以使用 String;单线程下大量修改使用 StringBuilder;多线程下大量修改使用 StringBuffer。
StringJoiner
StringJoiner 可以方便地进行字符串拼接。有两个构造函数,必须指定一个分割符 delimiter
,也可以指定拼接完成后加入的前缀和后缀。StringJoiner 不是线程安全的。
public StringJoiner(CharSequence delimiter) {}
public StringJoiner(CharSequence delimiter,CharSequence prefix,CharSequence suffix) {}
基本使用如下:
StringJoiner joiner = new StringJoiner(".");
String localhost = joiner.add("127").add("0").add("0").add("1").toString();
System.out.println(localhost); // sout: 127.0.0.1
拼接字符串还可以调用 String 的静态方法 join()
,该方法就是利用 StringJoiner 实现的。
public static String join(CharSequence delimiter, CharSequence... elements) {
// ignore some code
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
字符串比较
字符串比较使用 equals()
方法,首先判断是否是同一个对象,如果不是,判断是否是 String 类型的对象,如果是,先比较长度是否相等,不相等直接返回 false;长度相等则逐个比较,全部相等返回 true,否则返回 false。
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;
}
StringLatin1#equals
的实现如下:
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;
}
此外,String 还提供了 equalsIgnoreCase(String)
以忽略大小写的方式比较字符串。
转载自:https://juejin.cn/post/7097845709541474334