String的常用方法解析
String的方法很多,这里只列出常用的几种方法进行查看解析,研究底层 jdk的实现和他的代码编写。
startsWith 和 endsWith
startsWith主要是为了判断该String a 对象是是 String b开头。
例如
String a = "ABCD";
String b = "AB";
System.out.println(a.startsWith(b));
返回的一定是一个true,startsWith 还有一个重写的方法,加了一个int 参数,这个代表他是从下标多少开始进行查找判断的。
endsWith和startsWith恰恰相反,他是结束的字符是否匹配,他没有重写方法,通过查看底层源码我们可以知道startsWith(String prefix) 和 endsWith(String suffix) 这两个方法都调用了一个方法,就是startsWith重写的方法startsWith(String prefix, int toffset)。
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
为什么他们最终都调用了startsWith(String prefix, int toffset)??
startsWith我们可以理解,代码重用,而endsWith我们也可以想象的到,我们只需要判断他是否是这个结尾,也就是说我们只需要将偏移量设置正确,只留下字符串a的最后几位即可(这几位是字符串b的长度),这样写,可以代码复用,减少代码的冗余。我们可以在看看startsWith(String prefix, int toffset)是怎么写的
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// 判断偏移量是否低于0或者大于value的长度
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
这是一个很简单的算法实现。判断字符串a是否以字符串b开始或结束。需要掌握。
charAt
这个方法是获取指定下标的字符,很简单的一个方法,但是我之前一篇博客提到了一个UNICODE。如果这个UNICODE需要获取到怎么获取?String里面提供了这几个方法
codePointAt
codePointBefore
codePointCount
offsetByCodePoints
因为平时使用了不多就不多复述了。
getChars
这个方法相信有些同学会有些陌生。首先说下这个方法的作用,是将当前字符串复制到指定的字符数组中。他有两个实现
// 这个实现主要是String的其他方法调用,String的实例是调用不了的。
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
// 主要是这个方法。
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
我们可以看见他最终调用了什么—System.arraycopy 这个方法,而这个方法有什么特点呢?他也是一个本地方法,也就说native方法,这个方法是由JVM来实现的,也就是说这个方法的效率很高。所以懂了吧,String只是对其进行了封装判断输入,真正执行的还是System.arraycopy 。
getBytes
其实这个方法也没什么好说的,就是获取这个方法的字节数组,底层主要还是jdk的编码,想了解请看java.nio.charset.CharsetEncoder类。
equals
这个方法也是常用的方法,在java里面 == 不只是比较值,还要比较他们的内存。也就是说我们如果要比较两个字符串是否相等,就不可避免的要使用这个函数
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
// 判断这个对象是否是属于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;
}
equalsIgnoreCase 和 regionMatches
忽略大小写判断,底层实现也很有意思,看源码
public boolean equalsIgnoreCase(String anotherString) {
// 首先判断了这个String对象是否是本身,
return (this == anotherString) ? true
// 不是的话进行这一步判断是否为null,并且长度是否相同
: (anotherString != null)
&& (anotherString.value.length == value.length)
// 前面判断合理之后主要就是调用了这个方法
&& regionMatches(true, 0, anotherString, 0, value.length);
}
regionMatches这个方法功能很强大,主要有五个参数,
1.boolean类型,true代表忽略大小写,false反之
2.本身这个字符串的偏移量
3.需要比较的字符串
4.比较字符串的偏移量
5.比较的长度
public boolean regionMatches(boolean ignoreCase, int toffset,
String other, int ooffset, int len) {
char ta[] = value;
int to = toffset;
char pa[] = other.value;
int po = ooffset;
// 判断ooffset , toffset 或者len 是否有效
if ((ooffset < 0) || (toffset < 0)
|| (toffset > (long)value.length - len)
|| (ooffset > (long)other.value.length - len)) {
return false;
}
while (len-- > 0) {
char c1 = ta[to++];
char c2 = pa[po++];
// 判断是否相等,相等进入下一个字符判断,不相等判断是不是要忽略大小写
if (c1 == c2) {
continue;
}
if (ignoreCase) {
// 如果字符不匹配我们就需要判断是否忽略大小写
// 首先将字符串转换为大小,如果结果匹配,就应该进行下一个字符的匹配
char u1 = Character.toUpperCase(c1);
char u2 = Character.toUpperCase(c2);
if (u1 == u2) {
continue;
}
// 不过, 转换为大小格鲁吉亚字母可能无法正常的判断,
// 其中有关于大小写转换的奇怪规则
// 所以我们还需要最后一次进行判断后退出
if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
continue;
}
}
return false;
}
return true;
}
contentEquals
这个方法有两个实现分别是StringBuffer参数类型的和CharSequence类型,
其中StringBuffer类型的contentEquals主要还是调用了CharSequence类型的contentEquals方法。
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence)sb);
}
public boolean contentEquals(CharSequence cs) {
// 参数是 StringBuffer, StringBuilder类型
if (cs instanceof AbstractStringBuilder) {
if (cs instanceof StringBuffer) {
synchronized(cs) {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
// Argument is a String
if (cs instanceof String) {
return equals(cs);
}
// 参数是通用CharSequence
char v1[] = value;
int n = v1.length;
if (n != cs.length()) {
return false;
}
for (int i = 0; i < n; i++) {
if (v1[i] != cs.charAt(i)) {
return false;
}
}
return true;
}
private boolean nonSyncContentEquals(AbstractStringBuilder sb) {
char v1[] = value;
char v2[] = sb.getValue();
int n = v1.length;
if (n != sb.length()) {
return false;
}
for (int i = 0; i < n; i++) {
if (v1[i] != v2[i]) {
return false;
}
}
return true;
}
可以这么说一般情况下我们是使用equals方法就行了,但是如果遇见其他的字符串类型匹配就需要调用这个方法。StringBuffer 就需要进行同步操作了
compareTo
String 继承了Comparable接口,这个接口对实现它的每个类的对象强加了总体排序。此排序称为类的自然排序,而该类的compareTo方法被称为其自然比较方法,简单来说,实现这个接口,就是写一个比较方法。
将此对象与指定对象进行比较。返回负整数,零或正整数,因为此对象小于,等于或大于指定的对象。
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;
}
indexOf
获取当前字符或者UNICODE码代表的字符位置。
他有这些重载的方法
public int indexOf(int ch)
public int indexOf(int ch, int fromIndex)
public int indexOf(String str)
public int indexOf(String str, int fromIndex)
我们用的更多应该是第三种,但是我还是提一下其他几种
public int indexOf(int ch) {
// 调用了重载另一个方法indexOf(int ch, int fromIndex)
return indexOf(ch, 0);
}
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// 判断起始位置是否大于字符串长度.
return -1;
}
// 主要是对这个ch进行判断,判断他是否能够被char类型所容纳,
// 主要还是UNICODE的标准已经超出了char所能容纳的大小
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
// 这里调用的一个私有方法,就是因为char类型已经不能容纳
return indexOfSupplementary(ch, fromIndex);
}
}
private int indexOfSupplementary(int ch, int fromIndex) {
// 对这个ch进行校验,查看是否是UNICODE的代码点
if (Character.isValidCodePoint(ch)) {
final char[] value = this.value;
// 高位
final char hi = Character.highSurrogate(ch);
// 低位
final char lo = Character.lowSurrogate(ch);
final int max = value.length - 1;
for (int i = fromIndex; i < max; i++) {
if (value[i] == hi && value[i + 1] == lo) {
return i;
}
}
}
return -1;
}
然后是另外两个String的indexOf方法
public int indexOf(String str) {
// 调用了重载另一个方法indexOf(String str, int fromIndex)
return indexOf(str, 0);
}
public int indexOf(String str, int fromIndex) {
// 使用了一个静态方法
return indexOf(value, 0, value.length,
str.value, 0, str.value.length, fromIndex);
}
// 六个参数的意义
// 1、源数据,就是本事的value值
// 2、源数据偏移量
// 3、源数据检索的字符个数
// 4、子字符数组
// 5、子字符数组偏移量
// 6、子字符数组检索个数
// 7、开始搜索的索引
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/// 查找第一个字符
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
// 找到第一个字符后查找后面的字符
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;
}
为什么要设计成这样,主要是因为字符串类型,在String类会多次用到这样的操作,所以封装成一个静态的方法供其他的类进行调用,比如AbstractStringBuilder就使用了这个静态的indexOf的方法。String和StringBuffer共享的代码以进行搜索。 source是正在搜索的字符数组,target是正在搜索的字符串
lastIndexOf
这个和index大致相同,只是搜索方向不一致,这个从末尾搜索。就不多复述
substring
这个方法更需要理解java的设计了,通过构造函数实现了这个功能。
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);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
也就是说每次截取字符串就代表new了一个新的对象。只需要对参数进行校验即可。
concat
这个方法就有意思了,连接两个字符串。String是不能变的字符常量,怎么才能让他变的高效?
jdk的设计很巧妙,他调用了两次System.arraycopy方法进行复制来操作。也就是说他只通过调用本地方法来完成这个功能,JVM效率是很高的,你可以自己手写一个连接方法进行测试,不调用System.arraycopy的情况下。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
// Arrays.copyOf底层也是System.arraycopy
char buf[] = Arrays.copyOf(value, len + otherLen);
// 这个方法前面讲过,里面调用了System.arraycopy
str.getChars(buf, len);
return new String(buf, true);
}
replace replaceFirst replaceAll
public String replace(char oldChar, char newChar) 一般就是将一个字符替换成另外一个字符,他会生成一个副本,将他替换然后new一个新的String对象,返回。
public String replace(CharSequence target, CharSequence replacement) 本质上与上一个方法是一致的,底层实现不一样,参数不一样,用的少。
public String replaceFirst(String regex, String replacement) 他使用的正则的方式匹配对应字符,进行替换,不过他只替换第一个匹配到的字符
public String replaceAll(String regex, String replacement) 他使用的正则的方式匹配对应字符,进行替换,他替换的是所有匹配到的字符。
除了第一个replace方法之外,其余replace方法都是要了 Pattern 类中的方法进行正则匹配,然后通过Matcher进行替换,主要实现在Matcher 类中。
split
这个方法需要注意的是,他的分隔不是单纯的以字符串分隔,他还可以通过正则来进行切分。
join
这是个静态方法,也就是说我们只需要通过String类来调用,而不是实例,也就是说我们可以将它当成一个工具类的方法,这个方法的作用就是连接字符串,第一个参数是一个CharSequence类型的字符串,我们需要将第二个参数的值加在这后面,主要第二个参数有两种情况
1.他是一个可变字符序列
2.他是一个Iterable的迭代器。
要合理利用这两种,底层实现是一样的,只有参数不一样。
toLowerCase 和 toUpperCase
这两个方法就是转大写和小写,但是源码有个点你可以看看,平时肯定没接触到
scan: {
for (firstLower = 0 ; firstLower < len; ) {
int c = (int)value[firstLower];
int srcCount;
if ((c >= Character.MIN_HIGH_SURROGATE)
&& (c <= Character.MAX_HIGH_SURROGATE)) {
c = codePointAt(firstLower);
srcCount = Character.charCount(c);
} else {
srcCount = 1;
}
int upperCaseChar = Character.toUpperCaseEx(c);
if ((upperCaseChar == Character.ERROR)
|| (c != upperCaseChar)) {
break scan;
}
firstLower += srcCount;
}
return this;
}
提问这个 scan是什么??这段源码在toUpperCase 里。
trim
消除字符串两边的空格。
toCharArray
返回一个字符数组副本。
format
将字符串转换为指定格式
valueOf
这个有许多的重载方法,就是将这些数据类型转换为String类型,他们使用了什么,可以自行查看