String源码与常见问题
String不变性(immutable)
不可变指的是类值一旦被初始化,就不能再被改变了,如果被修改,将会是新的对象。
String str = "hello";
str = "world";
如上图,是str在被赋值过程中,debug体现的变化。从代码上来看,str 的值好像被修改了,但从 debug 的日志来看,其实是 str 的内存地址已经被修改了,也就说 str =“world” 这个看似简单的赋值,其实已经把 str 的引用指向了新的 String对象。
String 不可变的原因
从源码出发,可以看到String 是被final修饰的;其value[] 也是被private final 修饰的(请关注value属性,后文的许多分析案例,也会使用到)。也就是说任何对 String 类的操作方法,都不会被继承覆写;value[] 一旦被初始化,地址是无法被修改的,且外部也无法访问此属性。
以上两点就是 String 不变性的原因,充分利用了 final 关键字的特性。如果我们自定义类时,希望也是不可变的,也可以模仿 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 不可变,在许多String类的方法中,都会返回新的String对象。如下,是replace错误的使用方法
String str ="hello world !!";
// 这种写法是替换不掉的,必须接受 replace 方法返回的参数才行,这样才行:str = str.replace("l","dd");
str.replace("l","dd");
String 乱码
进行二进制转化操作时,经常在本地测试的都没有问题,到其它环境机器上时,有时会出现字符串乱码的情况。即一种"Work on my machine."问题。
这个主要是因为在二进制转化操作时,并没有强制规定文件编码,而不同的环境默认的文件编码不一致导致的。
String str = "nihao 你好 䳿";
// 字符串转化成 byte 数组
byte[] bytes = str.getBytes("ISO-8859-1");
// byte 数组转化成字符串
String s2 = new String(bytes);
System.out.println(s2);
// 结果打印为:
// nihao ?? ??
打印的结果为??,这就是常见的乱码表现形式。这时候有同学说,是不是我把代码修改成 String s2 = new String(bytes,"ISO-8859-1");
就可以了?这是不行的。主要是因为 ISO-8859-1 这种编码对中文的支持有限,导致中文会显示乱码。唯一的解决办法,就是在所有需要用到编码的地方,都统一使用 UTF-8,对于 String 来说,getBytes 和 new String 两个方法都会使用到编码,我们把这两处的编码替换成 UTF-8 后,打印出的结果就正常了。
String 相等判断
对象判断相等时,会通过Object的equals方法进行判断,每个继承于Object的类,都可以重写它。String的equals的源码如下。
public boolean equals(Object anObject) {
// 判断内存地址是否相同
if (this == anObject) {
return true;
}
// 待比较的对象是否是 String,如果不是 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 底层的结构(char 数组)来编写出相等的代码。这也提供了一种思路给我们:如果有人问如何判断两者是否相等时,我们可以从两者的底层结构出发,去分析。
String 替换、删除
替换在工作中也经常使用的场景
- replace 替换所有字符
- replaceAll 批量替换字符串
- replaceFirst 替换遇到的第一个字符串三种场景
其中在使用 replace 时需要注意,replace 有两个方法,一个入参是 char,一个入参是 String,前者表示替换所有字符,如:name.replace('a','b')
,后者表示替换所有字符串,如:name.replace("a","b")
,两者就是单引号和多引号的区别。
需要注意的是, replace 并不只是替换一个,是替换所有匹配到的字符或字符串哦。
当然我们想要删除某些字符,也可以使用 replace 方法,把想删除的字符替换成 “” 即可。
String 拆分和合并,建议使用Guava
拆分我们使用 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 对拆分的结果,是具有限制作用的,还有就是拆分结果里面不会出现被拆分的字段。那如果字符串里面有一些空值呢,拆分的结果如下:
String a =",a,,b,";
a.split(",") 结果:["","a","","b"]
从拆分结果中,我们可以看到,空值是拆分不掉的,仍然成为结果数组的一员,如果我们想删除空值,只能自己拿到结果后再做操作,但 Guava(Google 开源的技术工具) 提供了一些可靠的工具类,可以帮助我们快速去掉空值,如下:
String a =",a, , b c ,";
// Splitter 是 Guava 提供的 API
List<String> list = Splitter.on(',')
.trimResults()// 去掉空格
.omitEmptyStrings()// 去掉空值
.splitToList(a);
log.info("Guava 去掉空格的分割方法:{}",JSON.toJSONString(list));
// 打印出的结果为:
["a","b c"]
从打印的结果中,可以看到去掉了空格和空值,这正是我们工作中常常期望的结果,所以推荐使用 Guava 的 API 对字符串进行分割。
合并我们使用 join 方法,此方法是静态的,我们可以直接使用。方法有两个入参,参数一是合并的分隔符,参数二是合并的数据源,数据源支持数组和 List,在使用的时候,我们发现有两个不太方便的地方:
- 不支持依次 join 多个字符串,比如我们想依次 join 字符串 s 和 s1,如果你这么写的话
String.join(",",s).join(",",s1)
最后得到的是 s1 的值,第一次 join 的值被第二次 join 覆盖了; - 如果 join 的是一个 List,无法自动过滤掉 null 值。
而 Guava 正好提供了 API,解决上述问题,我们来演示一下:
// 依次 join 多个字符串,Joiner 是 Guava 提供的 API
Joiner joiner = Joiner.on(",").skipNulls();
String result = joiner.join("hello",null,"china");
log.info("依次 join 多个字符串:{}",result);
List<String> list = Lists.newArrayList(new String[]{"hello","china",null});
log.info("自动删除 list 中空值:{}",joiner.join(list));
// 输出的结果为;
依次 join 多个字符串:hello,china
自动删除 list 中空值:hello,china
从结果中,我们可以看到 Guava 不仅仅支持多个字符串的合并,还帮助我们去掉了 List 中的空值,这就是我们在工作中常常需要得到的结果。
面试问题
String和其value[]为什么要用final修饰(为什么要设计为不可变)?
String可以说是Java项目中使用频率最高的类不为过,综合考虑到资源性能方面和安全角度等,使用final修饰。比如,在创建String时Jvm会先去常量池寻找已有的缓冲常量,如果String没有被final修饰,这个时候被修改了其值,则可能会导致不可预料的问题。