您正在学习的是试看内容,报名后可学习全部内容 报名课程
人气值 2.9k

Java 基础教程: 手写 JDK String 类

请务必加入微信群,讲座时间会更新,讲座内容会更改,而且优惠信息,讲座资料会在群里发放

一 讲座简介

String作为必考知识点几乎出现在每一场面试中,与其费尽心思的看各种博客,还不如自己撸一个,一劳永逸的解决这个问题。

三 内容大纲

  1. jdk编写者的代码书写方式:精炼,高效,基于场景
  2. 徒手撸出 equal(), indexof(), compare(), subString()
  3. BMP编码,千万别用char
  4. 使用i--运算减少一条寄存指令等
  5. 最完美的字符串翻转

四 面向人群

初、中级后端开发

五 目标

理解、掌握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只能表示utf­16中的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.