请点赞关注,你的支持对我意义重大。 🔥 Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook[1] 中。
大家好,我是小彭。
过去两年,我们在掘金平台上发布 JetPack 专栏文章,小彭也受到了大家的意见和鼓励。最近,小彭会陆续搬运到公众号上。
在每种编程语言里,字符串都是一个躲不开的话题,也是面试常常出现的问题。在这篇文章里,我将总结 Java 字符串中重要的知识点 & 面试题 ,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
学习路线图:
\0
为结束符的字符数组字符数组,因此字符串和字符数组在本质上相同,都是一块连续的内存空间,以需要转义 \0
为结束符。C 语言是不关心 char[] 里存储字符的编码方式的,只有通过程序的上下文确定;UTF-16 BE
编码的字符数组(从 Java 9 开始变为字节数组)。其他字符编码输入的字节流在进入 String 时都会被转换为 UTF-16 BE
编码。java.lang.String
public final class String {
private final char value[];
private int hash;
...
}
语言 | 类型 | 存储空间(字节) | 最小值 | 最大值 |
---|---|---|---|---|
Java | char | 2 | 0 | 65535 |
C | char(相当于signed char) | 1 | -128 | 127 |
C | signed char | 1 | -128 | 127 |
C | unsigned char | 1 | 0 | 255 |
Java String 的内存表示本质上是基于 UTF-16 BE
编码的字符数组。UTF-16 是 2 个字节或 4 个字节的变长编码,这意味着即使是 UniCode 字符集的拉丁字母,使用 ASCII 编码只需要一个字节,但是在 String 中需要两个字节的存储空间。
为了优化存储空间,从 Java 9 开始,String 内部将 char 数组改为 byte 数组,String 会判断字符串中是否只包含拉丁字母。如果是的话则采用单字节编码(Latin-1),否则使用 UTF-16 编码。
String.java (since Java 9)
private final byte coder;
static final boolean COMPACT_STRINGS;
static {
COMPACT_STRINGS = true;
}
byte coder() {
return COMPACT_STRINGS ? coder : UTF16;
}
@Native static final byte LATIN1 = 0;
@Native static final byte UTF16 = 1;
不同编码实现的简单区别如下:
编码格式 | 编码单元长度 | BOM | 字节序 |
---|---|---|---|
UTF-8-无BOM | 1 ~ 4 字节 | 无 | 大端序 |
UTF-8 | 1 ~ 4 字节 | EF BB BF | 大端序 |
UTF-16-无BOM | 2 / 4 字节 | 无 | 大端序 |
UTF-16BE(默认) | 2 / 4 字节 | FE FF | 大端序 |
UTF-16LE | 2 / 4 字节 | FF FE | 小端序 |
UTF-32-无BOM | 4 字节 | 无 | 大端序 |
UTF-32BE(默认) | 4 字节 | 00 00 FE FF | 大端序 |
UTF-32LE | 4 字节 | FF EE 00 00 | 小端序 |
关于字符编码的更多内容,见:计算机基础:今天一次把 Unicode 和 UTF-8 说清楚[2]
String 是不可变的,每次操作都会创建新的变量,而另外两个是可变的,不需要创建新的变量;另外,StringBuffer 的每个操作方法都使用 synchronized 关键字保证线程安全,增加了更多加锁 & 释放锁的时间。因此,操作效率的简单排序为:StringBuilder > StringBuffer > String。
String 不可变,所以 String 和 StringBuffer 都是线程安全的,而 StringBuilder 是非线程安全的。
类型 | 操作效率 | 线程安全 |
---|---|---|
String | 低 | 安全(final) |
StringBuffer | 中 | 安全(synchronized) |
StringBuilder | 高 | 非安全 |
《Effective Java》中 可变性最小化原则,阐述了不可变类的规则:
以上规则 String 均满足。
提示: 反射可以破坏 String 的不可变性。
String +
操作符是编译器语法糖,编译后会被替换为 StringBuilder#append(...)
语句,例如:
示例程序
// 源码:
String string = null;
for (String str : strings) {
string += str;
}
return string;
// 编译产物:
String string = null;
for(String str : strings) {
StringBuilder builder = new StringBuilder();
builder.append(string);
builder.append(str);
string = builder.toString();
}
// 字节码:
0 aconst_null
1 astore_1
2 aload_0
3 astore_2
4 aload_2
5 arraylength
6 istore_3
7 iconst_0
8 istore 4
10 iload 4
12 iload_3
13 if_icmpge 48 (+35)
16 aload_2
17 iload 4
19 aaload
20 astore 5
22 new #7 <java/lang/StringBuilder>
25 dup
26 invokespecial #8 <java/lang/StringBuilder.<init>>
29 aload_1
30 invokevirtual #9 <java/lang/StringBuilder.append>
33 aload 5
35 invokevirtual #9 <java/lang/StringBuilder.append>
38 invokevirtual #10 <java/lang/StringBuilder.toString>
41 astore_1
42 iinc 4 by 1
45 goto 10 (-35)
48 aload_1
49 areturn
可以看到,如果在循环里直接使用字符串 +
,会生成非常多中间变量,性能非常差。应该在循环外新建一个 StringBuilder
,在循环内统一操作这个对象。
"abc"
=> 虚拟机首先检查 运行时常量池 中是否存在 "abc",如果存在则直接返回,否则在字符串常量池中创建 "abc" 对象并返回。因此,多次声明使用的是同一个对象;new String("abc")
=> 在编译过程中,Javac 会将 "abc" 加入到 Class 文件常量池 中。在类加载时期,Class 文件常量池会被加载进运行时常量池。在调用 new 字节码指令时,虚拟机会在堆中新建一个对象,并且引用常量池中的 "abc" 对象。如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回常量池中的这个字符串;否则,先将此 String 对象包含的字符串拷贝到常量池中,在常量池中的这个字符串。
从 JDK 1.7 开始,String#intern()
不再拷贝字符串到常量池中,而是在常量池中生成一个对原 String 对象的引用,并返回。
// 举例:
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
// 输出结果为:
JDK1.6以及以下:false false
JDK1.7以及以上:false true
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
我是小彭,带你构建 Android 知识体系。
[1]
GitHub · Android-NoteBook: https://github.com/pengxurui/Android-NoteBook
[2]
计算机基础:今天一次把 Unicode 和 UTF-8 说清楚: https://juejin.cn/post/7126396251322449934