String 简介

String实例是常量,一旦创建后就不能再修改其值。以下为它的继承关系:

1
2
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {

可以看出String实现了SerializableCharSequenceComparable接口,其中CharSequence主要提供一些对字符序列的只读访问,许多类如StringBuilderStringBuffer也都实现了此接口,里面就只有几个方法:

1
2
3
4
5
6
public interface CharSequence {
int length();
char charAt(int index);
CharSequence subSequence(int start, int end);
public String toString();
}

String类通过final修饰,不可被继承,同时String底层的字符数组也是被final修饰的,char属于基本数据类型,一旦被赋值之后也是不能被修改的,所以String是不可变的。这里对final做一个简单的总结:

  • 修饰类:当用final修饰一个类时,表明这个类不能被继承。也就是说,String类是不能被继承的。
  • 修饰方法:把方法锁定,以防任何继承类对其覆盖。
  • 修饰变量:修饰基本数据类型变量,则其数值一旦在初始化之后便不能更改;修饰引用类型变量,则在对其初始化之后便不能再让其指向另一个对象。

成员变量

1
2
3
4
private final char value[];	// String的核心,用final修饰,无法再被修改
private int hash; // 哈希值,默认为0
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];

构造函数

String有很多重载的构造方法,介绍如下:

  1. 空参数构造方法,初始化字符串实例,默认为空字符,理论上不需要用到这个构造方法,实际上定义一个空字符String = ""就会初始化一个空字符串的String对象,而此构造方法,也是把空字符的value[]拷贝一遍而已,源码实现如下:

    1
    2
    3
    public String() {
    this.value = "".value;
    }
  2. 通过一个字符串参数构造String对象,实际上将形参的valuehash赋值给实例对象作为初始化,相当于深拷贝了一个形参String对象,源码如下:

    1
    2
    3
    4
    public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
    }
  3. 通过字符数组去构建一个新的String对象,这里使用Arrays.copyOf方法拷贝字符数组:

    1
    2
    3
    public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
    }
  4. 在源字符数组基础上,通过偏移量(起始位置)和字符数量,截取构建一个新的String对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public String(char value[], int offset, int count) {
    //如果偏移量小于0,则抛出越界异常
    if (offset < 0) {
    throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) {
    //如果字符数量小于0,则抛出越界异常
    if (count < 0) {
    throw new StringIndexOutOfBoundsException(count);
    }
    //在截取的字符数量为0的情况下,偏移量在字符串长度范围内,则返回空字符
    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);
    }
    //使用Arrays.copyOfRange静态方法,截取一定范围的字符数组,从offset开始,长度为offset+count,赋值给当前实例的字符数组
    this.value = Arrays.copyOfRange(value, offset, offset+count);
    }
  5. 通过源字节数组,按照一定范围,从offset开始截取length个长度,初始化String实例,同时可以指定字符编码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public String(byte bytes[], int offset, int length, String charsetName)
    throws UnsupportedEncodingException {
    //字符编码参数为空,抛出空指针异常
    if (charsetName == null)
    throw new NullPointerException("charsetName");
    //静态方法 检查字节数组的索引是否越界
    checkBounds(bytes, offset, length);
    //使用 StringCoding.decode 将字节数组按照一定范围解码为字符串,从offset开始截取length个长度
    this.value = StringCoding.decode(charsetName, bytes, offset, length);
    }
  6. StringBuffer构建成一个新的String,比较特别的就是这个方法有synchronized锁,同一时间只允许一个线程对这个StringBuffer构建成String对象,所以是线程安全的:

    1
    2
    3
    4
    5
    6
    7
    public String(StringBuffer buffer) {
    //对当前 StringBuffer 对象加同步锁
    synchronized(buffer) {
    //拷贝 StringBuffer 字符数组给当前实例的字符数组
    this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
    }
    }
  7. StringBuilder构建成一个新的String,与另一个构造器不同的是,此构造器不是线程安全的:

    1
    2
    3
    public String(StringBuilder builder) {
    this.value = Arrays.copyOf(builder.getValue(), builder.length());
    }

