记录 王磊老师的 Java 源码剖析 34 讲
源码解析
以主流的 JDK 版本 1.8 来说,String 内部实际存储结构为 char 数组,源码如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
// 用于存储字符串的值
private final char value[];
/** Cache the hash code for the string */
// 缓存字符串的 hash code
private int hash; // Default to 0
// ......其他内容
}
从源码中可以看出
1、String 被 final 修饰, String 类不可能被继承了,不能被继承了String 的操作方法都不会被继承覆写,
final 用于声明属性,方法和类,对属性来说属性不可变,对方法来说方法不可覆盖,对方法来说类不可继承。
2、String 中保存数据的是一个 char 的数组 value。value 是被 final 修饰的,value 的权限是 private 的,外部绝对访问不到,String 也没有开放出可以对 value 进行赋值的方法,value 一旦产生,内存地址是绝对无法修改的。
/** The value is used for character storage. */
// 用于存储字符串的值
private final char value[];
Java 9 之后 String 的存储就从 char 数组转成了 byte 数组,char两个字节,byte一个字节,好处是存储变的更紧凑,占用的内存更少,操作性能更高了。
我们想设置自定义类是不可变的:final关键字的特性 final + private + 不提供赋值的方法。
String的不变性 类值一旦被初始化就不能被改变了,如果被修改,将会是新的类。
String s ="hello";
s ="world";
引用指向了新的String,内存地址已经被修改。
s =“world”,已经把 s 的引用指向了新的 String。
因为 String 具有不变性,所以 String 的大多数操作方法,都会返回新的 String。
@Test
public void test01() {
String s = "hello";
s = "world";
System.out.println(s);//world
String str = "hello world";
// 这种写法是替换不掉的,必须接受 replace 方法返回的参数才行。
str.replace("l", "dd");
System.out.println(str);//hello world
str = str.replace("l", "dd");
System.out.println(str);//heddddo worddd
}
String具有不变性 都会返回新的String。
String 源码中包含下面几个重要的方法。
多构造方法
String 字符串有以下 4 个重要的构造方法:
String为参数的构造方法
char[] 为参数构造方法
StringBuffer 为参数的构造方法
StringBuilder 为参数的构造方法
//an empty character sequence.
public String() {
this.value = "".value;
}
// String 为参数的构造方法
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
// char[] 为参数构造方法
// The initial value of the string
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());
}
StringBuffer 和 StringBuilder 为参数的构造函数要特别留意一下。
String String Buffer String Builder 区别
1、可变性。String 不可变,StringBuilder 与 StringBuffer 是可变的。
2、线程安全性。String 和 StringBuffer 是线程安全的,StringBuilder 是非线程安全的。
3、性能。
String类一般使用判断相等有两种办法,equals 和 equalsIgnoreCase。
equals()是比较两个字符串是否相等 ,equalsIgnoreCase()判断相等时,会忽略大小写。
执行的步骤也是有关系的,整理一下思路,来一起看下 equals 的源码
源码如下:
public boolean equals(Object anObject) {
//判断内存地址是否相同
// 对象引用相同直接返回 true
if (this == anObject) {
return true;
}
// 判断需要对比的值是否为 String 类型,如果不是String,则直接返回 false 不相等
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相等的逻辑,应该如何写呢。
String 类型重写了 Object 中的 equals() 方法,equals() 方法需要传递一个 Object 类型的参数值,在比较时会先通过 instanceof 判断是否为 String 类型,如果不是则会直接返回 false
当判断参数为 String 类型之后,会循环对比两个字符串中的每一个字符,当所有字符都相等时返回 true,否则则返回 false。
还有一个和 equals() 比较类似的方法 equalsIgnoreCase(),它是用于忽略字符串的大小写之后进行字符串对比。
如何判断两者是否相等时,从两者的底层结构出发
String 底层的数据结构是 char 的数组一样,判断相等时,就挨个比较 char 数组中的字符是否相等即可。
compareTo() 方法用于比较两个字符串,返回的结果为 int 类型的值,
>0 =0 <0
源码如下:
compareTo
compareTo()方法用于比较两个字符串
public int compareTo(String anotherString) {
//获取字符串的长度
int len1 = value.length;
int len2 = anotherString.value.length;
// 获取到两个字符串长度最短的那个 int 值
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) {
// 有字符不相等就返回差值
return c1 - c2;
}
k++;
}
return len1 - len2;
}
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) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
从源码中可以看出,compareTo() 方法会循环对比所有的字符,当两个字符串中有任意一个字符不相同时,则 return char1-char2。
比如,两个字符串分别存储的是 1 和 2,返回的值是 -1;
如果存储的是 1 和 1,则返回的值是 0 ,
如果存储的是 2 和 1,则返回的值是 1。
字符串 直接相加减了 原来是这样的
compareTo()
compareToIgnoreCase() 忽略大小写后比较两个字符串
可以看出 compareTo() 方法和 equals() 方法都是用于比较两个字符串的,但它们有两点不同:
它们都可以用于两个字符串的比较,当 equals() 方法返回 true 时,或者是 compareTo() 方法返回 0 时,则表示两个字符串完全相同。
split() 字符串分割并返回字符串数组
join() 把字符串数组转为字符串
替换 删除
替换在工作中也经常使用,有 replace 替换所有字符、replaceAll 批量替换字符串、replaceFirst 替换遇到的第一个字符串三种场景。
其中在使用 replace 时需要注意,replace 有两个方法,一个入参是 char,一个入参是 String,char ''表示替换所有字符,如:name.replace('a','b')
,
String ""后者表示替换所有字符串,如:name.replace("a","b")
,两者就是单引号和多引号的区别。
需要注意的是, replace 并不只是替换一个,是替换所有匹配到的字符或字符串哦。
public void testReplace(){
String str ="hello word !!";
log.info("替换之前 :{}",str);
str = str.replace('l','d');
log.info("替换所有字符 :{}",str);
str = str.replaceAll("d","l");
log.info("替换全部 :{}",str);
str = str.replaceFirst("l","");
log.info("替换第一个 l :{}",str);
}
//输出的结果是:
替换之前 :hello word !!
替换所有字符 :heddo word !!
替换全部 :hello worl !!
替换第一个 :helo worl !!
当然我们想要删除某些字符,也可以使用 replace 方法,把想删除的字符替换成 “” 即可。
拆分和合并
拆分我们使用 split 方法,该方法有两个入参数。第一个参数是我们拆分的标准字符,第二个参数是一个 int 值,叫 limit,来限制我们需要拆分成几个元素。如果 limit 比实际能拆分的个数小,按照 limit 的个数进行拆分,我们演示一个 demo:
String s ="boo:and:foo";
// 我们对 s 进行了各种拆分,演示的代码和结果是:
s.split(":") 结果:["boo","and","foo"]
s.split(":",2) 结果:["boo","and:foo"]
s.split(":",5) 结果:["boo","and","foo"]
s.split(":",-2) 结果:["boo","and","foo"]
s.split("o") 结果:["b","",":and:f"]
s.split("o",2) 结果:["b","o:and:foo"]
从演示的结果来看,limit 对拆分的结果,是具有限制作用的,还有就是拆分结果里面不会出现被拆分的字段。
== 和 equals 的区别
== 对于基本数据类型来说,是用于比较 “值”是否相等的;而对于引用类型来说,是用于比较引用地址是否相同的。
查看源码我们可以知道 Object 中也有 equals() 方法,源码如下:
public boolean equals(Object obj) {
return (this == obj);
}
可以看出,Object 中的 equals() 方法其实就是 ==,而 String 重写了 equals() 方法把它修改成比较两个字符串的值是否相等。