在java中,String虽然并不属于基本数据类型,凡是却是十分常用的一个类。本文简单的从源码角度,简单分析String类。
1 String类的定义及其两个重要的成员变量
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
上述源码,只包括了String类的定义及其两个重要的成员变量。通过源码,我们可以得出以下结论:
- String对象是不可变的。
- String类里的字符串本质是以char数组方式进行存储的。
至于String对象内存储的值是不可变的,其理由如下:
- 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
- String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
2 String类中的三个基本构造函数
// 无参构造函数
public String() {
this.value = "".value;
}
// 以char数组的参数为构造函数
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
// 以char数组的参数为构造函数,并指明起始的位置和数量
public String(char value[], int offset, int count) {
// 偏移的起始地址小于0,抛出异常
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
// 当需要复制的数量小于0
if (count <= 0) {
// count小于0则抛出异常
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// count等于0,且偏移的起始地址小于或等于char数组长度
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// 偏移的起始地址 + count 必须小于或等于char数组长度
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
// 正常进行数组的拷贝
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
3 以StringBuffer和StringBuilder为参的构造函数
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
首先,我们必须知道StringBuffer
和StringBuilder
都是继承自AbstractStringBuilder
类。
而上述源码中的buffer.getValue方法和builder.getValue方法本质上都是直接继承的AbstractStringBuilder的getValue() 方法。getValue()也是极为简洁的。
final char[] getValue() {
return value;
}
4 常用的charAt()
方法
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
极为简单,直接返回了value对应的元素即可。
5 getChars()
方法
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);
}
该方法的作用是将当前字符串从srcBegin到srcEnd(不包括)位置上的字符复制到字符数组dst中,并从dst的dstBegin处开始存放。
6 equals()
和contentEquals()
方法
equals()方法必须要检查被比较对象的类型,比较的两个对象都是String类,才能进行比较。否则返回false。
public boolean equals(Object anObject) {
// 如果是同一个对象,直接返回true
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;
}
contentEquals()只比较两者的内容是否相同,不检查被比较对象的类型。
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence)sb);
}
// 正差比较内容
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;
}
public boolean contentEquals(CharSequence cs) {
// 元素是 StringBuffer 或者 StringBuilder对象
if (cs instanceof AbstractStringBuilder) {
// 如果是StringBuffer 加锁
if (cs instanceof StringBuffer) {
synchronized(cs) {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
// 元素是String对象
if (cs instanceof String) {
return equals(cs);
}
// 元素是正常的字符序列
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;
}
7 compareTo()
方法
public int compareTo(String anotherString) {
//len1:当前字符串长度
int len1 = value.length;
//len2:参数字符串长度
int len2 = anotherString.value.length;
//len1和len2两者最小值
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];
//字符不同,则返回两字符的ASCII 码的差值
if (c1 != c2) {
return c1 - c2;
}
k++;
}
//相同则返回两字符长度差值
return len1 - len2;
}
源码可以看出,此方法对大小写敏感,且:
- 字符串前面部分的每个字符完全一样,返回:后面两个字符串长度差;
- 字符串前面部分的某个字符存在不一样,返回:出现不一样的字符 ASCII 码的差值。
- 字符串的每个字符完全一样,返回 0。
在String内部还有个静态内部类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);
// 还不一样则:返回不一样字符的ASCII 码的差值。
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
8 startsWith()
方法
public boolean startsWith(String prefix, int toffset) {
...
}
startsWith() 方法用于检测字符串是否以指定的前缀开始。两个参数:
- prefix – 前缀
- toffset – 字符串中开始查找的位置
9 indexOf()
方法和lastIndexOf()
方法
// 返回指定字符第一次出现的字符串内的索引,以指定的索引开始搜索。
public int indexOf(String str, int fromIndex) {
...
}
// 返回指定字符的最后一次出现的字符串中的索引。
public int lastIndexOf(String str, int fromIndex) {
...
}
10 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);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
substring(int beginIndex, int endIndex)表示在字符串中截取一段子字符串,从beginIndex开始到endIndex - 1结束
11 replace()
方法
// 用新字符newChar 替换所有的 旧字符oldChar
public String replace(char oldChar, char newChar) {
...
}
12 split()
方法
// 以字符串regex分割字符串,返回字符数组
public String[] split(String regex) {
...
}
13 toLowerCase()
方法和 toUpperCase()
方法
// 返回一个字符串,其中所有大写字母都转换为小写字母
public String toLowerCase() {
return toLowerCase(Locale.getDefault());
}
// 返回一个字符串,其中所有小写字母都转换为大写字母
public String toUpperCase() {
return toUpperCase(Locale.getDefault());
}
14 intern()
方法
public native String intern();
String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中。
15 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;
}
一般重写equals()
方法的同时,需要重新hashcode()
方法。
至于为何hashCode算法为什么采用 31 作为乘数,这是个好问题,建议参考这儿。
HashCode算法为什么采用31作为乘数?
或者
HashCode为什么要用31作为乘数?