Java基础探索之String
String是Java开发中使用非常频繁的类,本文将对String的源码和设计进行探索。
关键字:java、String、immutable、intern、hash
文章来源:陈同学 Java基础探索之String
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash; // Default to 0
}
直观来看,String及成员变量存在以下特性:
- String被final修饰,即String不可被继承
- String的实际存储为字符数组
value
,且value被final修饰,即value指向的数组引用不可变更
了解不可变(immutable)对象
什么是不可变对象?
如果一个对象在创建之后,不能再改变它的状态,那么这个对象就是不可变的。
状态指对象的成员变量,有以下情况:
- 成员变量是基础数据类型,那么其值不可改变
- 成员变量是引用类型,那么引用指向的对象不可改变(引用不能指向其他对象,但对象本身还是可以改变的)
为什么要设计不可变对象?
immutable对象存在以下几个优点:
- 线程安全,因为不可改变(read-only),因此可以被多个线程安全使用。
- 基于线程安全特性,可以在任何地方重复使用,提高性能。
- 保证了hashcode的唯一性,如果缓存hashcode,在频繁使用对象时可以不用重复计算hashcode
怎么创建一个不可变对象?
本质上就是不提供任何方式改变对象的状态,以下是一些细节:
- 使用final修饰类,确保没有任何子类可以变更其不可变的特性
- 成员变量使用final修饰
- 不提供任何改变对象状态的方法
- 若对对象进行修改,使用拷贝的方式,不要返回对象本身
为什么String要设计成immutable?
Why do we need immutable class?
这个问题应该拓展至一门语言为什么需要设计immutable数据类型?为什么基础数据类型往往都是immutable的?
程序是通过代码逻辑操纵内存中的数据并最终持久化下来。内存中的数据呈现显示为各程序设计语言中的数据类型,而持久化的存储可以是DB、磁盘等。
在程序运行时,数据会在程序中不断进行传递和变更,将基础数据类型设计成immutable类型可以使得软件构建变更容易,因为不用担心数据会在处理过程中发生变更。
Java中为8中基础数据类型和String都设计了缓存池。
String对象真的不可变吗?
因为String对象实际存储是字符串数组,虽然无法直接变更引用所指向的对象,但是可以直接变更对象。
也就是说:String对象的引用不可变,但是引用指向的对象可以被改变
public static void main(String[] args) throws Exception {
// 创建字符串"hello", 将引用s指向对象"hello". s用final修饰,无法显示将s指向其他对象
final String s = "hello";
System.out.println(s.hashCode());
// 获取String对象中的value字段
Field valueFiled = String.class.getDeclaredField("value");
// 将value字段设置为可访问的
valueFiled.setAccessible(true);
// 通过反射获取s引用指向的对象的"value"字段的值
char[] value = (char[]) valueFiled.get(s);
// 变更对象"hello"中的第一个字符为"H", s引用还是指向这个对象
value[0] = 'H';
System.out.println(s);
System.out.println(s.hashCode());
}
输入的结果:
通过反射变更了实际的对象,但是没有改变引用值,引用还是指向这个对象。由于String缓存了hashcode,所以即使值变了,但是hashcode却没变。
99162322
Hello
99162322
设计字符串常量池的意义在哪?
程序开发中涉及到许多池的概念,如:线程池、数据库连接池、字符串池、Spring容器管理的单例对象以及其它的一些Buffer设计等。设计这些概念并实现,无非是出于提高性能、节约资源(如内存)之类的考虑。
由于String是immutable对象,天然的具备线程安全特性,因为可以作为全局共享对象。
最为重要的是,String是Java中使用最为广泛的类型,与8大基础数据类型并列。实际应用中,往往String的使用更为频繁。因此设计常量池缓存String对象,可以带来如下几个好处:
-
节约内存,遇到大量重复String时节约内存
-
提高效率,遇到大量重复String时不用频繁创建对象
字符串常量池在不同JDK版本中如何存储?
- JDK1.6以前
常量池位于方法区中的Perm区
- jdk1.7以后
常量池位于堆中,JDK1.8时JVM内存模型已经移除了Perm区,由Metaspace代替
String、StringBuilder、StringBuffer的区别?
StringBuilder
- 利用char[]作为buffer存储数据,每次append时都是通过System.arraycopy做数据拷贝
- char[]的长度默认16,不够时将会自动扩容
- 非线程安全,适合单线程下字符串拼接场景
StringBuffer
特性和StringBuilder一样,但是每个操作方法中都用了synchronized
做同步处理
适合多线程下字符串拼接场景
String其他的有趣点
intern()函数的作用?
JDK源码注释:When the intern method is invoked, if the pool already contains a string equal to this object as determined by the method,then the string from the pool is returned;Otherwise, this object is added to the pool and a reference to this object is returned.
当调用intern方法时时,如果常量池已存在该字符串,则返回其引用;否则先将该字符串加入常量池,再返回其引用。
特别注意:JDK1.6及以下和JDK及以上版本对于intern的处理逻辑有变化。
假设:str为指向堆中字符串实例的引用,且String Pool中不存在值相同的字符串实例
- JDK1.6:调用str.intern()时,若str在String Pool不存在,将拷贝一份到String Pool
- JDK1.7:调用str.intern()时,若str在String Pool不存在,String Pool将存储堆中对象的引用
下面通过一段代码加两张图来解释下:
String s1 = new String("H") + new String("i");
String s2 = s1.intern();
String s3 = "Hi";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
jdk1.6
jdk1.7
执行s1.intern()时,String Pool也只会保存一个指向堆中对象的引用,不再像1.6那样复制一个对象实例到String Pool中
理解String的加法运算
开发中经常会碰到String变量之间的加法操作,那JVM实际上是如何处理的呢?
下面看一个例子:
String s1 = "Hello";
String s2 = "Kitty";
String s3 = s1 + s2;
System.out.println(s3 == "HelloKitty"); // false
s3 = s1 + s2
到底会怎么处理?我们看下这段代码对应的字节码指令:
0: ldc #2 // String Hello
2: astore_1
3: ldc #3 // String Kitty
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
25: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
28: aload_3
29: ldc #9 // String HelloKitty
31: if_acmpne 38
34: iconst_1
35: goto 39
38: iconst_0
39: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
本质上,String变量的加法运算是通过StringBuilder来处理。s3 = s1 + s2实际对应的代码应该是:
new StringBuilder().append("Hello").append("Kitty").toString();
而StringBuilder的toString()源码为:
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
因此String变量的加法操作实际上是在堆中创建了一个String对象,同时返回对象在堆中的引用。
String的hashCode函数为何要设计缓存?
String作为使用最频繁的类型,其很多细微的设计都非常有趣。先回顾下String的属性:
- value[] 字符数组用于存储数据
- hash用来缓存hash code
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
}
hashCode的源码:
如果hash码已计算好,将不再进行再次计算,直接返回cache的hash.
这样设计主要是考虑到一方面String使用非常频繁,另一方面String经常所为一些数据结构的检索字段,例如:Map. 缓存hash可以提升性能
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;
}
String的immutable特性
String是不可变对象,前面我们也探讨了怎么样设计一个不可变对象。这里来看看String的情况:
- 类命、实际存储数据的字符数组都用final修饰
- 没有提供任何setter方法
- 操作性的函数都是拷贝新的对象
针对第三点,我们看几个实际的函数:
replace:返回的是new String
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
...
return new String(buf, true);
...
}
return this;
}
substring: 返回的是new String
public String substring(int beginIndex, int endIndex) {
...
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
编译器对于String的优化
现在编译器会对String的操作做些基本的优化,下面用代码举例:
编译前的Java代码
String s1 = "H" + "i";
final String s2 = "H";
String s3 = s2 + "i";
String s4 = s1 + s2;
编译后的class进行反编译
String s1 = "Hi";
String s2 = "H";
String s3 = "Hi";
String s4 = s1 + "H";