介绍类的结构
可以看到String类父类是Object类,并且它实现了3个接口。一个CharSequence(是所有char字符的可读的根接口),一个comparable可比较的接口,以及序列化接口。
源码类上的注释是这样说明String类的:
-
String类是一个字符串,Java程序中的所有字符串都是该类的实例。
-
String类的实例都是一个常量,他们一旦创建就无法改变,由于这个特性,就设计了字符串缓存池,可以高效的利用它们。比如:
-
String s1 = "abc";
-
String s2 = "abc";
-
它两的地址指向同一个缓存池中的地址。因为第一个abc一旦被创建就被缓存起来了。
-
-
该类中提供了许多操作字符串的方法,比如比较字符串,查找字符串, 截取字符串,拷贝字符串,转大/小字符串等等。
-
Java语言提供对字符串做+操作,这个操作依靠StringBuilder or StringBuffer 的append()方法来完成,以及可以将对象转换成字符串,通过toString()完成。
-
null 类型的字符串去做操作会触发Null 异常。
-
A
String
表示UTF-16格式的字符串,其中补充字符由代理对表示 (有关详细信息,请参阅Character课程中的Character
部分)。 索引值是指char
代码单元,所以补充字符在String中使用两个String
。String
类提供处理Unicode代码点(即字符)的方法,以及用于处理Unicode代码单元(即char
值)的方法。
由此我们可以知道String类是操作字符串的,提供了大量对字符串的操作。为了高效,设计了缓存池,所有字符串值都会缓存起来。另外如果字符串是null类型,去操作的时候会触发空异常。
类源码
类字段
// 用来存储字符。(字符串的值本身都是字符,最终都存储在这个数组中),可以看到被final修饰,也就是为什么String一旦创建不可变的原因。
private final char value[];
// 缓存字符的哈希值,默认是0
private int hash;
// 序列化值
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields=new ObjectStreamField[0];
// 忽略大小写的比较器
public static final Comparator<String> CASE_INSENSITIVE_ORDER= new CaseInsensitiveComparator();
类中的内部类
String类中持有一个内部类,就是它的忽略大小写的比较器
private static class CaseInsensitiveComparator
implements Comparator<String>, java.io.Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = 8575799808933029326L;
public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
/** Replaces the de-serialized object. */
private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}
可以看到这个比较器里面有一个compare比较两个字符串的算法。
算法思路:
比较两个字符串,其实比较的是对应的阿斯克吗值,只要对应位置阿斯克吗值不相等就可以立马分出胜负,另外在考虑一些特殊情况,就好了。源码具体实现流程是这样的: 先计算出两个字符串中的最小长度。然后以最小长度为单位打循环,每循环一次,就比较这两个字符串对应位置的阿斯克吗值,只要不相等,立马可以分出胜负。 那么当整个循环结束,都没有分出胜负的时候,只有两个情况:
-
两个字符串一模一样。
-
其中一个字符串是另一个字符串的子串。
这个时候直接return s1.len-s2.len 即可。 对于第一种情况,直接就retrun 0 了。正好说明相等。 对于第二种情况,如果s1包含s2.那么return的就是正数,说明s1大。否者s2大。 一句 return s1.len-s2.len 巧妙的解决了两种特殊情况。
补充:源码中还有一写字符转大小写,那都是它比较的定义,它这个比较器就叫做忽略大小写的比较。所以在实现的时候,都要把这个考虑进去。
类的构造方法
String类中有许多的重载构造器。
初始化一个空字符串String实例
新创建一个字符串以至于去替代原始的。
将char数组包装成String对象(实际调用工具类Arrays.copyOf()实现)
将char数组指定长度位置包装成Stirng对象(一样通过util包下的方法实现。)
类常用方法解析
charAt(int index):返回自身char[index]元素
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
直接就返回自身char[]数组指定index的值。当然再次之前做了一个index的判断。
compareTo(String anotherString):按字段顺序比较两个字符串
源码注释说:按照字典顺序比较,前者大返回正数,前者小,放回负数。相等返回0;
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
这个方法可以说是和String内部类比较器的compare方法一模一样,之不是这个方法不忽略大小写。实现原理一模一样。
concat(String str):将指定的字符串连接到该字符串的末尾
源码注释说明:如果参数长度为0,则返回自身。否者返回一个全新拼接的String
/*
1. 校验参数长度
2. 调用Arrays.copyOf()方法将当前字符串扩大
3. 调用getChars()方法将参数中的字符追加到原字符串中。
*/
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
该方法是先获取参数长度,做一个校验。所以如果参数是null,会触发null异常。
当校验通过后,调用数组工具类的copyOf方法返回一个夸大长度的新char[],然后将参数值全部追加到这个新char数组中,在调用构造器new String(char[] buf)包装成String即可。
所以重点落在了Arrays.copyOf() 和 getChars()方法中。
Arrays.copyOf() 会在util包下做深究。
getChars方法则是一个本地方法,是调用c++或者c库实现的。
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
contains(CharSequence s): 判断当前字符串是否包含char字符s
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
调用indexOf()方法,而indexOf()采用暴力匹配法。
valueOf(int i):将int参数转换成String
public static String valueOf(int i) {
return Integer.toString(i);
}
调用Integer.toString(i); 而Integer.toString(i)方法实际就是先计算出i参数的长度,然后初始化一个i长度的char数组,然后底层还是调用getChars()方法填充char数组,最后调用new String(char[]) 包装返回。
valueOf(char[] data):将char数组,包装成String
public static String valueOf(char data[]) {
return new String(data);
}
实际调用的是构造器的包装方法。而构造器中的包装技术调用的是Arrays.copyOf().
toCharArray():将字符串转换成一个新的字符数组。
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
可以看到是调用System.arraycopy()c++代码实现copy。然后将新数组返回。
值得注意的是:还告诉你由于类初始化顺序,没有使用Arrays.copyOf(). 也就是说String类比Arrays类先加载。
substring(int beginIndex,int endIndex):截取begin~end之间的字符串,不包括end
截取一个新的子数组,新的子数组操作不会影响到原数组。(这里说数组是因为String本身就是char数组)
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);
}
// 如果起始起始点等于0 && 终点等于本字符串的长度,就return this 。否者调用new String
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
先是参数校验一下。判断起始点是否小于0,判断终点是否大于字符串的长度,判断起始点是否大于终止点。
当校验通过后,如果beginIndex== && endIndex等于字符串的长度就return this。否者调用重载构造器。
而这个重载构造器其实还是调用Arrays.copyOfRange(value, offset, offset+count); 这行代码。所以重点还是落在了Arrays类下。
split(String regex) & split(String regex, int limit):将字符串按照指定字符切割成String数组
当调用split(String regex) ,其实调用的是split(String regex,int limit);
public String[] split(String regex) {
return split(regex, 0);
}
源码方法的注释描述大概就这意思:
围绕正则表达式进行分割字符串,最终返回分割后的数组。第二个参数就是期待分割后数组的长度,如果<=0,则尽可能分割。
这个方法暂时看不太懂,值得注意的是如果字符串中包含.$|()[{^?* 的话,进行分割需要前面加上转移字符//
replaceAll(String regex,String replacement):将自身的包含的regex字符,全部替换成replacement字符。
indexOf():返回子串str在大字符串中第一次出现的索引。
public int indexOf(String str) {
// 上来会一直调用重载方法
....
}
// 最终会调用到这个重载方法
/*
source:大串字符串的char数组
sourceOffset:大串的原始偏移量,默认0
sourceCount:大串字符串的长度
target:子串的char数组
targetOffset:子串的偏移量,默认0
targetCount:子串字符串的长度
fromIndex:从大串开始搜索的起始索引
方法解说:
大致思路就是:遍历大串,然后判断大串的下标i的字符是不是等于子串的首字符,如果是,那么继续比对剩余的字符。
如果能把子串全部比对完毕,就说明找到了,直接return 大串的下标i字符。
可以看出JDK源码中用的是最普通的暴力法,并没有用到kmp算法。
*/
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
/*
判断大串是否为空字符串,如果是,就继续判断子串的情况
子串如果也是空,那么return 大串的长度也就是0;
如果不是,那么说明大串都已经为空字符串了,不可能找到,直接return -1;
*/
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
// 更新正确的fromIndex
if (fromIndex < 0) {
fromIndex = 0;
}
// 如果子串长度等于0,直接返回fromIndex 也就是0
if (targetCount == 0) {
return fromIndex;
}
// 获取子串的首字符
char first = target[targetOffset];
// 计算出一个最大max长度,目的是为了优化代码。如果当前大串还有3个长度未遍历,而子串长度是4,那么在怎么比对也不可能全部比对上了。所以计算出一个max。遍历大串只要遍历到max位置就好了。
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
// 判断当前大串下标是不是和子串首字符相等。如果不是,就一直操作i下标,直到相等。
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* 程序能执行到此处,说明当前大串的i下标位置和子串的首位置相等了,那么接下来的工作就是继续比对剩余的部分了。
源码中的继续比对环节是这样的。先定义2个指针,一个j,一个end。j:就是大串i+1的位置。end:就是全部比对上,最后的位置。
然后遍历子串,从子串1位置开始,每遍历一个看看j的位置是否和子串对应的位置相等。如果相等j++,子串下标++。
当j指针能碰到end的位置的时候,说明子串全部比对上了,就返回大串的i下标即可。
*/
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
if (j == end) {
return i - sourceOffset;
}
}
}
return -1;
}
equals(Object anObject):将此字符串与指定对象比较。
源码方法注释:
将字符串与指定参数对象比较,如果参数是String,并且对应字符序列全相等则return true.
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
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 类equals还是很好理解的,
-
比较内存地址是否一致,如果一致,return true;
-
判断参数是否是String类,如果是继续判断两字符串长度是否一致。都满足开始正规比较。
正规比较:对应位置字符是否一致,只要不一致就终止。当循环结束,return true。
总结
在String类中只要涉及到改变字符串的char[]数组,如new String(char[] char)、concat(String s)、substring()等就调用Arrays.copyOf()实现数组的拷贝,而Arrays.copyOf(),调用的是System.arraycopy()也就是c代码。所以重点来时落在了c原理上。这块需要深究。另外还有一些字符串匹配的方法如split(),raplaceAll等,都使用的是util包下的Pattern类实现,依靠正则表达式实现。
在String类中还有一些比较好玩的方法,如compare比较、indexOf、equals、repalceAll等都可以手动实现下。
comapre:可以刷leetcode 165. 比较版本号
indexOf:对应leetcode 28.实现strstr()
replaceAll:对应leetcode 833: 字符串中的查找与替换