你真的了解 String 吗?
写在最前:本文内容参考了慕课网专栏《面试官系统精讲Java源码及大厂真题》。
1 不变性
String
是一个不可变的类,这里的不可变指的是一旦类的值被初始化,就不能再被改变,如果被修改,则将会产生新的类,我们根据以下代码来看一下 String
的不变性:
String str = "hello";
System.out.println("修改前的地址" + System.identityHashCode(str));
str = "world";
System.out.println("修改后的地址" + System.identityHashCode(str));
---------------------------------------------------------------
打印结果如下:
修改前的地址2117255219
修改后的地址2058534881
从上面的代码以及打印结果可以看出,同一个变量 str
打印的地址是不同的,str = "world"
这个看似简单的赋值操作,其实已经把 str
指向了新的 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
,它也是被 final
修饰,也就是说 value
一旦被赋值,内存地址是绝对无法修改的,而且权限为 private
,外部是无权限访问的,String
也没有开放出对应的赋值方法,所以 value
一旦产生,内存地址就无法被改变了。
因为 String
的不变性,所以 String
的大多数操作方法,都会返回新的 String
对象,如下所示:
String str = "hello world111";
String newStr = str.replace("111", "222");
System.out.println("原字符串:" + str);
System.out.println("替换后的字符串" + newStr);
---------------------------------------------------------------
打印结果如下:
原字符串:hello world111
替换后的字符串:hello world222
2 相等判断
我们来看一看如何判断两个 String
相等的逻辑,我们打开 String
源码,找到 equals()
方法的源码,源码如下:
public boolean equals(Object anObject) {
// 判断内存地址是否相同
if (this == anObject) {
return true;
}
// 判断 anObject 是否是 String 类型,如果不是,直接返回不相等
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
// 判断需要比较的两个字符串的长度是否相同,如果不相同,直接返回不相等
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 依次比较每个字符是否相等
while (n-- != 0) {
// 若有一个字符不相等,直接返回不相等
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
equals()
的源码逻辑非常清晰,完全根据 String
的底层结构编写出了相等的代码。
3 字符串乱码
在我们平时的开发中,经常会碰到二进制转化的操作,本地测试没有问题,发布到生产环境或者其他环境的机器上时,有事就会出现乱码的情况,这个主要是因为我们在进行二进制转化的过程中没有强制规定文件的编码,而不同环境默认的文件编码是不一样的,从而导致乱码现象的出现,我们根据以下代码来体会以下乱码的现象:
String str = "Hello World!你好,世界!";
// 字符串转化为 byte 数组
byte[] bytes = str.getBytes("ISO-8859-1");
// byte 数组转化为字符串
String newStr = new String(bytes);
System.out.println(newStr);
---------------------------------------------------------------
打印结果如下:
Hello World!??????
上面的代码模拟了乱码现象,由于我们将字符串转化为 byte 数组时设置了文件编码为 ISO-8859-1
,而我的电脑环境的编码为 utf-8
,文件编码不一致,导致乱码的出现,其实字符串转化为 byte 数组和byte 数组转化为字符串的代码中都可以设置文件编码,我们来对上面的代码进行修正:
String str = "Hello World!你好,世界!";
// 字符串转化为 byte 数组
byte[] bytes = str.getBytes("UTF-8");
// byte 数组转化为字符串
String newStr = new String(bytes, "UTF-8");
System.out.println(newStr);
---------------------------------------------------------------
打印结果如下:
Hello World!??????
这里我们把文件编码都设置成了 UTF-8
,从而解决了乱码问题,那么有的小伙伴会有这样的疑问,如果编码格式都设置成 ISO-8859-1
可以吗?这里的答案是:不可以,这个主要是因为 ISO-8859-1
这种编码对中文的支持有限,导致中文会显示乱码,我们的解决办法就是所有需要编码的地方,统一使用 UTF-8
的编码,这样就可以避免乱码问题了。
4 首字母大小写
要想实现首字母大小写的效果,我们需要使用到 String
类的一个 substring()
方法用来将首字母截取出来,使用 String
类的 toLowerCase()
或者 toUpperCase()
方法实现字符串大小写的转换。
substring()
有两个方法,我们看一下 substring()
的源码:
// beginIndex:开始位置 endIndex:结束位置
public String substring(int beginIndex, int endIndex)
// beginIndex:开始位置,结束位置为文本末尾
public String substring(int beginIndex)
接下来我们看下一下 substring()
方法底层是如何实现的:
// java.lang.String#substring(int)
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 此处使用了 new String()
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
// java.lang.String#String(char[], int, int)
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
// substring() 方法底层由 Arrays.copyOfRange() 实现
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
看过源码后,我们通过一个小例子看一下具体的使用方法:
// 首字母大写
String lowerStr = "shelby";
String newLowerStr = lowerStr.substring(0, 1).toUpperCase() + lowerStr.substring(1);
System.out.println("首字母大写的新字符串:" + newLowerStr);
// 首字母小写
String upperStr = "SHELBY";
String newUpperStr = upperStr.substring(0, 1).toLowerCase() + upperStr.substring(1);
System.out.println("首字母小写的新字符串:" + newUpperStr);
---------------------------------------------------------------
打印结果如下:
首字母大写的新字符串:Shelby
首字母小写的新字符串:sHELBY
5 替换、删除
在日常的开发工作中,我们经常有字符串替换、删除的需求,而对于这两个需求,我们都可以使用 String
类中的 replace()
方法来实现,但是使用 replace()
方法时需要注意,他有两个方法,一个入参是 char
,一个入参是 String
,前者表示替换所有字符,而后者表示替换所有字符串,方法声明如下:
// 入参是 char --> 例如源码注释中的例子:
// "mesquite in your cellar".replace('e', 'o')
public String replace(char oldChar, char newChar)
// 入参是 String --> 例如:
// "mesquite in your cellar".replace("your", "my")
public String replace(CharSequence target, CharSequence replacement)
从以上例子可以看出来,两者在编码方面的区别就是单引号和双引号的区别,但需要注意的是, replace()
方法并不只是替换一个,而是替换所有匹配到的字符或字符串。
下面我们编写一个 demo 来演示一下它的使用场景:
String str = "alanshelby";
System.out.println("替换之前:" + str);
System.out.println("替换所有字符:" + str.replace('a', 'b'));
System.out.println("替换全部:" + str.replaceAll("a", "b"));
System.out.println("替换第一个:" + str.replaceFirst("a", "b"));
System.out.println("删除所有的a:" + str.replace("a", ""));
---------------------------------------------------------------
打印结果如下:
替换之前:alanshelby
替换所有字符:blbnshelby
替换全部:blbnshelby
替换第一个:blanshelby
删除所有的a:lnshelby
6 拆分和合并
拆分我们使用 String
类中的 split()
方法来实现,该方法有两个入参,第一个是 regex
用于拆分的分隔字符串,第二个是 limit
用于限制我们需要拆分成几个元素。如果 limit
比实际能拆分的个数小,按照 limit
的个数进行拆分。下面我们通过一个 demo 进行演示:
String str = "three,two,three";
String[] strArr = str.split(",");
Arrays.stream(strArr).forEach(item -> System.out.println(item));
// 输出结果为:"three" | "two" | "three"
String[] strArr1 = str.split(",", 2);
Arrays.stream(strArr1).forEach(item -> System.out.println(item));
// 输出结果为:"three" | "two,three"
String[] strArr2 = str.split(",", 5);
Arrays.stream(strArr2).forEach(item -> System.out.println(item));
// 输出结果为:"three" | "two" | "three"
String[] strArr3 = str.split(",", -2);
Arrays.stream(strArr3).forEach(item -> System.out.println(item));
// 输出结果为:"three" | "two" | "three"
String[] strArr4 = str.split("e");
Arrays.stream(strArr4).forEach(item -> System.out.println(item));
// 输出结果为:"thr" | "" | ",two,thr"
String[] strArr5 = str.split("e", 2);
Arrays.stream(strArr5).forEach(item -> System.out.println(item));
// 输出结果为:"thr" | "e,two,three"
根据以上 demo 中的例子来看,limit
对拆分的结果,是具有限制作用的,还有就是拆分结果里面是不会出现被拆分的字段的。