String 源码学习
1. String
1.1不变性
不可变指的是类值一旦被初始化,就不能被修改,如果被修改那将会是新的类。
String s = "hello";
s = "world";
从代码上看,s的值好像被修改了但是debug来看
s的内存地址已经被修改了,也就是说 s= "world",看似简单的赋值,其实已经将s的引用指向了新的String。
补充(使用idea dubug模式,@XXX;就是变量的相对内存地址)
查看源码
通过源码可以看出
- string类被final修饰,说明string类不能够被继承,也就是说对任何string的操作方法,都不会被继承覆盖。
- string保存数据是byte[]。我们看到value也是被final修饰的,也就是说value一旦被赋值,那内存地址就无法被修改,而且value的权限是private的,外部访问不到,string也没有开放出对value进行赋值的方法,所以说value一旦产生,内存地址根本无法被修改。
注:(在jdk8中string实现是char[] ,而在jdk9中变更成byte[] ,使用byte数组可以减少一半的内存,byte使用一个字节来存储一个char字符,char使用两个字节来存储一个char字符。只有当一个char字符大小超过0xFF时,才会将byte数组变为原来的两倍,用两个字节存储一个char字符。)
以上两点是string不变性原因,充分利用了final关键字的特性,如果你定义类时也希望是不变的,可以参考string这两点操作。
因为string的不变性,所以string的大多数操作方法,都会返回新的string,下面需要注意
String str ="hello world";
// 这种写法是替换不掉的,必须接受 replace 方法返回的参数才行,这样才行:str = str.replace("h","xx");
str.replace("h","xx");
System.out.println(str);
str = str.replace("d", "qq");
System.out.println(str);
1.2 字符串乱码
在平时进行二进制转化操作时,本地测试没问题,但是运行到其他机器上时,会出现字符串乱码情况,主要原因是在进行二进制转化操作时,并没有强制规定文件编码,而不同的环境默认的文件编码不一致导致。
输出 : nihao ?? ??
打印的结果为??,这就是常见的乱码表现形式。这时候有人说,是不是我把代码修改成 String s2 = new String(bytes,"ISO-8859-1"); 就可以了?这是不行的。主要是因为 ISO-8859-1 这种编码对中文的支持有限,导致中文会显示乱码。唯一的解决办法,就是在所有需要用到编码的地方,都统一使用 UTF-8,对于 String 来说,getBytes 和 new String 两个方法都会使用到编码,我们把这两处的编码替换成 UTF-8 后,打印出的结果就正常了。
1.3首字母大小写
如果项目被spring托管,有时候我们会通过 application.getBean(className); 这种方式得到SpringBean,这时className必须满足首字母小写。除了该场景,在反射场景下,我们也经常要使类属性的首字母小写。
str.substring(0,1).toLowerCase()+ str.substring(1);
使用 substring 方法,该方法主要是为了截取字符串连续的一部分,substring 有两个方法:
/*beginIndex :开始位置,结束位置为文本末尾 */
public java.lang.String substring(int beginIndex)
/*beginIndex :开始位置,endIndex 结束位置 */
public java.lang.String substring(int beginIndex, int endIndex)
substring 方法的底层使用的是字符数组范围截取的方法 :
Arrays.copyOfRange(字符数组, 开始位置, 结束位置);
从字符数组中进行一段范围的拷贝。
1.4相等判断
我们判断相等有两种办法,equals 和 equalsIgnoreCase。后者判断相等时,会忽略大小写。
一些面试题在问:如果让你写判断两个 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;
}
目前源码
String重写Object的equals方法,先用“==”判断地址,地址相同则直接返回true;然后再比较类型,类型不同则直接返回false;最后才比较内容。
用于比较两字符串对象是否相等,如果引用相同则返回 true。否则判断比较对象是否为 String 类的实例,是的话转成 String 类型,接着比较编码是否相同,分别以 LATIN1 编码和 UTF16 编码进行比较。
代码如下:
public boolean equals(Object anObject) {
//判断地址是否相等
if (this == anObject) {
return true;
} else {
//比较类型
if (anObject instanceof String) {
//比较内容
//转成 String 类型,接着比较编码是否相同,
//分别以 LATIN1 编 码和 UTF16 编码进行比较
String aString = (String)anObject;
if (this.coder() == aString.coder()) {
return this.isLatin1() ? StringLatin1.equals(this.value, aString.value) : StringUTF16.equals(this.value, aString.value);
}
}
return false;
}
}
由于equals是Objec的方法,意味着任意引用类型对象都可以调用,而且,入参是Object类型,所以,不同类型是可以用equals()方法的,不会像“==”一样编译异常。
注:会遇到的一个小坑,例如:char chr = ‘a’,String str = “a”,我经常会写成str.equals(chr),而且还傻傻的等着返回true,上面说到过,两个不同类型的变量比较,equals()会直接返回false。str.equals(chr+"")倒是可以解决。
1.5替换、删除
有 replace 替换所有字符、replaceAll 批量替换字符串、replaceFirst 替换遇到的第一个字符串三种场景。
replace 有两个方法,一个入参是 char,一个入参是 String,
前者表示替换所有字符,如:str.replace('a','b')
后者表示替换所有字符串,如:str.replace("a","b")
两者就是单引号和多引号的区别。
需要注意的是, replace 并不只是替换一个,是替换所有匹配到的字符或字符串哦。
如果是删除 某些字符,也可以使用 replace 方法,把想删除的字符替换成 “” 即可
1.6拆分,合并
拆分我们使用 split 方法,该方法有两个入参数。第一个参数是我们拆分的标准字符,第二个参数是一个 int 值,叫 limit,来限制我们需要拆分成几个元素。如果 limit 比实际能拆分的个数小,按照 limit 的个数进行拆分。
从演示的结果来看,limit 对拆分的结果,是具有限制作用的,还有就是拆分结果里面不会出现被拆分的字段。
那如果字符串里面有一些空值呢,空值是拆分不掉的,仍然成为结果数组的一员,如果我们想删除空值,只能自己拿到结果后再做操作。
但 Guava(Google 开源的技术工具) 提供了一些可靠的工具类,可以帮助我们快速去掉空值,如下:
List on = Splitter.on(":")
.trimResults() //去掉空值
.omitEmptyStrings() //去掉空格
.splitToList(s);
输出:
[bbb, aaa, oo, o, asd]
合并我们使用 join 方法,此方法是静态的,我们可以直接使用。方法有两个入参,参数一是合并的分隔符,参数二是合并的数据源,数据源支持数组和 List,在使用的时候,我们发现有两个不太方便的地方:
- 不支持依次 join 多个字符串,比如我们想依次 join 字符串 s 和 s1,如果你这么写的话 String.join(",",s).join(",",s1) 最后得到的是 s1 的值,第一次 join 的值被第二次 join 覆盖了;
- 如果 join 的是一个 List,无法自动过滤掉 null 值。
// 依次 join 多个字符串,Joiner 是 Guava 提供的 API
Joiner joiner = Joiner.on(",").skipNulls();
String result = joiner.join("hello",null,"china");
List list = Lists.newArrayList(new String[]{"hello","china",null});
// 输出的结果为;
依次 join 多个字符串:hello,china
自动删除 list 中空值:hello,china