在此之前有无数次下定决心要把JDK的源码大致看一遍,但是每次还没点开就已被一个超链接或者其他事情吸引直接跳开了。直到最近突然意识到,因为对源码的了解不深导致踩了许多莫名其妙的坑,所以再次下定决心要把常用的类全部看一遍。。。
一. 声明和成员变量(不可变性)
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
}
String的大部分操作都是围绕value
这个字符数组定义的。同时String为了并发和一些安全性的考虑被设计成了不可变的类型,表明一旦被初始化完成后就是不可改变的。
在这里可能有人会疑惑,比如:
public static void main(String[] args) {
String s1 = "abc";
s1 = "bcd";
System.out.println(s1);
}
// 打印: bcd
其中“abc”被初始化完成之后即为不可改变的,s1只是stack里面的引用变量,随后s1将引用指向了“bcd”这个不可变字符,所以最后打印出来的是“bcd”。
而为了实现String的不可变性:
- String被声明为final类型:表明String不能被继承,即不能通过继承的方式改变其中的value值。
- value被声明为final类型:这个final并不能表示value这个字符数组的值不可变,只是确定了value这个字符数组在内存中的位置是不可变的。
- 在随后的方法介绍中可以看到,String并没有提供修改value[]值得方法,并且所有的方法都不是直接返回value[],而是copy value[]中的值或者新建一个String对象。所以在通常意义上String是不可变的,但是却不是绝对意义上的不可变,比如:
方法一:上面的第二点也说了,final char[] value,只定义了value所指向的内存地址不变,其中的值是可以变的,所以我们可以通过反射直接修改value的值
private void test03_reflection() throws Exception {
String s = "abc";
System.out.println("s = " + s);
Field valueFieldOfString = String.class.getDeclaredField("value");
valueFieldOfString.setAccessible(true);
char[] value = (char[]) valueFieldOfString.get(s);
value[1] = 'o';
System.out.println("s = " + s);
}
// 最终打印: aoc
方法二:可以直接使用unsafe类进行修改
// unsafe类不能直接new,构造方法里面有对调用者进行校验,但是我们同样可以通过反射获取
public static Unsafe getUnsafe() throws Exception {
Field theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeInstance.setAccessible(true);
return (Unsafe) theUnsafeInstance.get(Unsafe.class);
}
// 通过unsafe类替换value[]
public void test04_unsafe() throws Exception {
String s = "abc";
System.out.println("s = " + s);
Field f = s.getClass().getDeclaredField("value");
f.setAccessible(true);
long offset; Unsafe unsafe = getUnsafe();
char[] nstr = new char[]{'a', 'o', 'c'};
offset = unsafe.objectFieldOffset(f);
Object o = unsafe.getObject(s, offset);
unsafe.compareAndSwapObject(s, offset, o, nstr);
System.out.println("s = " + s);
}
// 最终打印: aoc
// 通过unsafe类,定位value[]的内存位置修改值
public void test05_unsafe() throws Exception {
String s = "abc";
System.out.println("s = " + s);
Field f = s.getClass().getDeclaredField("value");
f.setAccessible(true); long offset;
Unsafe unsafe = getUnsafe();
offset = unsafe.arrayBaseOffset(char[].class) + 2;
char[] arr = (char[]) f.get(s);
unsafe.putChar(arr, offset, 'o');
System.out.println("s = " + s);
}
// 最终打印: aoc
二、构造函数
public String()
public String(String original)
public String(char value[])
public String(char value[], int offset, int count)
public String(int[] codePoints, int offset, int count)
public String(byte ascii[], int hibyte, int offset, int count)
public String(byte ascii[], int hibyte)
public String(byte bytes[], int offset, int length, String charsetName)
public String(byte bytes[], int offset, int length, Charset charset)
public String(byte bytes[], Charset charset)
public String(byte bytes[], int offset, int length)
public String(byte bytes[])
public String(StringBuffer buffer)
public String(StringBuilder builder)
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
以上15个构造方法除了最后一个,都是将传入的参数copy到value中,并生成hash。这也是符合string的不可变原则。而最后一个则是用于string和包内部产生的string对象,他没有复制value数组,而是持有引用,共享value数组。这是为了加快中间过程string的产生,而最后得到的string都是持有自己独立的value,所以string任然是不可变的。
三、常用方法
这个再次强调,String方法的所有返回值,都是new的一个新对象,以保证不可变性
1. String.equals
和String.hashCode
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;
}
equals首先比较是否指向同一个内存地址,在比较是不是String类,再是长度最后内容注意比较。
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;
}
hashcode使用的数学公式:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
String 经常会用作 hashMap 的 key,所以我们希望尽量减少 String 的 hash 冲突。(冲突是指 hash 值相同,从而导致 hashMap 的 node 链表过长,所以我们通常希望计算的 hash 值尽可能的分散,从而提高查询效率),而这里选择31是因为,如果乘数是偶数,并且结果益处,那么信息就会是丢失(与2相乘相当于移位操作)31是一个奇素数,并且31有个很好的特性(目前大多数虚拟机都支持的优化):
31 * i == (i << 5) - i
2. String.intern
/**
* Returns a canonical representation for the string object. * <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the * <cite>The Java™ Language Specification</cite>.
* @return a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.
*/
public native String intern();
可以看到intern这是一个native方法,主要用来查询常量池的字符串,注释中也写了:
- 如果常量池中中存在当前字符串,就会直接返回此字符串(此时当前字符串和常量池中的字符串一定不相同);
- 如果常量池中没有,就将当前字符串加入常量池后再返回(此时和常量池的实现相关)。
- 这里有关常量池的设计,其实是享元模式。
将字符串加入常量池的两种方式:
- 编译期生成的各种字面量和符号引用
将javap反编译测试:
String s1 = "abc";
public static void main(String[] args) {
String s2 = "123";
}
javap -v **.class:
Constant pool:
...
#24 = Utf8 abc
#25 = NameAndType #7:#8 // s1:Ljava/lang/String;
#26 = Utf8 123
...
- 运行期间通过
intern
方法将常量放入常量池
// 将javap反编译测试:
String s1 = "abc";
public static void main(String[] args) {
String s2 = (new String("1") + new String("2")).intern();
}
javap -v **.class:
Constant pool:
...
#2 = String #32 // abc - "abc" 常量的引用对象
#7 = String #36 // 1 -
#10 = String #39 // 2 -
#15 = Utf8 s1 - 引用变量
#28 = Utf8 s2 -
#32 = Utf8 abc - 变量
#36 = Utf8 1 -
#39 = Utf8 2 -
...
常量池中没有"12"变量,但是在运行的时候,会动态添加进去,后面可以用==测试
JDk版本实验,先简单讲一下常量池在不同JDK中的区别:
- 在 JDK6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生
java.lang.OutOfMemoryError: PermGen space
错误的。 - 在 JDK7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。为什么要移动,Perm 区域太小是一个主要原因。
- 在 JDK8 则直接使用 Meta 区代替了 Perm 区,并且可以动态调整 Mata 区的大小。
测试:
public void test06_intern() {
String s1 = new String("1");
s1.intern();
String s2 = "1";
System.out.println(s1 == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
JDK6: false false
JDK7、8:false true
分析:
- 对于 JDK6:
如上图所示 JDK6 中的常量池是放在 Perm 区中的,Perm 区和正常的 JAVA Heap 区域是完全分开的。上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern
方法也是没有任何关系的。所以但会的都是false。
- 对于 JDK7:
public void test06_intern() {
String s1 = new String("1"); //生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象
s1.intern(); // 生成一个 s2的引用指向常量池中的“1”对象。
String s2 = "1"; // 常量池中已经有“1”这个对象了,所以直接返回。
System.out.println(s1 == s2); // 最后比较s1、s2都指向同一个对象,所以是true
/**
* 生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。
* 中间还有2个匿名的`new String("1")`我们不去讨论它们。此时s3引用对象内容是"11",但此时常量池中是没有 “11”对象的。
*/
String s3 = new String("1") + new String("1");
/**
* 将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,因此常规做法是跟 jdk6 图中表示的那样。
* 在常量池中生成一个 "11" 的对象,关键点是 jdk7 中常量池不在 Perm 区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。
* 这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。
*/
s3.intern();
String s4 = "11"; // "11"是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。
System.out.println(s3 == s4); // 所以最后比较是true。
}
- 这里如果我们修改一下
s3.intern();
这句的顺序
String s3 = new String("1") + new String("1");
String s4 = "11"; // 声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。
s3.intern(); // 常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。
System.out.println(s3 == s4); // 所以最终结果是false
- 还有一些详细的性能测试可以查看
http://java-performance.info/string-intern-in-java-6-7-8/
http://java-performance.info/string-intern-java-6-7-8-multithreaded-access/
3. 其他常用方法
String
除此之外还提供了很多其他方法,主要都是操作CharSequence
的,另外这里大致讲一下UTF-16编码。
Unicode(统一码、万国码、单一码)是为了解决传统的字符编码方案的局限而产生的。
- 在存贮英文和一些常见字符的时候用到的区域叫做 BMP(Basic Multilingual Plane)基本多文本平面,这个区域占两个字节;
- 当超出 BMP 范围的时候,使用的是辅助平面(Supplementary Planes)中的码位,在UTF-16中被编码为一对16比特长的码元(即32位,4字节),称作代理对;Unicode标准现在称高位代理为前导代理(lead surrogates),称低位代理为后尾代理(trail surrogates)。
四、StringBuilder和StringBuffer
由于String对象是不可变的,所以进行字符串拼接的时候就可以使用StringBuilder
和StringBuffer
两个类。他们和String的关系如下:
从图中可以看到StringBuilder
和StringBuffer
也是实现的CharSequence
接口,同时他们实现了Appendable
接口,具有对字符串动态操作的能力。
从他们父类AbstractStringBuilder
的源码来看:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0)
ensureCapacityInternal(minimumCapacity);
}
public AbstractStringBuilder append(***) {
....
}
public AbstractStringBuilder insert(***) {
....
}
}
- 他们同样持有一个字符数组
char[] value
,并且每次在对字符串进行操作的时候需要首先对数组容量进行确定,不足的时候需要扩容。 - 他们每个对字符串进行操作的方法,都会返回自身,所以我们可以使用链式编程的方式进行操作。另外现在还有一种通过泛型类定义链式操作的方式。
public class A<T extends A> {
public T **(**) {
return t;
}
}
StringBuilder
和StringBuffer
的 API 都是互相兼容的,只是StringBuffer的每个方法都用的synchronized
进行同步,所以是线程安全的。
1. 操作符重载
每当我们要就行字符串拼接的时候,自然会使用到+
,同时+
和+=
也是 java 中仅有的两个重载操作符。
public void test07_StringBuffer() {
String s1 = "a" + "b";
s1 += "c";
System.out.println(s1);
}
javap -v **.class
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: ldc #37 // String ab
2: astore_1
3: new #16 // class java/lang/StringBuilder
6: dup
7: invokespecial #17 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #19 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #38 // String c
16: invokevirtual #19 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #20 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_1
23: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_1
27: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
LineNumberTable:
line 83: 0
line 84: 3
line 85: 23
line 86: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 this LJDK/Test01_string;
3 28 1 s1 Ljava/lang/String;
从字节码大致可以看出编译器每次碰到”+”的时候,会new一个StringBuilder出来,接着调用append方法,在调用toString方法,生成新字符串,所以在字符串连接的时候,有很多“+”的时候,可以直接使用StringBuffer
或者StringBuilder
以提高性能;当然如果遇到类似String s = “a” + “b” + “c” + ...
类似的连续加号的时候,JVM 会自动优化为一个 StringBuilder。
五、switch对String的支持
String str = "b";
switch (str) {
case "a":
System.out.println(str);
break;
case "b":
System.out.println(str);
break;
default:
System.out.println(str);
break;
}
System.out.println(str);
javap -v **:
code:
...
8: invokevirtual #8 // Method java/lang/String.hashCode:()I
11: lookupswitch { // 2
97: 36
98: 50
default: 61
}
...
// 可以看到是首先拿到String的hashcode,在进行switch操作的
总结
本来想就这样结束的,但是总觉得不写点总结什么的就不完整。。。另外文章中还有很多细节没有写完,比如 String 在用于锁对象时,需要使用 intern 来保证是同一把锁。。。
参考链接
https://tech.meituan.com/in_depth_understanding_string_intern.html