困而学,学而知
说到源码,相信大家的第一个念头就是很难、难啃,比如说Spring源码。从而每每都对源码望而却步,只要是能用就可以了。但是这样真的可以了吗?为什么在代码评审的时候,别人能够在代码中看出问题并提出优化意见,而自己却不能?为什么别人的代码bug总是很少...都是别人,我决定了,我也要做这个别人
,那么就和我一起来看Java源码吧。
String可以说是我们在代码开发中用的最大的类之一,本着从常用的开始说起的原则,我们就先从String的源码开始吧。本文主要说String的一些特性和常用的一些类。
String 类
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** 用来保存字符的数组. */
private final char value[];
/** 缓存String的hash值 */
private int hash; // Default to 0
}
使用 final 修饰 String 类
首先来说说final
,这个关键字决定了String的不可变性。我们先来看一个例子。
String s = "hello";
s = "world";
虽然只是重新赋值了,但是s的内存地址却发生了改变。也就是说s重新被分配了一个内存空间,原先存储hello 的内存依然还存在。可以从文章开头的源码中看出,String类
和存储字符数据的char[]数组
都是被final
关键字修饰的。我们先从final关键的作用来看这样做的作用。
- final修饰变量,该变量初始化之后就不能修改
- final修饰方法,该方法不能被重写
- final修饰类,则该类就是一个不可变类,不能别重写,不能被重新分配地址。
使用final来修饰String类,说明了String类是一个不可变类,不可被继承和重写String的方法。请注意这句话,这句话十分重要。后面我们会讲到String如何保证不可变的。
使用final来修饰value属性,也就是说 value 一旦被赋值,内存地址是绝对无法修改的,而且 value 的权限是 private 的,外部绝对访问不到,String 也没有开放出可以对 value 进行赋值的方法,所以说 value 一旦产生,内存地址就根本无法被修改。
为什么要让String是不可变类呢?
这是一道面试题,也有其他的问发:String类型为什么设计成final且不可变的?
-
String Pool 的需要
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool
-
缓存HashCode
String类型的值经常用来作为hash值。使String不可变性可以保证hash值一直不变而不用进行重复运算。
-
安全性
String常被用作参数。比如说网络连接、打开文件等。网络连接中,如果String是可变的,就有可能会在连接过程中改变从而导致连接到其他主机。
可变的字符串数组在反射中也可能造成安全问题?这个没有想到例子,后期看到了再说
-
线程安全
不可变的类本身就是线程安全的,因为初始化就不可能任何线程修改
Serializable, Comparable, CharSequence
分别来说这几个接口的作用
-
Serializable
说明String是可序列化的
-
Comparable
是一个排序接口
若一个类实现了Comparable接口,就意味着***该类支持排序***。 即然实现Comparable接口的类支持排序,假设现在存在“实现Comparable接口的类的对象的List列表(或数组)”,则该List列表(或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序。
此外,“实现Comparable接口的类的对象”可以用作“有序映射(如TreeMap)”中的键或“有序集合(TreeSet)”中的元素,而不需要指定比较器。
-
CharSequence
说明String是一个可读的
字符
数组,这个接口为不同的字符数组
操作提供了统一可读的接口。charAt
、length
、subSequence
、toString
String 的数据容器 value[]
再看看String类中最重要的属性,我们可以看到这个属性其实也是被final修饰,说明value[]一旦被初始化了,就不会太修改了。请注意,final修饰对象,说明对象的内存地址是不能改变,但是对象中的属性是可变的。也就是说,我们在对value[]
初始化之后就不能再new
一次了(分配地址)。
我们可以直接把value[]直接当成String的值,我们对String的任何操作其实都是对value[]的操作。
hashCode
// s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
其实这个我们在平常工作中用得很少,或者说根本都没有用,但是他的作用确实巨大的。最重要的一个做法就是来判断两个String是否相等。之前有过一篇写HashMap一些概念的文章,详细说了说Hash的概念。需要详细了解Hash的概念可以参考这篇文章。
比较两个字符串:equals
说了hashCode
当然要说说equals
,谁叫它俩是一对呢。
equals
和==
区别在于他们的用处是不同的。equals
用于比较引用类型,==
用于比较基本类型。究其原因是equals
是比较两个对象的地址是否相等(或者每个对象域是否相等),==
用于比较两个的值是否相等。
先来说说要实现一个高效的equals
的原则。
- 使用
==
操作符检查「参数是否为这个对象的引用」,也就是说两个对象是否相等,如果是直接返回true。这一步是为了性能的考虑。 - 使用
instanceof
操作符检查「参数是否为正确的类型」,如果不是直接返回false。 - 将参数转换成正确的类型,因为经过
instanceof
检查,会一直正确。 - 对比对象的中个
关键域
,检查每个关键域是否相等,如果有一个关键域不相等,则返回false。否则返回true。
引用《Effecttive Java 第三版 》 第三章
通过上面实现高效equals的原则,我们来看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;
// 下面就是对比每个关键域是否相等,由于String本质是一个value[],
// 这里也就是对比数组中的每一个值是否相等, 只有有一个不等,直接返回false
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;
}
截取字符串:substring
substring的作用是在当前的字符串的基础上截取一个字符串。请注意,调用该方法必须要接收返回值,不然也没有用呀。
substring有两个重载函数:substring(int beginIndex)
和substring(int beginIndex, int endIndex)
。两个方法的原理都是一样的,都是调用String的构造函数新建一个String返回。
// 用参数多的类说明
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的一个构造函数,
// value[]: 用来构建字符串的原始字符数组
// offset: 初始偏移量,也就是说从数组的那个位置开始构建字符串, 0<=offset<value.length
// count: 要构建过长的字符串. 这个大小要根据offset变化
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);
}
// 其实这里是调用了
// System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
// 也就是将一个数组复制到一个新的数组
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
用法
public static void main(String[] args) {
String str = "The movie NeZha is awesome.";
// 从第五个字符开始截取,也就是"ovie NeZha is awesome."
System.out.println(str.substring(5));
// 截取第2到第5个字符, 也就是"NeZha"
System.out.println(str.substring(10, 15));
System.out.println(str);
}
ovie NeZha is awesome.
NeZha
The movie NeZha is awesome.
// 这里截取了两次,我们后面输出str,还是没有变,也印证了String的不变特性
replace和replaceAll
replace有两个重载方法: replace(char oldChar, char newChar)
和String replace(CharSequence target, CharSequence replacement)
replaceAll只有一个方法: String replaceAll(String regex, String replacement)
replace(char oldChar, char newChar)
这个方法其实比较简单。也就是遍历String中的value[]每个值,找到相同的,替换掉。
public String replace(char oldChar, char newChar) {
// 两个值相等就不需要替换了
if (oldChar != newChar) {
int len = value.length;
int i = -1;
// 避免:获取指定类的实例域,并将其值压入栈顶
// 在一个方法中需要大量引用实例域变量的时候,
// 使用方法中的局部变量代替引用可以减少getfield操作的次数,提高性能
char[] val = value; /* avoid getfield opcode */
// 找到oldChar第一次出现的位置
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;
}
replace(CharSequence target, CharSequence replacement) 和replaceAll(String regex, String replacement)
public String replace(CharSequence target, CharSequence replacement) {
return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
}
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
为什么要放到一起讲,大家其实可以从源码都可以看出来了。
看了两个两个源码,我们可以看出来什么?
那就是repace(char, char)比replace(CharSequence, CharSequence)和replaceAll(String, String)的性能普遍要好,因为后面两个用了正则,而第一个只是遍历替换而已。
startWith和endsWith
其实不管是startWith和endsWith最终的都是调用下面的方法:
public boolean startsWith(String prefix, int toffset) {
// 原始字符串数组
char ta[] = value;
// 对比开始位置 startWith是0, endsWith是value.lenght-prefix.length
// 用于指向value数组的下一个位置,每次遍历都会+1
int to = toffset;
// 字符串的数组
char pa[] = prefix.value;
// 字符串, 用于指向prefix数组的下一个位置,每次遍历都会+1
int po = 0;
// 字符串的长度
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
// 遍历prefix
while (--pc >= 0) {
// 对比从ta[toOffset]和prefix[0]开始的每一个位置上面的值是否相等。
// 如果有一个不相等,就直接返回false
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
上面算是我理解的关于String
的源码,其实String还有很多可以探究的地方,一篇文章太短,无法全部说的明明白白。学无止境,其他的还需要继续摸索。