记录 王磊老师的 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 是可变的。
String 类中使用只读字符数组保存字符串,private final char value [],所以是不可变的(Java 9 中底层把 char 数组换成了 byte 数组,占用更少的空间)。StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,char [] value,这两种对象都是可变的。2、线程安全性。String 和 StringBuffer 是线程安全的,StringBuilder 是非线程安全的。
String 线程安全是因为其对象是不可变的,StringBuffer 线程安全是因为对方法加了同步锁或者对调用的方法加了同步锁。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。3、性能。
String 的性能较差,因为每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。而 StringBuffer/StringBuilder 性能更高,是因为每次都是对对象本身进行操作,而不是生成新的对象并改变对象引用。一般情况下 StringBuilder 相比 StringBuffer 可获得 10%~15% 左右的性能提升。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() 可以接收一个 Object 类型的参数,而 compareTo() 只能接收一个 String 类型的参数;equals() 返回值为 Boolean,而 compareTo() 的返回值则为 int。它们都可以用于两个字符串的比较,当 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() 方法把它修改成比较两个字符串的值是否相等。