零、Java 中 String 的源码

学习笔记来自

  • 慕课网专栏 —— 面试官系统精讲 Java 源码及大厂真题
  • 拉钩教育 —— Java面试真题及源码 34讲

    一、开篇词

    我们在学习 Java SE 的过程中,经常会用到这个类型,我们只会觉得用这个很方便。可以直接申明一个字符串类型。但是大家有没想过这个是怎么来的呢?

在学 C语言的时候,如果没有接触过 C++ 的同学,对于字符串处理的时候一般都是使用一个 char 类型的字符数组来解决。没错 String 的核心就是这样的。接下里我们来看一看 String 的源码是什么样子的呢?

二、String 类型的不可变性

我们写一个示例看看

  1. package test;
  2. public class Test {
  3. public static void main(String[] args) {
  4. String a= "Hello";
  5. a = "wwww";
  6. System.out.println(a);
  7. }
  8. }

我们打上两个断点看一看。

image.png
image.png

从打断点的结果可以看出,当我们将字符串的值改为 www ,的时候,它的地址的引用就发生了改变,同时里面存储的字符也发生了变化,同时我们从打断点的结果看出,String 中的每个字符都是一个一个存储的。所以我们可以猜测两个结论

  • String 被 final 修饰,是不能直接修改的,需要改变地址的引用才能换一个新的值
  • String 中的字符使用字符数组存储的。

