Java源码阅读(一)String类
读一门语言的源码是学习一门语言非常好的方法,而String类是Java中最常用的类之一,因此就来看一下String的底层源码实现。
1 不变性
我们常听人说,HashMap的key建议使用不可变类,String就是这种不可变类。这里说的不可变指的是类值一旦被初始化,就不能再被改变了,如果被修改,将会是新的类,举个例子来说明。
String s ="hello"; s ="world";从代码上看,s的值好像被修改了,但实际上,是把s的内存地址已经被修改了,也就是说s =“world” 这个看似简单的赋值,其实已经把 s 的引用指向了新的String “world” 。
从源码上查看一下原因:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; 可以看出两点: - String 被 final 修饰,说明 String 类绝不可能被继承了,也就是说任何对 String 的操作,方法都不会继承覆写。
- String 中保存数据的是一个 char 的数组 value。我们发现 value 也是被 final 修饰的,也就是说 value 一旦被赋值,内存地址是绝对无法修改的,而且 value 的权限是private 的,外部绝对访问不到,String 也没有开放出可以对 value 进行赋值的方法,所以说 value 一旦产生,内存地址就根本无法被修改。
以上两点就是 String 不变性的原因,充分利用了 final 关键字的特性,如果你自定义不可变类,可以模仿String的操作。
2 字符串乱码
开发过程经常会碰到这样的场景,进行二进制转化操作时,本地测试的都没有问题,到其它环境机器上时,有时会出现字符串乱码的情况,这个主要是因为在二进制转化操作时,并没有强制规定文件编码,而不同的环境默认的文件编码不一致导致的。
写一个demo模仿游一下字符串乱码:
String str ="nihao 你好 喬亂";
// 字符串转化成 byte 数组
byte[] bytes = str.getBytes("ISO-8859-1");
// byte 数组转化成字符串
String s2 = new String(bytes);
log.info(s2);
// 结果打印为:
nihao ?? ?? 打印的结果为 ?? ,这就是常见的乱码表现形式。解决办法,就是在所有需要用到编码的地方,都统一使用 UTF-8,对于 String 来说,getBytes 和 new String 两个方法都会使用到编码,我们把这两处的编码替换成 UTF-8后,打印出的结果就正常了。String s = new String("nihao 你好");
byte[] bytes = s.getBytes("utf-8");
String s1 = new String(bytes,"utf-8");
logger.info(s1); getBytes源码 public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, value, 0, value.length);
} 3 字符串截取
如果我们的项目被 Spring 托管的话,有时候我们会通过 applicationContext.getBean(className); 这种方式得到 SpringBean,这时 className 必须是要满足首字母小写的,,除了该场景,在反射场景下面,我们也经常要使类属性的首字母小写,这时候我们一般都会这么做:
name.substring(0, 1).toLowerCase() name.substring(1);使用到了substring 方法,该方法主要是为了截取字符串连续的一部分,substring 有两个方法:
- public String substring(int beginIndex, int endIndex) beginIndex:开始位置,endIndex:结束位置,但是截取的字符串不包含endIndex的值;
- public String substring(int beginIndex)beginIndex:开始位置,结束位置为文本末;
substring 方法的底层使用的是字符数组范围截取的方法
来看一下substring的源码
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//使用了String的一个构造方法
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
} 再来看一下String的这个构造函数的源码 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);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
} 可以看到使用的是Arrays.copyOfRange(字符数组, 开始位置, 结束位置);从字符数组中进行一段范围的拷贝。该方法底层使用的System类的arraycopy方法,该方法为native方法。4 相等判断 重写equals方法
String重写了equals方法来判断字符串是否相等;
- 先判断 引用是否相等 this == 传入的参数anobject,地址引用相等说明指向同一个对象,那么值肯定也相等了
- 再使用instanceof 判断类型是否与String类型相等
- 最后逐个判断底层字符数组中的每一个字符是否相等
来看一下源码的实现:
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 底层的结构来编写出相等的代码。这也提供了一种思路给我们:如果有人问如何判断两者是否相等时,我们可以从两者的底层结构出发,这样可以迅速想到一种贴合实际的思路和方法,就像 String 底层的数据结构是 char 的数组一样,判断相等时,就挨个比较 char数组中的字符是否相等即可。5 替换、删除
替换在工作中也经常使用,有 replace 替换所有字符、replaceAll 批量替换字符串、replaceFirst 替换遇到的第一个字符串三种场景。
写了一个 demo 演示一下三种场景:
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 方法,把想删除的字符替换成 “”即可。 看一下String的实现源码
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
} 6 拆分和合并
拆分我们使用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"] 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 中的空值,这就是我们在工作中常常需要得到的结果。 
京公网安备 11010502036488号