Java 基础教程: 手写 JDK String 类
请务必加入微信群,讲座时间会更新,讲座内容会更改,而且优惠信息,讲座资料会在群里发放
一 讲座简介
String作为必考知识点几乎出现在每一场面试中,与其费尽心思的看各种博客,还不如自己撸一个,一劳永逸的解决这个问题。
三 内容大纲
- jdk编写者的代码书写方式:精炼,高效,基于场景
- 徒手撸出 equal(), indexof(), compare(), subString()
- BMP编码,千万别用char
- 使用i--运算减少一条寄存指令等
- 最完美的字符串翻转
四 面向人群
初、中级后端开发
五 目标
理解、掌握JDK的底层原理
六 知识点
1 i++ vs i--
String源码的第985行,equals方法中
while (n--!= 0) {
if (v1[i] != v2[i])
return false;
i++;
}
这段代码是用于判断字符串是否相等,但有个奇怪地方是用了i--!=0来做判断,我们通常不是用i++么?为什么用i--呢?而且循环次数相同。原因在于编译后会多一条指令:
i-- 操作本身会影响CPSR(当前程序状态寄存器),CPSR常见的标志有N(结果为负), Z(结果为0),C(有进位),O(有溢出)。i > 0,可以直接通过Z标志判断出来。
i++操作也会影响CPSR(当前程序状态寄存器),但只影响O(有溢出)标志,这对于i < n的判断没有任何帮助。所以还需要一条额外的比较指令,也就是说每个循环要多执行一条指令。
简单来说,跟0比较会少一条指令。所以,循环使用i--,高端大气上档次。
2 不要用char
char在Java中utf-16编码,是2个字节,而2个字节是无法表示全部字符的。2个字节表示的称为 BMP,另外的作为high surrogate和 low surrogate 拼接组成由4字节表示的字符。比如String源码中的indexOf:
//这里用int来接受一个char,方便判断范围
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1.
return -1;
}
//在Bmp范围
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
//否则转到四个字节的判断方式
return indexOfSupplementary(ch, fromIndex);
}
}
所以Java的char只能表示utf16中的bmp部分字符。对于CJK(中日韩统一表意文字)部分扩展字符集则无法表示。
例如,下图中除Ext-A部分,char均无法表示。
此外还有一种说法是要用char,密码别用String,String是常量(即创建之后就无法更改),会保存到常量池中,如果有其他进程可以dump这个进程的内存,那么密码就会随着常量池被dump出去从而泄露,而char[]可以写入其他的信息从而改变,即是被dump了也会减少泄露密码的风险。
但个人认为你都能dump内存了难道是一个char能够防范的住的?除非是String在常量池中未被回收,而被其它线程直接从常量池中读取,但恐怕也是非常罕见的吧。
3 完美的字符串翻转
参考StringBuilder,直接用char来判断是非常快的,但是要特别注意对增补字符集的判断
public AbstractStringBuilder reverse() {
boolean hasSurrogates = false;
int n = count - 1;
// 使用移位操作达到最大速度,相当于j = (n-1)/2
for (int j = (n-1) >> 1; j >= 0; j--) {
int k = n - j;
char cj = value[j];
char ck = value[k];
value[j] = ck;
value[k] = cj;
//如果是增补字符集,那么需要对所有的增补字符集进行调换
if (Character.isSurrogate(cj) ||
Character.isSurrogate(ck)) {
hasSurrogates = true;
}
}
if (hasSurrogates) {
// 最后再循环一遍对所有的增补字符集进行调换
reverseAllValidSurrogatePairs();
}
return this;
}
/** Outlined helper method for reverse() */
private void reverseAllValidSurrogatePairs() {
for (int i = 0; i < count - 1; i++) {
char c2 = value[i];
if (Character.isLowSurrogate(c2)) {
char c1 = value[i + 1];
if (Character.isHighSurrogate(c1)) {
value[i++] = c1;
value[i] = c2;
}
}
}
}
4 final语义
如果一个类包含final字段,且在构造函数中初始化,那么正确的构造一个对象后,final字段被设置后对于其它线程是可见的。
这里所说的正确构造对象,意思是在对象的构造过程中,不允许对该对象进行引用,不然的话,可能存在其它线程在对象还没构造完成时就对该对象进行访问,造成不必要的麻烦。
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x;
int j = f.y;
}
}
}
上面这个例子描述了应该如何使用final字段,一个线程A执行reader方法,如果f已经在线程B初始化好,那么可以确保线程A看到x值是3,因为它是final修饰的,而不能确保看到y的值是4。
如果构造函数是下面这样的:
public FinalFieldExample() { // bad!
x = 3;
y = 4;
// bad construction - allowing this to escape
global.obj = this;
}
这样通过global.obj拿到对象后,并不能保证x的值是3.