本文基于 JDK11。
String 类
// java.util.Stringpublic 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 1String str1 = "abc";String str2 = "";String str3 = " def ";System.out.println(str1 == str1.trim()); // trueSystem.out.println(str2 == str2.trim()); // trueSystem.out.println(str3 == str3.trim()); // false
注意,通过反射仍然可以修改字节数组的值(如 code 2)。
// code 2String 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); // falseSystem.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对象。
下面通过几个例子说明:
例一
// Example1String s1 = String.valueOf(1234);String s2 = s1.intern();String s3 = "1234";System.out.println(s1 == s2); // trueSystem.out.println(s1 == s3); // true
创建 s1 时没有生成 "1234" 的字面量,所以常量池还没有 "1234";所以调用 s1.intern() 时会将 s1 加入常量池(具体就是创建一个Entry
加入常量池,将 _literal
指向 s1)。最终的结构如下:
例二
// Example2String s1 = String.valueOf(1234);String s3 = "1234";String s2 = s1.intern();System.out.println(s1 == s2); // falseSystem.out.println(s1 == s3); // falseSystem.out.println(s2 == s3); // true
首先 s1 和 s3 不相等,因为它们和 intern() 无关,在堆中和常量池分别创建不同的对象;调用 s1.intern()
时,常量池中已经存在了 "1234",直接返回它的引用,即 s3。
JDK7之前
由于常量池和堆空间是隔离的区域,就不存在共享同一个String的问题了,调用 intern() 时,如果常量池不存在,会重新创建一个 String 对象。
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) {}
基本使用如下:
// code 1String str1 = "abc";String str2 = "";String str3 = " def ";System.out.println(str1 == str1.trim()); // trueSystem.out.println(str2 == str2.trim()); // trueSystem.out.println(str3 == str3.trim()); // false0
拼接字符串还可以调用 String 的静态方法 join()
,该方法就是利用 StringJoiner 实现的。
// code 1String str1 = "abc";String str2 = "";String str3 = " def ";System.out.println(str1 == str1.trim()); // trueSystem.out.println(str2 == str2.trim()); // trueSystem.out.println(str3 == str3.trim()); // false1
字符串比较
字符串比较使用 equals()
方法,首先判断是否是同一个对象,如果不是,判断是否是 String 类型的对象,如果是,先比较长度是否相等,不相等直接返回 false;长度相等则逐个比较,全部相等返回 true,否则返回 false。
// code 1String str1 = "abc";String str2 = "";String str3 = " def ";System.out.println(str1 == str1.trim()); // trueSystem.out.println(str2 == str2.trim()); // trueSystem.out.println(str3 == str3.trim()); // false2
StringLatin1#equals
的实现如下:
// code 1String str1 = "abc";String str2 = "";String str3 = " def ";System.out.println(str1 == str1.trim()); // trueSystem.out.println(str2 == str2.trim()); // trueSystem.out.println(str3 == str3.trim()); // false3
此外,String 还提供了 equalsIgnoreCase(String)
以忽略大小写的方式比较字符串。