零、Java 中 String 的源码
学习笔记来自
- 慕课网专栏 —— 面试官系统精讲 Java 源码及大厂真题
- 拉钩教育 —— Java面试真题及源码 34讲
一、开篇词
我们在学习 Java SE 的过程中,经常会用到这个类型,我们只会觉得用这个很方便。可以直接申明一个字符串类型。但是大家有没想过这个是怎么来的呢?
在学 C语言的时候,如果没有接触过 C++ 的同学,对于字符串处理的时候一般都是使用一个 char 类型的字符数组来解决。没错 String 的核心就是这样的。接下里我们来看一看 String 的源码是什么样子的呢?
二、String 类型的不可变性
我们写一个示例看看
package test;
public class Test {
public static void main(String[] args) {
String a= "Hello";
a = "wwww";
System.out.println(a);
}
}
我们打上两个断点看一看。
从打断点的结果可以看出,当我们将字符串的值改为 www ,的时候,它的地址的引用就发生了改变,同时里面存储的字符也发生了变化,同时我们从打断点的结果看出,String 中的每个字符都是一个一个存储的。所以我们可以猜测两个结论
- String 被 final 修饰,是不能直接修改的,需要改变地址的引用才能换一个新的值
- String 中的字符使用字符数组存储的。
我们看下 String 的源码是什么样子的。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
现在看看 String 的部分源码,和刚才猜测的相差不大
- String 是不可变类型,不可变就代表被 final 关键字修饰,也就是说 String 类不可能被继承,也就是说对 String 操作的方法,都不会被继承重写
- String 中保存数据的是一个 char 的数组 value。我们发现 value 也是被 final 修饰的,可以这么说,value 一旦被赋值,内存地址是绝对无法修改的,value 的访问修饰符是 private ,外部也访问不到,String 也没有提供对应的 setter 方法。因此 value 一旦产生,就无法被修改。
这两点就是使 String 不变性的原因,充分利用了 final 关键字的特性,以后在开发遇到这种情况,可以采取 String 源码的设计方式加到自己的项目里面。
我们看一个例子
public class Test {
public static void main(String[] args) {
String a= "Hello";
a.replace("H","t"); // 这样写不会有效果,也不会有任何改变
System.out.println(a);
a = a.replace("H","t"); // 分析源码我们知道,我们对 String 的操作会引用一个新的地址,因此需要换一个新的 String 对象来接收。
System.out.println(a);
}
}
三、String 中的方法
3.1 String 中的构造方法
有四类构造方法,需要我们注意的是后面两个构造方法,这两类一般都是单独使用的比较多
// String 为参数的构造方法
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
// char[] 为参数构造方法
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
// StringBuffer 为参数的构造方法
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
// StringBuilder 为参数的构造方法
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
3.2 equals() 比较两个字符串是否相等
public boolean equals(Object anObject) {
// 对象引用相同直接返回 true
if (this == anObject) {
return true;
}
// 判断需要对比的值是否为 String 类型,如果不是则直接返回 false, instanceof 是用来判断数据类型的一个方法
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
// 把两个字符串都转换为 char 数组对比
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 循环比对两个字符串的每一个字符
while (n-- != 0) {
// 如果其中有一个字符不相等就 true false,否则继续对比
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
String 类型重写了 equals() 方法,equals() 方法需要传递一个 Object 类型参数值,因此比较字符串时,会先通过比较 instanceof 判断是否为 String 类型,如果不是直接返回 false
3.3 compareTo() 比较两个字符串
compareTo() 方法用于比较两个字符串,返回结果为 int 类型的值,源码如下
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
// 获取两个字符串中较短的那一个
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
// 对比每一个字符
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
// 比较单个字符,如果是字母字符,比较对应的 ASCLL 吗
return c1 - c2;
}
k++;
}
// 长度不一样,直接返回长度之差
return len1 - len2;
}
从源码的返回值可以看出,compareTo() 会循环比较所有的字符,然后会取长度较短的字符依次循环遍历每一个字符,如果有不相等的,则返回 return char1 - char2, char2 使我们从方法外面传进来的 字符串,方法一是被比较的字符串,两者相减得到的值就是返回值。
由此可以看出 compareTo() 和 equals() 方法的区别:
- equals() 可以接受一个 Object 类型参数,而 compareTo() 只能接收一个 String 参数。
- equals() 返回 Boolean 值,compareTo() 返回 int
两者都用于字符串比较,当 equals() 为 true,compareTo() 为 0 的时候,两者功能是一样的。都表示同一个字符串
3.4 String 中常用的其他方法
public static void main(String[] args) {
String str = "adminc123332aa";
System.out.println(str.indexOf("12")); // int 返回子字符串第一次出现的下标位置
System.out.println(str.lastIndexOf("a")); // int 返回子字符串最后一次出现的下标位置
System.out.println(str.contains("admin")); // boolean 判断字符串是否存在
String str1 = "ABCDEFG";
System.out.println(str.toLowerCase()); // 将字符串全部大写转换为小写
System.out.println(str.toUpperCase()); // 将字符串中全部小写转换为大写
System.out.println(str.length()); // 获得字符串的长度
String str2 = " aa ";
System.out.println(str.trim()); // 去掉字符串首尾空格
System.out.println(str.replace("admin","cococ")); // 替换字符串数组的某些字符
// 分割字符串,返回一个字符串数组
String[] chrs = str.split("");
System.out.println(chrs[0]);
}
四、面试询问
为什么 String 要用 final 修饰?
使用 final 修饰表明 String 是不可继承的类,这样做表示更安全,更高效
== 和 equals 的区别是什么?
- == 对于基本数据类型来说,是比较 “值” 相等的,对于引用类型来说,是比较引用地址是否相同
- String 重写了 equals 方法把它变成了两个字符串是否相等
- String 和 StringBuilder、StringBuffer 有什么区别?