查看所有目录
String类是我们日常开发中使用最频繁的类之一,曾今有人说String类用的好坏能评判你是否是一个合格的java程序员。
基础知识:
String对象的存放位置:大家都知道java中的对象大都是存放在堆中的,但是String对象是一个特例,它被存放在常量池中。
当创建一个字面量String对象时,首先会去检查常量池中这个对象的存在与否。
java本地方法:一个本地方法就是一个java调用非java代码的接口。该方法是非java实现,由C或C++语言实现。形式是:
修饰符 native 返回值类型 本地方法名(); 如public native String intern();
在我们看java源码时如果追溯到了本地方法,在java层面上就到头了,如果需要更深层次的了解本地方法的实现,就需要下载openjdk源码然后看它是如何实现的了。有兴趣的同学可以看如何查看java本地方法这篇文章。
String类:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {}
可以看到String类是final修饰的不能被继承,同时它实现了Serializable接口可以序列化和反序列化,实现了Comparable支持字符串的比较,实现了CharSequence接口说明它是一个字符序列。
成员变量:
private final char value[];//存储字符串
private int hash; //字符串的hash code 默认是0
private static final long serialVersionUID = -6849794470754667710L;//序列化id
String对象的字符串实际是维护在一个字符数组中的。操作字符串实际上就是操作这个字符数组,而且这个数组也是final修饰的不能够被改变。
构造方法:
public String() {
this.value = "".value;
}
无参构造方法,值为空串。基本不用。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
参数String对象参数来构造String对象,该构造函数经常被用来做面试题。问new String("abc");共创建了几个对象。答案是两个,字面量"abc"创建一个对象放在常量池中,new String()又创建一个对象放在堆中。
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
通过整个char数组参数来构造String对象,实际将参数char数组值复制给String对象的char数组。
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);
}
截取入参数组的一部分来构造String对象,具体哪一部分由offset和count决定,其中做了些参数检查,传入非法参数会报数组越界异常StringIndexOutOfBoundsException
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}
通过byte数组构造String对象,将入参byte数组中指定内容,用指定charsetName的字符集转换后构造String对象。
其中StringCoding.decode(charsetName, bytes, offset, length)是根据指定编码对byte数组进行解码,解码返回char数组。
checkBounds(bytes, offset, length)是对参数进行检查(源码如下),该方法是私有的只能在String类中调用。
private static void checkBounds(byte[] bytes, int offset, int length) {
if (length < 0)
throw new StringIndexOutOfBoundsException(length);
if (offset < 0)
throw new StringIndexOutOfBoundsException(offset);
if (offset > bytes.length - length)
throw new StringIndexOutOfBoundsException(offset + length);
}
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, length);
}
该构造方法与上述构造方法类似,只不过这里的字符集是用Charset指定的,上述是用String指定的。Charset与charsetName是java中表示字符集的两种不同形式。它们之间相互转换如下:
字符串转Charset对象:Charset charset = Charset.forName("UTF-8");
Charset对象转字符串:String s = charset.displayName();
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}
public String(byte bytes[], Charset charset) {
this(bytes, 0, bytes.length, charset);
}
这两个方法同上述两个方法类似,上述是转换byte数组中的部分数据构造String对象,这里是转换全部byte数组构造String对象。通过转换byte数组构造String对象在工作中还是挺常用的。
public String(byte bytes[], int offset, int length) {
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
通过byte数组,不指定字符集构造String对象。实际要在 StringCoding.decode(bytes, offset, length);解码byte数组的时候会构造默认的字符编码,默认的也就是系统默认的可能过GBK,可能过UTF-8,也可能是其它。可通过-Dfile.encoding=UTF-8进行修改系统默认编码。
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
通过StringBuffer构造String对象,StringBuffer内部也是维护了一个char数组,这里将StringBuffer数组中的内容复制给String对象中的数组。而且StringBuffer是线程安全的,所以这里也加了synchronized块保证线程安全。
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
通过StringBuilder构造String对象,原理同StringBuffer一样,只不过StringBuilder是线程不安全的,所在这里没有加synchronized块。基础面试中面试官经常询问StringBuffer与StringBuilder的区别,有兴趣的同学可以搜一下。
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
乍一看不知道这个构造函数是用来干嘛的,仔细分析就知道这个函数大有作用。首先它同String(char[] value)函数相比多了个参数share,虽然在方法本身没有用到share,目前是只支持true,注释也说了不支持false。这个方法定义成这样应该是为了同String(char[] value)进行区分。否则没办法构成方法重载。再来看下这个方法的作用。它是直接将参数的地址传给了String对象,这样要比直接使用String(char[] value)的效率要高,因为String(char[] value)是逐一拷贝。有人会问这样Stirng对象和参数传过来的char[] value共享同一个数组,不就破坏了字符串的不可变性。设计都也考虑到了,所以它设置了保护用protected修饰而没有公开出去。所以从安全性角度考虑,他也是安全的。在java中也有很多地方用到了这种性能好的、节约内存的、安全的构造函数。如replace、concat、valueOf等方法。
其它方法
length:
public int length() {
return value.length;
}
获取字符串的长度,实际上就是返回内部数组的长度。因为char数组被final修饰是不可变的,只要构造完成char数组中的内容长度都不会改变,所以这里可以直接返回数组的长度。
isEmpty:
public boolean isEmpty() {
return value.length == 0;
}
判断字符串是否为空。同理如果char的长度为0则表示字符串空。
charAt:
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
获取字符串中指定索引位置的字符。先判断索引是否合法,不能小于0或是大于字符串长度。然后直接返回数组对应位置的字符。
codePointAt:
public int codePointAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointAtImpl(value, index, value.length);
}
同charAt类似,获取字符串指定索引位置的字符的代码点。也就是将对应位置的字符转换成UniCode。
codePointBefore:
public int codePointBefore(int index) {
int i = index - 1;
if ((i < 0) || (i >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointBeforeImpl(value, index, 0);
}
获取指定索引位置前一个位置的字符的代码点。
codePointCount:
public int codePointCount(int beginIndex, int endIndex) {
if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {
throw new IndexOutOfBoundsException();
}
return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex);
}
获取字符串代码点个数,是实际上的字符个数。length()方法返回的是使用的是UTF-16编码的字符代码单元数量,不一定是实际上我们认为的字符个数。如 String str = “/uD835/uDD6B”,那么机器会识别它是2个代码单元代理的1个代码点"Z",故而,length的结果是代码单元数量2,而codePointCount()的结果是代码点数量1。
getChars(char dst[], int dstBegin):
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
复制字符串中数组内容到指定字符数组指定位置中。该方法并没有范围检查,方法仅供内部使用不对外公开。
getChars(int srcBegin, int srcEnd, char dst[], int dstBegin):
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);
}
获取字符串中指定位置的字符到目标字符数组中。该方法为公有的,做了范围检查。可以看到对外提供的方法还是要严谨一些。在工作中也是一样,内部使用的可以稍微宽松一些,对外提供的需要做严格的限制。
getBytes:
public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, value, 0, value.length);
}
public byte[] getBytes(Charset charset) {
if (charset == null) throw new NullPointerException();
return StringCoding.encode(charset, value, 0, value.length);
}
public byte[] getBytes() {
return StringCoding.encode(value, 0, value.length);
}
获取字符串对应的字节数组。将字符串编码成byte数组并返回,其中前两个方法是调用者指定字符集,后一个方法使用系统默认的字符集。
equals:
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;
}
重写了Object类的equals方法,这里是比较两个字符器的内容是否完全相等。先判断长度是否相等,长度不相等字符串必然不相等。然后再逐一比较每个对应位置的字符是否相等,如果全部相等则返回true,否则返回false。
contentEquals(StringBuffer sb) :
public boolean contentEquals(StringBuffer sb) {
return SequencontentEquals((Charce)sb);
}
比较字符串与StringBuffer对象的内容是否相等,调用了contentEquals(CharSequence cs) 方法,该方法实现如下。
contentEquals(CharSequence cs) :
public boolean contentEquals(CharSequence cs) {
// Argument is a 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);
}
// Argument is a generic 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;
}
比较字符串内容与字符序列内容是否相等。首先判断是否为AbstractStringBuilder类型,AbstractStringBuilder有两种实现方式,StringBuffer(线程安全的)和StringBuilder(线程不安全的),如果是则判断是否为StringBuffer类型,此类判断需要加锁以保证线程安全,它们两个都调用了nonSyncContentEquals方法进行判断(见下)。其次判断是否为String类型,因为String类也实现了CharSequence接口,如果是则调用String类的equals方法。最后如果是其它字符序列,则逐一比较字符数组中每个位置的字符是否相等。
nonSyncContentEquals:
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;
}
该方法为私有的供String类内部使用,也使用了相同的逻辑,先判字符数组长度是否相等,再逐一进行比较。其实不论是String的equals方法,contentEquals(CharSequence cs) 方法还是nonSyncContentEquals方法里面的比较逻辑都差不多,是否可以考虑将这类似的逻辑抽取出来单独成立个方法呢。
equalsIgnoreCase:
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}
从方法名也可以看出,该方法是忽略大小写比较字符串内容是否相同。先判断两个对象地址是否一样,地址一样内容自然也一样。再判断长度,如果长度一样再调用regionMatches方法进行比较(见后)。这里用了&&逻辑运算符的断路原理,如果前一个判断为假,后面的判断就没意义了。
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;
}
该方法是实现Comparable接口的方法,用于对字符串进行比较大小。逻辑是对两个字符串中的数组进行逐位比较大小,从第一位开始比较,大的字符串就大,如果相同就继续向下比较,直到比较出大小为止。这里取了两个字符串中长度较小的作为循环次数。从源码也可以看出字符串比较并不是我们表面上认为的先进行长度比较,长度不一样再进行每个位置的比较。
内部内
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
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; }
}
该内部内实际上就是String类定义的一个内部比较器,私有的仅供内部使用,用于进行忽略大小写比较字符串是否相等。CaseInsensitiveComparator(大小写不敏感比较器),只看名字不看其具体实现也能大致看出来其作用,可见起一个好的名字是多么的重要。比较逻辑是对每位字符逐一进行比较,如果不等则将字符转换为对应的大写字符再进行比较,如果还不等再转换为对应的小写进行比较,最后返回两个字符的大小即为整个字符串的大小。这里先转换为大写,再转换为小写的目的是不是所有的字符都是用英文字母进行表示的,比如汉字等。
compareToIgnoreCase:
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}
忽略大小写比较两个字符串的大小,用到了上述内部内的比较逻辑。
regionMatches(int toffset, String other, int ooffset,int len):
public boolean regionMatches(int toffset, String other, int ooffset,
int len) {
char ta[] = value;
int to = toffset;
char pa[] = other.value;
int po = ooffset;
// Note: toffset, ooffset, or len might be near -1>>>1.
if ((ooffset < 0) || (toffset < 0)
|| (toffset > (long)value.length - len)
|| (ooffset > (long)other.value.length - len)) {
return false;
}
while (len-- > 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
区域比较,比较两个字符串指定区域指定长度的内容是否相等。从指定区域,开始逐一比较指定长度字符数组内容是否相等。
regionMatches(boolean ignoreCase, int toffset,String other, int ooffset, int len) :
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;
// Note: toffset, ooffset, or len might be near -1>>>1.
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;
}
该方法同上述方法类似,只是加了一个参数ignoreCase,是否忽略大小写,如果忽略大小写则还要将字符转换成对应的大写,小写进行比较。
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;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
判断字符串的子串是否以指定字符开始。如"HelloWorld"就是以指定前缀"Hello"开头。逻辑是先创建字符串与指定前缀的副本(目的是为了保护字符串和避免其它线程对字符串进行修改导致比较出错),再从指定位置判断指定长度,也就是前缀长度字符串内容是否相等。
startsWith(String prefix):
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
同上述方法,只不过该方法的指定位置是从字符串的第0个位置开始。
endsWith:
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
判断字符串是否以指定字符结尾。调用了startsWith的判断逻辑。
hashCode:
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;
}
重写Object的hashCode方法。java中的hashCode有两个作用。一:Object的hashCode返回对象的内存地址。二:对象重写的hashCode配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable等。对于大量的元素比较时直接比较equals效率低下,可先判断hashCode再判断equals,因为不同的对象可能返回相同的hashCode(如"Aa"和"BB"的hashCode就一样),所以比较时有时需要再比较equals。hashCode只是起辅助作用。为了使字符串计算出来的hashCode尽可能的少重复,即降低哈希算法的冲突率,设计者选择了31这个乘数。选31有两个好处。1:31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一,其它的像37、41、43也是不错的乘数选择。2:31可以被 JVM 优化,31 * i = (i << 5) - i。计算hashCode的原理也很简单,即用原hashCode乘以31再加上char数组的每位值。
indexOf(int ch):
public int indexOf(int ch) {
return indexOf(ch, 0);
}
获取指定字符在字符串中第一次出现的索引位置。具体调用了indexOf(ch, 0) (见后)。
indexOf(int ch, int fromIndex):
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1.
return -1;
}
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return indexOfSupplementary(ch, fromIndex);
}
}
获取指定字符在字符串中指定位置后第一次出现的索引位置。逻辑是:先对开始索引位置fromIndex进行检查,如果小于0则取0,如果大于数组长度则待查找的结果不存在,返回-1.如果fromIndex合法,再判断待查找的字符是否是在两个字节以内。ch < Character.MIN_SUPPLEMENTARY_CODE_POINT 这个条件非常重要,是分界BmpCode的界限,Character.MIN_SUPPLEMENTARY_CODE_POINT这个数代表十进制中62355,刚好是2个字节。如果在两个字节内则遍历字符数组找到即返回所引。待查找字符超过两个字节,则使用indexOfSupplementary(int ch, int fromIndex)方法进行查找。该方法是拆分字符的高低位进行比较,int类型在java中占4个字节,如果不是BmpCode代码(2字节以内)点是ValidCodePoint(2字节到四字节),代码点是有高两位和低两位,这种类型的int转化为字符时分开来处理,作为两个字符。源码如下。
private int indexOfSupplementary(int ch, int fromIndex) {
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;
}
lastIndexOf:
public int lastIndexOf(int ch) {
return lastIndexOf(ch, value.length - 1);
}
public int lastIndexOf(int ch, int fromIndex) {
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
int i = Math.min(fromIndex, value.length - 1);
for (; i >= 0; i--) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return lastIndexOfSupplementary(ch, fromIndex);
}
}
从后住前查找指定字符在字符串中第一次出现的位置。原理同indexOf方法类似,这里就不赘述了。
public int indexOf(String str) {
return indexOf(str, 0);
}
public int indexOf(String str, int fromIndex) {
return indexOf(value, 0, value.length,
str.value, 0, str.value.length, fromIndex);
}
查找指定字符串在原字符串中第一次出现的索引位置。具体实现是调用了indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,int fromIndex)方法。实现如下。
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++) {
/* Look for first character. */
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
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) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
该方法是保护的,只能在包内调用。逻辑是(假设是从第0个位置开始找,从其它位置开始逻辑类似):
1.遍历当前字符串,找到当前字符串中和参数str字符串第一个字符相同的字符的位置记为i。
2.然后逐一比较接下来的每个字符是否相等,如果相等则返回,不等进行3
3.从原字符串第i个位置后找与str第一个字符相等的位置,再比较接下来的每个字符是否相等。
如此循环直到找到,或原字符串遍历完成结束方法。
lastIndexOf(String str):
public int lastIndexOf(String str) {
return lastIndexOf(str, value.length);
}
从后往前查找字符串str在原字符串中第一次出现的索引位置。该系列方法同indexOf(String str)系列方法逻辑类似。只不过查找顺序是从后往前。
substring(int beginIndex):
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);
}
截取字符串的子串。截取从指定位置beginIndex开始(包含这个位置),到字符串结束之间的字符串内容。如果beginIndex=0则返回原串,否则创建一个新的字符串返回。
substring(int beginIndex, int endIndex):
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);
}
截取原字符串的子串,同上个方法类似,上一个方法的结束位置是原串的结尾,这个方法是指定结束位置endIndex(不包含这个位置)。需要注意的是这个方法是包含头不包含尾。
subSequence:
public CharSequence subSequence(int beginIndex, int endIndex) {
return this.substring(beginIndex, endIndex);
}
截取指定区间内的字符序列。调用了substring方法,因为String本身就是一个CharSequence,所以这里可以直接返回。
concat:
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);
}
连接两个字符串。先创建了一个新的字符数组复制了两个字符串中的内容。然后通过String(char[] value, boolean share)创建结果字符串。注意这里用的是直接复制引用的方式而不是复制数组中字符的内容来创建字符串,这可以提高效率,前面写字符串的构造方法时也提到过。创建的新的字符串,对原来的两个字符串的内容没有影响。
replace(char oldChar, char newChar):
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;
}
将字符串中所有的旧字符oldChar,替换为新的字符newChar。逻辑是:先找到字符串中第一次出现oldChar字符的位置i。将之前的字符数组复制给新数组buf,然后从i后将字符数组中的内容复制给buf,只不过如果字符为oldCha则替换为newChar.然后再通过buf创建新的字符串返回。
matches:
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
查找字符串是否包含指定正则规则的字符串。关于正则在java也是一个很有用知识点,有兴趣的同学可以查一下。
contains:
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
判断字符串中是否包含指定的字符序列。实际是调用indexOf方法,查找序列在字符串中的位置来判断的,如果不包含则查找的索引为-1.
replaceFirst:
public String replaceFirst(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceFirst(replacement);
}
替换第一个正则匹配项。需要注意一点,如果需要替换的内容中包含反斜杠\需要用) in the replacement
replaceAll
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
替换所有的正则匹配项。同理新替换的内容中包含反斜杠\需要用$代替。
split(String regex, int limit) :
public String[] split(String regex, int limit) {
char ch = 0;
if (((regex.value.length == 1 && //判断参数长度是否为1
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) || //判断参数不在特殊符号".$|()[{^?*+\\"中
(regex.length() == 2 && //判断参数长度是否为2
regex.charAt(0) == '\\' && \\第一位为转义符"\\"
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && //第二位不是0-9之间 '0'转换为int为48 '9'转换为int为57
((ch-'a')|('z'-ch)) < 0 && //判断不在 a-z之间
((ch-'A')|('Z'-ch)) < 0)) && //判断不在A-Z之间
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE)) //判断分隔符不在特殊符号中
{
int off = 0;//当前索引
int next = 0;//下一个分割符出现的索引
boolean limited = limit > 0;//只分割前limit份还是全部分割,limit=0代表全部分割
ArrayList<String> list = new ArrayList<>();//创建一个集合,用于存放切割好的子串
while ((next = indexOf(ch, off)) != -1) {//判断是否包含下个分隔符,如果有则进入循环
if (!limited || list.size() < limit - 1) {//判断是全部分割或当前分割次数小于总分割次数
list.add(substring(off, next));//切割当前索引到下一个分隔符之间的字符串并添加到list中
off = next + 1; //继续切割下一下子串
} else { // last one
//assert (list.size() == limit - 1);
list.add(substring(off, value.length));//切割当前索引到字符串结尾的子字符串并添加到list
off = value.length;//将当前索引置为字符串长度
break;//结束循环
}
}
// If no match was found, return this
if (off == 0) //如果找不到分隔符则返回只有本字符串的数组
return new String[]{this};
// Add remaining segment
if (!limited || list.size() < limit)//如果是全部分割,或者没有达到分割数,则追加最后一项
list.add(substring(off, value.length));
// Construct result
int resultSize = list.size();
if (limit == 0) {//移除多余集合项
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
String[] result = new String[resultSize];//创建对应长度数组,因为返回结果为字符串数组
return list.subList(0, resultSize).toArray(result);//集合转数组并返回
}
return Pattern.compile(regex).split(this, limit);//其它情况用正则的切割规则去切割
}
根据指定规则切割原字符串。如 "abc,def,ghi".split(",")则返回包含"abc","def","ghi"三个字符串元素的字符串数组。源码分析直接写在源码中。
split:
public String[] split(String regex) {
return split(regex, 0);
}
根据指定规则切割字符串,切割全部子串。
join:
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
public static String join(CharSequence delimiter,
Iterable<? extends CharSequence> elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
join方法是jdk1.8之后新加的方法,作用是将字符序列数组,或是字符序列集合通过分割符delimiter连接成一个字符串。提供这两个实现原理差不多,第一个方法使用的可变参数,第二个方法使用的可迭代参数,这样设计主要是为了让方法更好用,参数可以是一个数组也可以是一个集合。再来看下原理。
通过遍历数组和集合将数组元素或集合元素添加到StringBuilder,添加前会先加入一个分割符delimiter,然后将StringBuilder中的内容返回,具体如下:
//1.StringJoiner的add方法,使用了方法调用链的方式,返回对象本身,可重复使用add方法。
public StringJoiner add(CharSequence newElement) {
prepareBuilder().append(newElement);//调用prepareBuilder()包含之前添加的元素和新加入一个分割符,然后再append添加新的元素
return this;
}
//2.StringJoiner的prepareBuilder方法,内部维护了一个StringBuilder
private StringBuilder prepareBuilder() {
if (value != null) {
value.append(delimiter);//每次调用这个方法时会,往StringBuilder中添加分割符delimiter
} else {
value = new StringBuilder().append(prefix);//第一次调用时创建StringBuilder对象
}
return value;//返回StringBuilder对象,以便下次调用的时候操作的是同一个StringBuilder
}
大小写转换函数:
public String toLowerCase() {
return toLowerCase(Locale.getDefault());
}
public String toUpperCase() {
return toUpperCase(Locale.getDefault());
}
从名字可以看出这两个函数是对字符串进行大小写转换的,需要注意的是只是针对英文字母[a-z][A-Z]转换有效,其它字符转换无效。
trim:
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
去掉字符串两端的空白字符,空白字符包括,空格、tab、回车符。逻辑:
1.从左到右循环字符数组,若字符为空字符,则继续循环,直到第一个不为空的字符记录其位置st。
2.从右到左循环字符数组,若字符为空字符,则继续循环,直到第一个不为空的字符记录其位置len。
3.截取字符串中从st到len位置的子串。
toString:
public String toString() {
return this;
}
返回字符串对象的字符串形式,实际上就是返回他本身。
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;
}
将字符串转换为字符数组返回。将字符串中维护的字符数组复制一份返回。这里有两点需要注意的地方:
1.这里不能直接返回内部字符数组value,如果直接返回 value,返回的数组(假设为chArray)与value指向同一个地址,一旦你修改了 chArray数组的内容,value所指向的内容也随之改变,这样破坏了String的不变性。
2.源码中有一行注释:Cannot use Arrays.copyOf because of class initialization order issues(由于类初始化顺序问题,无法使用ARARY.COSTOFF),这里我猜测是这样的,字符串比Arrays先初始化完成,但是在JDK中存在其它对象使用了toCharArray方法,而这个对象比String对象初始化晚,但比Arrays对象初始化早,导致使用时Arrays未初始化完成从而报错。故这里有了这个注释,而使用 System.arraycopy则不会存在这样的问题,因为这个方法是本地方法。
format
public static String format(String format, Object... args) {
return new Formatter().format(format, args).toString();
}
public static String format(Locale l, String format, Object... args) {
return new Formatter(l).format(format, args).toString();
}
用于创建格式化的字符串以及连接多个字符串对象。熟悉C语言的同学应该记得C语言的sprintf()方法,两者有类似之处。这里给出了两种重载形式,第一种使用本地语言环境,第二种使用指定的语言环境。
如:String.format("Hi,%s:%s.%s", "z3","l4","w5");返回Hi:z3,l4,w5
valueOf系列:
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
public static String valueOf(char data[]) {
return new String(data);
}
public static String valueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
public static String copyValueOf(char data[]) {
return new String(data);
}
public static String valueOf(boolean b) {
return b ? "true" : "false";
}
public static String valueOf(char c) {
char data[] = {c};
return new String(data, true);
}
public static String valueOf(int i) {
return Integer.toString(i);
}
public static String valueOf(long l) {
return Long.toString(l);
}
public static String valueOf(float f) {
return Float.toString(f);
}
public static String valueOf(double d) {
return Double.toString(d);
}
valueOf系列,将传入的参数,转换成各自对应的字符串对象。需要注意两点:
1.对于对象如果是null则返回字符串"null".
2.对于boolean类型真返回"true",假返回"false"
intern:
public native String intern();
最后String还有一个intern方法,这个方法是本地方法,无方法体。
jdk1.7之前intern方法执行后如果在常量池找不到对应的字符串,则会将字符串拷贝到常量池,然后返回常量池中的引用。
jdk1.7之后intern方法执行后如果在常量池找不到对应的字符串,则会在常量池中生成一个对原字符串的引用。
原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。
实际上在常量池中有一个对象StringTable,可以看作是一个HashSet。使用StringTable来维护所有存活的字符串的一个对象。
使用String的intern方法可以节省内存。在某些情况下可以使用 intern() 方法,它能够使内存中的不同字符串都只有一个实例对象。
看下面的例子:
public void testIntern() {
String str2 = new String("hello") + new String("world");
str2.intern();// 使用intern方法后str2与str1指向同一个对象,否则它们指向两个不同的对象。这样就能达到节省内存的效果。
String str1 = "helloworld";
System.out.println(str2 == str1);
}