我们看下 String 的源码是什么样子的。

  1. public final class String
  2. implements java.io.Serializable, Comparable<String>, CharSequence {
  3. /** The value is used for character storage. */
  4. private final char value[];

现在看看 String 的部分源码,和刚才猜测的相差不大

  • String 是不可变类型,不可变就代表被 final 关键字修饰,也就是说 String 类不可能被继承,也就是说对 String 操作的方法,都不会被继承重写
  • String 中保存数据的是一个 char 的数组 value。我们发现 value 也是被 final 修饰的,可以这么说,value 一旦被赋值,内存地址是绝对无法修改的,value 的访问修饰符是 private ,外部也访问不到,String 也没有提供对应的 setter 方法。因此 value 一旦产生,就无法被修改。

这两点就是使 String 不变性的原因,充分利用了 final 关键字的特性,以后在开发遇到这种情况,可以采取 String 源码的设计方式加到自己的项目里面。

我们看一个例子

  1. public class Test {
  2. public static void main(String[] args) {
  3. String a= "Hello";
  4. a.replace("H","t"); // 这样写不会有效果,也不会有任何改变
  5. System.out.println(a);
  6. a = a.replace("H","t"); // 分析源码我们知道,我们对 String 的操作会引用一个新的地址,因此需要换一个新的 String 对象来接收。
  7. System.out.println(a);
  8. }
  9. }

image.png

三、String 中的方法

3.1 String 中的构造方法

有四类构造方法,需要我们注意的是后面两个构造方法,这两类一般都是单独使用的比较多

  1. // String 为参数的构造方法
  2. public String(String original) {
  3. this.value = original.value;
  4. this.hash = original.hash;
  5. }
  6. // char[] 为参数构造方法
  7. public String(char value[]) {
  8. this.value = Arrays.copyOf(value, value.length);
  9. }
  10. // StringBuffer 为参数的构造方法
  11. public String(StringBuffer buffer) {
  12. synchronized(buffer) {
  13. this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
  14. }
  15. }
  16. // StringBuilder 为参数的构造方法
  17. public String(StringBuilder builder) {
  18. this.value = Arrays.copyOf(builder.getValue(), builder.length());
  19. }

3.2 equals() 比较两个字符串是否相等

  1. public boolean equals(Object anObject) {
  2. // 对象引用相同直接返回 true
  3. if (this == anObject) {
  4. return true;
  5. }
  6. // 判断需要对比的值是否为 String 类型,如果不是则直接返回 false, instanceof 是用来判断数据类型的一个方法
  7. if (anObject instanceof String) {
  8. String anotherString = (String)anObject;
  9. int n = value.length;
  10. if (n == anotherString.value.length) {
  11. // 把两个字符串都转换为 char 数组对比
  12. char v1[] = value;
  13. char v2[] = anotherString.value;
  14. int i = 0;
  15. // 循环比对两个字符串的每一个字符
  16. while (n-- != 0) {
  17. // 如果其中有一个字符不相等就 true false,否则继续对比
  18. if (v1[i] != v2[i])
  19. return false;
  20. i++;
  21. }
  22. return true;
  23. }
  24. }
  25. return false;
  26. }

String 类型重写了 equals() 方法,equals() 方法需要传递一个 Object 类型参数值,因此比较字符串时,会先通过比较 instanceof 判断是否为 String 类型,如果不是直接返回 false

3.3 compareTo() 比较两个字符串

compareTo() 方法用于比较两个字符串,返回结果为 int 类型的值,源码如下

  1. public int compareTo(String anotherString) {
  2. int len1 = value.length;
  3. int len2 = anotherString.value.length;
  4. // 获取两个字符串中较短的那一个
  5. int lim = Math.min(len1, len2);
  6. char v1[] = value;
  7. char v2[] = anotherString.value;
  8. int k = 0;
  9. // 对比每一个字符
  10. while (k < lim) {
  11. char c1 = v1[k];
  12. char c2 = v2[k];
  13. if (c1 != c2) {
  14. // 比较单个字符,如果是字母字符,比较对应的 ASCLL 吗
  15. return c1 - c2;
  16. }
  17. k++;
  18. }
  19. // 长度不一样,直接返回长度之差
  20. return len1 - len2;
  21. }

从源码的返回值可以看出,compareTo() 会循环比较所有的字符,然后会取长度较短的字符依次循环遍历每一个字符,如果有不相等的,则返回 return char1 - char2, char2 使我们从方法外面传进来的 字符串,方法一是被比较的字符串,两者相减得到的值就是返回值。

由此可以看出 compareTo() 和 equals() 方法的区别:

  • equals() 可以接受一个 Object 类型参数,而 compareTo() 只能接收一个 String 参数。
  • equals() 返回 Boolean 值,compareTo() 返回 int

两者都用于字符串比较,当 equals() 为 true,compareTo() 为 0 的时候,两者功能是一样的。都表示同一个字符串

3.4 String 中常用的其他方法

  1. public static void main(String[] args) {
  2. String str = "adminc123332aa";
  3. System.out.println(str.indexOf("12")); // int 返回子字符串第一次出现的下标位置
  4. System.out.println(str.lastIndexOf("a")); // int 返回子字符串最后一次出现的下标位置
  5. System.out.println(str.contains("admin")); // boolean 判断字符串是否存在
  6. String str1 = "ABCDEFG";
  7. System.out.println(str.toLowerCase()); // 将字符串全部大写转换为小写
  8. System.out.println(str.toUpperCase()); // 将字符串中全部小写转换为大写
  9. System.out.println(str.length()); // 获得字符串的长度
  10. String str2 = " aa ";
  11. System.out.println(str.trim()); // 去掉字符串首尾空格
  12. System.out.println(str.replace("admin","cococ")); // 替换字符串数组的某些字符
  13. // 分割字符串,返回一个字符串数组
  14. String[] chrs = str.split("");
  15. System.out.println(chrs[0]);
  16. }

image.png

四、面试询问

  • 为什么 String 要用 final 修饰?

    使用 final 修饰表明 String 是不可继承的类,这样做表示更安全,更高效

  • == 和 equals 的区别是什么?

    • == 对于基本数据类型来说,是比较 “值” 相等的,对于引用类型来说,是比较引用地址是否相同
    • String 重写了 equals 方法把它变成了两个字符串是否相等
  • String 和 StringBuilder、StringBuffer 有什么区别?