成员方法

value相关

  1. 获取字符串长度,实际上是获取字符数组长度:

    1
    2
    3
    public int length() {
    return value.length;
    }
  2. 判断字符串是否为空,实际上是判断字符数组长度是否为0:

    1
    2
    3
    public boolean isEmpty() {
    return value.length == 0;
    }
  3. 根据索引参数获取字符:

    1
    2
    3
    4
    5
    6
    7
    8
    public char charAt(int index) {
    //索引小于0或者索引大于字符数组长度,则抛出越界异常
    if ((index < 0) || (index >= value.length)) {
    throw new StringIndexOutOfBoundsException(index);
    }
    //返回字符数组指定位置字符
    return value[index];
    }

compareTo

实现了Comparable接口的compareTo方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}

可以看出该方法实现还是比较简单的,直接逐个比较每个字符是否相等,如果其中某个字符不相等就直接返回结果,否则比较它们的长度。

equals与hashCode

equals方法首先判断两个对象的地址是否相等,如果不等再判断对象是否为String类型,如果是的话再比较它们的长度与值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  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;
}

hashCode方法:

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

因为在大多数构造函数中,其实并没有设置成员变量hash的值,默认值是为0的,因此调用此方法会根据value数组重新计算哈希值,并赋给成员变量hash,下次就可以直接拿到该哈希值了。这里还有一行h = 31 * h + val[i];比较特别,它其实可以推导成一个公式:val[0]*31^(n-1) + val[1]*31^(n-2) + ... + val[n-1],这里之所以要取31这个数字,原因如下:

选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) - i,现代的 Java 虚拟机可以自动的完成这个优化。

具体可以参考这篇文章:String hashCode 方法为什么选择数字31作为乘子

intern

在Java中有8种基本类型和一种比较特殊的类型String,这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊,它的主要使用方法有两种:

  • 直接使用双引号声明出来的String对象会直接存储在常量池中。
  • 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

关于这个方法网上的解释有很多,但大多都比较混乱,我暂时还没有整理清楚,日后回来填坑。

String对 + 的重载

1
2
3
4
5
6
7
8
public class StringTest {
public static void main(String[] args) {
String apple = "Apple,";
String fruit = apple + "Pear," + "Orange";

System.out.println(fruit);
}
}

编译器在执行上述代码的时候会自动引入StringBuilder类。虽然在上面的代码中我们并没有使用到StringBuilder类,但是编译器却自动引入了它,因为它更高效。编译器首先会创建一个StringBuilder对象,用来构造最终要生成的String,并为每一个字符串调用一次StringBuilder中的append()方法,因此上述代码一共执行了三次append()方法,最后调用toString生成最终的结果,并保存为fruit

但是我们不能认为编译器已经帮我们做了优化,我们就可以随意的使用String对象,下面是两种方法生成一个String,一个方法使用了String对象,另一个使用了StringBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static String getString1(String[] strArray){
String result = "";

for(int i = 0; i < strArray.length; i++)
result += strArray[i];
return result;
}

public static String getString2(String[] strArray){
StringBuilder result = new StringBuilder();

for(int i = 0; i < strArray.length; i++)
result.append(strArray[i]);
return result.toString();
}

上面两个方法都是用来对一个数组中的数据进行连接并返回一个String对象,但是需要我们注意的是getString1方法是在循环内部构造StringBuilder对象的,这意味着每循环一次就会创建一个新的StringBuilder对象。

getString2方法只生成了一个StringBuilder对象,这样更简单更高效的实现了同getString1一样的功能。所以在我们使用String对象时,最好考虑一下是否可以用StringBuilder对象更高效的实现我们想要的功能。

总结

  • String类被修饰符final修饰是无法被继承的,而它内部的关键成员变量char value[]同样也是被final修饰不能更改的,因此String是不可变的。
  • String实现了Serializable接口,可以被序列化;实现了Comparable接口,可用于比较大小;实现了CharSequence,实现了通用的字符序列的只读方法。
  • String重载了+运算符,会创建一个StringBuilder对象并调用其append()实现字符串拼接。

参考资料