String 简介
String
实例是常量,一旦创建后就不能再修改其值。以下为它的继承关系:
1 | public final class String |
可以看出String
实现了Serializable
、CharSequence
、Comparable
接口,其中CharSequence
主要提供一些对字符序列的只读访问,许多类如StringBuilder
、StringBuffer
也都实现了此接口,里面就只有几个方法:1
2
3
4
5
6public 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 | private final char value[]; // String的核心,用final修饰,无法再被修改 |
构造函数
String
有很多重载的构造方法,介绍如下:
空参数构造方法,初始化字符串实例,默认为空字符,理论上不需要用到这个构造方法,实际上定义一个空字符
String = ""
就会初始化一个空字符串的String
对象,而此构造方法,也是把空字符的value[]
拷贝一遍而已,源码实现如下:1
2
3public String() {
this.value = "".value;
}通过一个字符串参数构造
String
对象,实际上将形参的value
和hash
赋值给实例对象作为初始化,相当于深拷贝了一个形参String
对象,源码如下:1
2
3
4public String(String original) {
this.value = original.value;
this.hash = original.hash;
}通过字符数组去构建一个新的
String
对象,这里使用Arrays.copyOf
方法拷贝字符数组:1
2
3public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}在源字符数组基础上,通过偏移量(起始位置)和字符数量,截取构建一个新的
String
对象:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public 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);
}通过源字节数组,按照一定范围,从
offset
开始截取length
个长度,初始化String
实例,同时可以指定字符编码:1
2
3
4
5
6
7
8
9
10public 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);
}将
StringBuffer
构建成一个新的String
,比较特别的就是这个方法有synchronized
锁,同一时间只允许一个线程对这个StringBuffer
构建成String
对象,所以是线程安全的:1
2
3
4
5
6
7public String(StringBuffer buffer) {
//对当前 StringBuffer 对象加同步锁
synchronized(buffer) {
//拷贝 StringBuffer 字符数组给当前实例的字符数组
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}将
StringBuilder
构建成一个新的String
,与另一个构造器不同的是,此构造器不是线程安全的:1
2
3public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
成员方法
value相关
获取字符串长度,实际上是获取字符数组长度:
1
2
3public int length() {
return value.length;
}判断字符串是否为空,实际上是判断字符数组长度是否为0:
1
2
3public boolean isEmpty() {
return value.length == 0;
}根据索引参数获取字符:
1
2
3
4
5
6
7
8public 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
18public 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 | public boolean equals(Object anObject) { |
hashCode
方法:1
2
3
4
5
6
7
8
9
10
11
12public 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 | public class StringTest { |
编译器在执行上述代码的时候会自动引入StringBuilder
类。虽然在上面的代码中我们并没有使用到StringBuilder
类,但是编译器却自动引入了它,因为它更高效。编译器首先会创建一个StringBuilder
对象,用来构造最终要生成的String
,并为每一个字符串调用一次StringBuilder
中的append()
方法,因此上述代码一共执行了三次append()
方法,最后调用toString
生成最终的结果,并保存为fruit
。
但是我们不能认为编译器已经帮我们做了优化,我们就可以随意的使用String
对象,下面是两种方法生成一个String
,一个方法使用了String
对象,另一个使用了StringBuilder
:
1 | public static String getString1(String[] strArray){ |
上面两个方法都是用来对一个数组中的数据进行连接并返回一个String
对象,但是需要我们注意的是getString1
方法是在循环内部构造StringBuilder
对象的,这意味着每循环一次就会创建一个新的StringBuilder
对象。
getString2
方法只生成了一个StringBuilder
对象,这样更简单更高效的实现了同getString1
一样的功能。所以在我们使用String
对象时,最好考虑一下是否可以用StringBuilder
对象更高效的实现我们想要的功能。
总结
String
类被修饰符final
修饰是无法被继承的,而它内部的关键成员变量char value[]
同样也是被final
修饰不能更改的,因此String
是不可变的。String
实现了Serializable
接口,可以被序列化;实现了Comparable
接口,可用于比较大小;实现了CharSequence
,实现了通用的字符序列的只读方法。String
重载了+运算符,会创建一个StringBuilder
对象并调用其append()
实现字符串拼接。
参考资料
- 《Java 编程思想》Bruce Eckel 著 陈昊鹏 译
- String 源码浅析(一)
- String源码分析
- String hashCode 方法为什么选择数字31作为乘子