本文主要介绍Java
中与字符串相关的一些内容,主要包括String
类的实现及其不变性、String
相关类(StringBuilder
、StringBuffer
)的实现 以及 字符串缓存机制的用法与实现。html
String
类的核心逻辑是经过对char
型数组进行封装来实现字符串对象,但实现细节伴随着Java
版本的演进也发生过几回变化。java
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
}
复制代码
在Java 6
中,String
类有四个成员变量:char
型数组value
、偏移量 offset
、字符数量 count
、哈希值 hash
。value
数组用来存储字符序列, offset
和 count
两个属性用来定位字符串在value
数组中的位置,hash
属性用来缓存字符串的hashCode
。git
使用offset
和count
来定位value
数组的目的是,能够高效、快速地共享value
数组,例如substring()
方法返回的子字符串是经过记录offset
和count
来实现与原字符串共享value
数组的,而不是从新拷贝一份。substring()
方法实现以下:github
String(int offset, int count, char value[]) {
this.value = value; // 直接复用原数组
this.offset = offset;
this.count = count;
}
public String substring(int beginIndex, int endIndex) {
// ...... 省略一些边界检查的代码 ......
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
复制代码
可是这种方式却颇有可能会致使内存泄漏。例如在以下代码中:面试
String bigStr = new String(new char[100000]);
String subStr = bigStr.substring(0,2);
bigStr = null;
复制代码
在bigStr
被设置为null
以后,其中的value
数组却仍然被subStr
所引用,致使垃圾回收器没法将其回收,结果虽然咱们实际上仅仅须要2
个字符的空间,可是实际却占用了100000
个字符的空间。数组
在Java 6
中,若是想要避免这种内存泄漏状况的发生,可使用下面的方式:缓存
String subStr = bigStr.substring(0,2) + "";
// 或者
String subStr = new String(bigStr.substring(0,2));
复制代码
在语句执行完以后,substring
方法返回的匿名String
对象因为没有被别的对象引用,因此可以被垃圾回收器回收,不会继续引用bigStr
中的value
数组,从而避免了内存泄漏。安全
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
}
复制代码
在Java 7
-Java 8
中,Java
对 String
类作了一些改变。String
类中再也不有 offset
和 count
两个成员变量了。substring()
方法也再也不共享 value
数组,而是从指定位置从新拷贝一份value
数组,从而解决了使用该方法可能致使的内存泄漏问题。substring()
方法实现以下:markdown
public String(char value[], int offset, int count) {
// ...... 省略一些边界检查的代码 ......
// 从原数组拷贝
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
public String substring(int beginIndex, int endIndex) {
// ...... 省略一些边界检查的代码 ......
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
复制代码
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;
/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
/** Cache the hash code for the string */
private int hash; // Default to 0
}
复制代码
为了节省内存空间,Java 9
中对String
的实现方式作了优化,value
成员变量从char[]
类型改成了byte[]
类型,同时新增了一个coder
成员变量。咱们知道Java
中char
类型占用的是两个字节,对于只占用一个字节的字符(例如,a-z
,A-Z
)就显得有点浪费,因此Java 9
中将char[]
改成byte[]
来存储字符序列,而新属性 coder
的做用就是用来表示value
数组中存储的是双字节编码的字符仍是单字节编码的字符。coder
属性能够有 0
和 1
两个值,0
表明 Latin-1
(单字节编码),1
表明 UTF-16
(双字节编码)。在建立字符串的时候若是判断全部字符均可以用单字节来编码,则使用Latin-1
来编码以压缩空间,不然使用UTF-16
编码。主要的构造函数实现以下:网络
String(char[] value, int off, int len, Void sig) {
if (len == 0) {
this.value = "".value;
this.coder = "".coder;
return;
}
if (COMPACT_STRINGS) {
byte[] val = StringUTF16.compress(value, off, len); // 尝试压缩字符串,使用单字节编码存储
if (val != null) { // 压缩成功,可使用单字节编码存储
this.value = val;
this.coder = LATIN1;
return;
}
}
// 不然,使用双字节编码存储
this.coder = UTF16;
this.value = StringUTF16.toBytes(value, off, len);
}
复制代码
咱们注意到String
类是用final
修饰的;全部的属性都是声明为private
的;而且除了hash
属性以外的其余属性也都是用final
修饰。这保证了:
String
类由final
修饰,因此没法经过继承String
类改变其语义;private
的, 因此没法在String
外部直接访问或修改其属性;hash
属性以外的其余属性都是用final
修饰,表示这些属性在初始化赋值后不能够再修改。上述的定义共同实现了String
类一个重要的特性 —— 不变性,即 String
对象一旦建立成功,就不能再对它进行任何修改。String
提供的方法substring()
、concat()
、replace()
等方法返回值都是新建立的String
对象,而不是原来的String
对象。
hash
属性不是final
的缘由是:String
的hashCode
并不须要在建立字符串时当即计算并赋值,而是在hashCode()
方法被调用时才须要进行计算。
为何String类要设计为不可变的?
String
对象的安全性。String
被普遍用做JDK
中做为参数、返回值,例如网络链接,打开文件,类加载,等等。若是 String
对象是可变的,那么 String
对象将可能被恶意修改,引起安全问题。String
类的不可变性自然地保证了其线程安全的特性。String
对象的hashCode
的不变性。String
类的不可变性,保证了其hashCode
值可以在第一次计算后进行缓存,以后无需重复计算。这使得String
对象很适合用做HashMap
等容器的Key
,而且相比其余对象效率更高。字符串常量池
。Java
为字符串对象设计了字符串常量池
来共享字符串,节省内存空间。若是字符串是可变的,那么字符串对象便没法共享。由于若是改变了其中一个对象的值,那么其余对象的值也会相应发生变化。除了String
类以外,还有两个与String
类相关的的类:StringBuffer
和StringBuilder
,这两个类能够看做是String
类的可变版本,提供了对字符串修改的各类方法。二者的区别在于StringBuffer
是线程安全的而StringBuilder
不是线程安全的。
StringBuffer
和StringBuilder
都是继承自AbstractStringBuilder
,AbstractStringBuilder
利用可变的char
数组(Java 9
以后改成为byte
数组)来实现对字符串的各类修改操做。StringBuffer
和StringBuilder
都是调用AbstractStringBuilder
中的方法来操做字符串, 二者区别在于StringBuffer
类中对字符串修改的方法都加了synchronized
修饰,而StringBuilder
没有,因此StringBuffer
是线程安全的,而StringBuilder
并不是线程安全的。
咱们以Java 8
为例,看一下AbstractStringBuilder
类的实现:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/** The value is used for character storage. */
char[] value;
/** The count is the number of characters used. */
int count;
}
复制代码
value
数组用来存储字符序列,count
则用来存储value
数组中已经使用的字符数量,字符串真实的内容是value
数组中[0,count)
之间的字符序列,而[count,length)
之间是未使用的空间。须要count
属性记录已使用空间的缘由是,AbstractStringBuilder
中的value
数组并非每次修改都会从新申请,而是会提早预分配必定的多余空间,以此来减小从新分配数组空间的次数。(这种作法相似于ArrayList
的实现)。
value
数组扩容的策略是:当对字符串进行修改时,若是当前的value
数组不知足空间需求时,则会从新分配更大的value
数组,分配的数组大小为min( 原数组大小×2 + 2 , 所需的数组大小 )
,更加细节的逻辑能够参考以下代码:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2; //原数组大小×2 + 2
if (newCapacity - minCapacity < 0) { // 若是小于所需空间大小,扩展至所需空间大小
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
复制代码
固然AbstractStringBuilder
也提供了trimToSize
方法去释放多余的空间:
public void trimToSize() {
if (count < value.length) {
value = Arrays.copyOf(value, count);
}
}
复制代码
由于String
对象的使用普遍,Java
为String
对象设计了缓存机制,以提高时间和空间上的效率。在JVM
的运行时数据区中存在一个字符串常量池
(String Pool
),在这个常量池中维护了全部已经缓存的String
对象,当咱们说一个String
对象被缓存(interned
)了,就是指它进入了字符串常量池
。
咱们经过解答下面三个问题来理解String
对象的缓存机制:
String
对象会被缓存进字符串常量池
?String
对象被缓存在哪里,如何组织起来的?String
对象是何时进入字符串常量池
的?说明: 如未特殊指明,本文中说起的
JVM
实现均指的是Oracle
的HotSpot VM
,而且不考虑 逃逸分析(escape analysis
)、标量替换(scalar replacement
)、无用代码消除(dead-code elimination
)等优化手段,测试代码基于不添加任何额外JVM
参数的状况下运行。
为了更好的阅读体验,在解答上面三个问题前,但愿读者对如下知识点有简单了解:
JVM
运行时数据区class文件
的结构JVM
基于栈的字节码解释执行引擎Java
中的几种常量池为了内容的完整性,咱们对下文涉及较多的其中两点作简要介绍。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期依次为:加载(Loading
)、验证(Verification
)、准备(Preparation
)、解析(Resolution
)、初始化(Initialization
)、使用(Using
)和卸载(Unloading
)7个阶段。其中验证、准备、解析3个部分统称为链接(Linking
)。 加载、验证、准备、初始化和卸载这5个阶段的顺序是肯定的,类的加载过程必须按照这种顺序循序渐进地开始,而解析阶段则不必定:它在某些状况下能够在初始化阶段以后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
1. class文件中的常量池 咱们知道java
后缀的源代码文件会被javac
编译为class
后缀的class文件
(字节码文件)。在class文件
中有一部份内容是 常量池(Constant Pool) ,这个常量池中主要存储两大类常量:
字面量
或者常量表达式
的值;2. 运行时常量池 在JVM
运行时数据区(Run-Time Data Areas)中,有一部分是运行时常量池(Run-Time Constant Pool),属于方法区
的一部分。运行时常量池
是class文件
中每一个类或者接口的常量池(Constant Pool
)的运行时表示形式,class文件
的常量池中的内容会在类加载后进入方法区的运行时常量池
。
3. 字符串常量池 字符串常量池
(String Pool
)也就是咱们上文提到的用来缓存String
对象的常量池。 这个常量池是全局共享的,属于运行时数据区的一部分。
在Java
中,有两种字符串会被缓存到字符串常量池
中,一种是在代码中定义的字符串字面量
或者字符串常量表达式
,另外一种是程序中主动调用String.intern()
方法将当前String
对象缓存到字符串常量池
中。下面分别对两种方式作简要介绍。
之因此称之为隐式缓存是由于咱们并不须要主动去编写缓存相关代码,编译器和JVM
会帮咱们完成这部分工做。
字符串字面量 第一种会被隐式缓存的字符串是 字符串字面量。字面量
是类型为原始类型、String
类型、null
类型的值在源代码中的表示形式。例如:
int i = 100; // int 类型字面量
double f = 10.2; // double 类型字面量
boolean b = true; // boolean 类型字面量
String s = "hello"; // String类型字面量
Object o = null; // null类型字面量
复制代码
字符串字面量
是由双引号括起来的0
个或者多个字符构成的。 Java
会在执行过程当中为字符串字面量
建立String
对象并加入字符串常量池
中。例如上面代码中的"hello"
就是一个字符串字面量
,在执行过程当中会先 建立一个内容为"hello"
的String
对象,并缓存到字符串常量池
中,再将s
引用指向这个String
对象。
关于字符串字面量
更加详细的内容请参阅Java语言规范
(JLS - 3.10.5. String Literals)。
字符串常量表达式 另一种会被隐式缓存的字符串是 字符串常量表达式。常量表达式
指的是表示简单类型值或String
对象的表达式,能够简单理解为常量表达式
就是在编译期间就能肯定值的表达式。字符串常量表达式
就是表示String
对象的常量表达式。例如:
int a = 1 + 2;
double d = 10 + 2.01;
boolean b = true & false;
String str1 = "abc" + 123;
final int num = 456;
String str2 = "abc" +456;
复制代码
Java
会在执行过程当中为字符串常量表达式
建立String
对象并加入字符串常量池
中。例如,上面的代码中,会分别建立"abc123"
和"abc456"
两个String
对象,这两个String
对象会被缓存到字符串常量池
中,str1
会指向常量池中值为"abc123"
的String
对象,str2
会指向常量池中值为"abc456"
的String
对象。
关于常量表达式
更加详细的内容请参阅Java语言规范
(JLS - 15.28 Constant Expressions)。
除了声明为字符串字面量
/字符串常量表达式
以外,经过其余方式获得的String
对象也能够主动加入字符串常量池
中。例如:
String str = new String("123") + new String("456");
str.intern();
复制代码
在上面的代码中,在执行完第一句后,常量池中存在内容为"123"
和"456"
的两个String
对象,可是不存在"123456"
的String
对象,但在执行完str.intern();
以后,内容为"123456"
的String
对象也加入到了字符串常量池
中。
咱们经过String.intern()
方法的注释来看下其具体的缓存机制:
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned. It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.
简单翻译一下:
当调用
intern
方法时,若是常量池中已经包含相同内容的字符串(字符串内容相同由equals (Object)
方法肯定,对于String
对象来讲,也就是字符序列相同),则返回常量池中的字符串对象。不然,将此String
对象将添加到常量池中,并返回此String
对象的引用。 所以,对于任意两个字符串s
和t
,当且仅当s.equals(t)
的结果为true
时,s.intern() == t.intern()
的结果为true
。
HotSpot VM
中,有一个用来记录缓存的String
对象的全局表,叫作StringTable
,结构及实现方式都相似于Java
中的HashMap
或者HashSet
,是一个使用拉链法解决哈希冲突的哈希表,能够简单理解为HashSet<String>
,注意它只存储对String
对象的引用,而不存储String
对象实例。 通常咱们说一个字符串进入了字符串常量池
实际上是说在这个StringTable
中保存了对它的引用,反之,若是说没有在其中就是说StringTable
中没有对它的引用。
而真正的字符串对象实际上是保存在另外的区域中的,在Java 6
中字符串常量池
中的String
对象是存储在永久代
(Java 8
以前HotSpot VM
对方法区
的实现)中的,而在Java 6
以后,字符串常量池
中的String
对象是存储在堆
中的。
Java 7
中将字符串常量池
中的对象移动到堆
中的缘由是在Java 6
中,字符串常量池
中的对象在永久代
建立,而永久代
代的大小通常不会设置太大,若是大量使用字符串缓存将可能对致使永久代
发生OOM
异常。
对于经过 在程序中调用String.intern()
方法主动缓存进入常量池的String
对象,很显然就是在调用intern()
方法的时候进入常量池的。
咱们重点来研究一下会被隐式缓存的两种值(字符串字面量
和字符串常量表达式
),主要是两个问题:
String
类的构造方法,那么它们是在什么时候被建立?字符串常量池
的?咱们如下面的代码为例来分析这两个问题:
public class Main {
public static void main(String[] args) {
String str1 = "123" + 123; // 字符串常量表达式
String str2 = "123456"; // 字面量
String str3 = "123" + 456; //字符串常量表达式
}
}
复制代码
咱们对上述代码编译以后使用javap
来观察一下字节码文件,为了节省篇幅,只摘取了相关的部分:常量池表部分以及main
方法信息部分:
Constant pool:
#1 = Methodref #5.#23 // java/lang/Object."<init>":()V
#2 = String #24 // 123123
#3 = String #25 // 123456
// ...... 省略 ......
#24 = Utf8 123123
#25 = Utf8 123456
// ...... 省略 ......
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String 123123
2: astore_1
3: ldc #3 // String 123456
5: astore_2
6: ldc #3 // String 123456
8: astore_3
9: return
LineNumberTable:
line 7: 0
line 8: 3
line 9: 6
line 10: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 str1 Ljava/lang/String;
6 4 2 str2 Ljava/lang/String;
9 1 3 str3 Ljava/lang/String;
复制代码
在常量池
中,有两种与字符串相关的常量类型,CONSTANT_String
和CONSTANT_Utf8
。CONSTANT_String
类型的常量用于表示String
类型的常量对象,其内容只是一个常量池的索引值index
,index
处的成员必须是CONSTANT_Utf8
类型。而CONSTANT_Utf8
类型的常量用于存储真正的字符串内容。 例如,上面的常量池中的第2
、3
项是CONSTANT_String
类型,存储的索引分别为24
、25
,常量池中第24
、25
项就是CONSTANT_Utf8
,存储的值分别为"123123"
,"123456"
。
class文件
的方法信息中Code
属性是class文件
中最为重要的部分之一,其中包含了执行语句对应的虚拟机指令,异常表,本地变量信息等,其中LocalVariableTable
是本地变量的信息,Slot
能够理解为本地变量表中的索引位置。ldc
指令的做用是从运行时常量池
中提取指定索引位置的数据并压入栈中;astore_<n>
指令的做用是将一个引用类型的值从栈中弹出并保存到本地变量表的指定位置,也就是<n>
指定的位置。能够看出三条赋值语句所对应的字节码指令其实都是相同的:
ldc #<index> // 首先将常量池中指定索引位置的String对象压入栈中
astore_<n> // 而后从栈中弹出刚刚存入的String对象保存到本地变量的指定位置
复制代码
仍是围绕上面的代码,咱们结合 从编译到执行的过程 来分析一下字符串字面量
和字符串常量表达式
的建立及缓存时机。
1. 编译 首先,第一步是javac
将源代码编译为class
文件。在源代码编译过程当中,咱们上文提到的两种值 字符串字面量
("123456"
) 和 字符串常量表达式
("123" + 456
)这两类值都会存在编译后的class文件
的常量池中,常量类型为CONSTANT_String
。值得注意的两点是:
字符串常量表达式
会在编译期计算出真实值存在class
文件的常量池
中。例如上面源代码中的"123" + 123
这个表达式在class
文件的常量池中的表现形式是123123
,"123" + 456
这个表达式在class
文件的常量池中的表现形式是123456
;字符串字面量
或者字符串常量表达式
在class文件
的常量池中只会存在一个常量项(CONSTANT_String
类型和CONSTANT_Utf8
都只有一项)。例如上面源代码中,虽然声明了两个常量值分别为"123456"
和"123" + 456
,可是最后class
文件的常量池中只有一个值为123456
的CONSTANT_Utf8
常量项以及一个对应的CONSTANT_String
常量项。2. 类加载 在JVM
运行时,加载Main
类时,JVM
会根据 class文件
的常量池 建立 运行时常量池
, class文件
的常量池 中的内容会在类加载时进入方法区的 运行时常量池
。对于class文件
的常量池中的符号引用,会在类加载的解析(resolve)阶段
,会将其转化为真正的值。但在HotSpot
中,符号引用的解析
并不必定是在类加载时当即执行的,而是推迟到第一次执行相关指令(即引用了符号引用的指令,JLS - 5.4.3. Resolution )时才会去真正进行解析,这就作延迟解析
/惰性解析
("lazy" or "late" resolution
)。
CONSTANT_Integer_info
,CONSTANT_Float_info
,CONSTANT_Long_info
,CONSTANT_Double_info
,在类加载阶段会将class
文件常量池中的值转化为运行时常量池
中的值,分别对应C++
中的int
,float
,long
,double
类型;CONSTANT_Utf8
类型的常量项,在类加载的解析阶段被转化为Symbol
对象(HotSpot VM
层面的一个C++
对象)。同时HotSpot
使用SymbolTable
(结构与StringTable
相似)来缓存Symbol
对象,因此在类加载完成后,SymbolTable
中应该有全部的CONSTANT_Utf8
常量对应的Symbol
对象;CONSTANT_String
类型的常量项,由于其内容是一个符号引用(指向CONSTANT_Utf8
类型常量的索引值),因此须要进行解析,在类加载的解析阶段会将其转化为java.lang.String
对象对应的oop
(能够理解为Java
对象在HotSpot VM
层面的表示),并使用StringTable
来进行缓存。可是CONSTANT_String
类型的常量,属于上文提到的延迟解析
的范畴,也就是在类加载时并不会当即执行解析,而是等到第一次执行相关指令时(通常来讲是ldc
指令)才会真正解析。3. 执行指令 上面提到,JVM
会在第一次执行相关指令的时候去执行真正的解析,对于上文给出的代码,观察字节码能够发现,ldc
指令中使用到了符号引用,因此在执行ldc
指令时,须要进行解析操做。那么ldc
指令到底作了什么呢?
ldc
指令会从运行时常量池
中查找指定index
对应的常量项,并将其压入栈中。若是该项还未解析,则须要先进行解析,将符号引用转化为具体的值,而后再将其压入栈中。若是这个未解析的项是String
类型的常量,则先从字符串常量池
中查找是否已经有了相同内容的String
对象,若是有则直接将字符串常量池
中的该对象压入栈中;若是没有,则会建立一个新的String
对象加入字符串常量池
中,并将建立的新对象压入栈中。可见,若是代码中声明多个相同内容的字符串字面量
或者字符串常量表达式
,那么只会在第一次执行ldc
指令时建立一个String
对象,后续相同的ldc
指令执行时相应位置的常量已经解析过了,直接压入栈中便可。
总结一下:
字符串字面量
或者字符串常量表达式
转化为了class文件
的常量池中的CONSTANT_String
常量项。class文件
的常量池
中的CONSTANT_String
常量项被存入了运行时常量池
中,但保存的内容仍然是一个符号引用,未进行解析。ldc
指令时,运行时常量池
中的CONSTANT_String
项还未解析,会真正执行解析,解析过程当中会建立String
对象并加入字符串
常量池。能够看到,其实ldc
指令在解析String
类型常量的时候与String.intern()
方法的逻辑很类似:
ldc
指令中解析String
常量:先从字符串常量池
中查找是否有相同内容的String
对象,若是有则将其压入栈中,若是没有,则建立新对象加入字符串常量池
并压入栈中。String.intern()
方法:先从字符串常量池
中查找是否有相同内容的String
对象,若是有则返回该对象引用,若是没有,则将自身加入字符串常量池
并返回。实际在HotSpot
内部实现上,ldc
指令 与 String.intern()
对应的native
方法 调用了相同的内部方法。咱们以OpenJDK 8
的源代码为例,简单分析一下其过程,代码以下(源码位置:src/share/vm/classfile/SymbolTable.cpp
):
// String.intern()方法会调用这个方法
// 参数 "oop string"表明调用intern()方法的String对象
oop StringTable::intern(oop string, TRAPS)
{
if (string == NULL) return NULL;
ResourceMark rm(THREAD);
int length;
Handle h_string (THREAD, string);
jchar* chars = java_lang_String::as_unicode_string(string, length, CHECK_NULL); // 将String对象转化为字符序列
oop result = intern(h_string, chars, length, CHECK_NULL);
return result;
}
// ldc指令执行时会调用这个方法
// 参数 "Symbol* symbol" 是 运行时常量池 中 ldc指令的参数(索引位置)对应位置的Symbol对象
oop StringTable::intern(Symbol* symbol, TRAPS) {
if (symbol == NULL) return NULL;
ResourceMark rm(THREAD);
int length;
jchar* chars = symbol->as_unicode(length); // 将Symbol对象转化为字符序列
Handle string;
oop result = intern(string, chars, length, CHECK_NULL);
return result;
}
// 上面两个方法都会调用这个方法
oop StringTable::intern(Handle string_or_null, jchar* name, int len, TRAPS) {
// 尝试从字符串常量池中寻找
unsigned int hashValue = hash_string(name, len);
int index = the_table()->hash_to_index(hashValue);
oop found_string = the_table()->lookup(index, name, len, hashValue);
// 若是找到了直接返回
if (found_string != NULL) {
ensure_string_alive(found_string);
return found_string;
}
// ...... 省略部分代码 ......
Handle string;
// 尝试复用原字符串,若是没法复用,则会建立新字符串
// JDK 6中这里的实现有一些不一样,只有string_or_null已经存在于永久代中才会复用
if (!string_or_null.is_null()) {
string = string_or_null;
} else {
string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
}
//...... 省略部分代码 ......
oop added_or_found;
{
MutexLocker ml(StringTable_lock, THREAD);
// 添加字符串到 StringTable 中
added_or_found = the_table()->basic_add(index, string, name, len,
hashValue, CHECK_NULL);
}
ensure_string_alive(added_or_found);
return added_or_found;
}
复制代码
说明:由于在
Java 6
以后字符串常量池
从永久代
移到了堆
中,可能在一些代码上Java 6
与以后的版本表现不一致。因此下面的代码都使用Java 6
和Java 7
分别进行测试,若是未特殊说明,表示在两个版本上结果相同,若是不一样,会单独指出。
final int a = 4;
int b = 4;
String s1 = "123" + a + "567";
String s2 = "123" + b + "567";
String s3 = "1234567";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
复制代码
结果:
false
true
false
复制代码
解释:
a
被定义为常量,因此"123" + a + "567"
是一个常量表达式
,在编译期会被编译为"1234567"
,因此会在字符串常量池
中建立"1234567"
,s1
指向字符串常量池
中的"1234567"
;b
被定义为变量,"123"
和"567"
是字符串字面量
,因此首先在字符串常量池
中建立"123"
和"567"
,而后经过StringBuilder
隐式拼接在堆中建立"1234567"
,s2
指向堆中的"1234567"
;"1234567"
是一个字符串字面量
,由于此时字符串常量池
中已经存在了"1234567"
,因此s3
指向字符串字符串常量池
中的"1234567"
。String s1 = new String("123");
String s2 = s1.intern();
String s3 = "123";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
复制代码
结果:
false
false
true
复制代码
解释:
"123"
是一个字符串字面量
,因此首先在字符串常量池
中建立了一个"123"
对象,而后使用String
的构造函数在堆中建立了一个"123"
对象,s1
指向堆中的"123"
;字符串常量池
中已经有了"123"
,因此s2
指向字符串常量池
中的"123"
;字符串常量池
中已经有了"123"
,因此s3
指向字符串常量池
中的"123"
。String s1 = String.valueOf("123");
String s2 = s1.intern();
String s3 = "123";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
复制代码
结果:
true
true
true
复制代码
解释:与上一种状况的区别在于,String.valueOf()
方法在参数为String
对象的时候会直接将参数做为返回值,不会在堆上建立新对象,因此s1
也指向字符串常量池
中的"123"
,三个变量指向同一个对象。
String s1 = new String("123") + new String("456");
String s2 = s1.intern();
String s3 = "123456";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
复制代码
上面的代码在Java 6
和Java 7
中结果是不一样的。 在Java 6
中:
false
false
true
复制代码
解释:
"123"
和"456"
是字符串字面量
,因此首先在字符串常量池
中建立"123"
和"456"
,+
操做符经过StringBuilder
隐式拼接在堆中建立"123456"
,s1
指向堆中的"123456"
;"123456"
缓存到字符串常量池
中,由于Java 6
中字符串常量池
中的对象是在永久代建立的,因此会在字符串常量池
(永久代)建立一个"123456"
,此时在堆中和永久代中各有一个"123456"
,s2
指向字符串常量池
(永久代)中的"123456"
;"123456"
是字符串字面量
,由于此时字符串常量池
(永久代)中已经存在"123456"
,因此s3
指向字符串常量池
(永久代)中的"123456"
。在Java 7
中:
true
true
true
复制代码
解释:与Java 6
的区别在于,由于Java 7
中字符串常量池
中的对象是在堆
上建立的,因此当执行第二行String s2 = s1.intern();
时不会再建立新的String
对象,而是直接将s1
的引用添加到StringTable
中,因此三个对象都指向常量池中的"123456"
,也就是第一行中在堆中建立的对象。
Java 7
下,s1 == s2
结果为true
也可以用来佐证咱们上面延迟解析
的过程。咱们假设若是"123456"
不是延迟解析的,而是类加载的时候解析完成并进入常量池的,s1.intern()
的返回值应该是常量池中存在的"123456"
,而不会将s1
指向的堆中的"123456"
对象加入常量池,因此结果应该是s2
不等于s1
而等于s3
。
String s1 = new String("123") + new String("456");
String s2 = "123456";
String s3 = s1.intern();
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
复制代码
结果:
false
false
true
复制代码
解释:
"123"
和"456"
是字符串字面量
,因此首先在字符串常量池
中建立"123"
和"456"
,+
操做符经过StringBuilder
隐式拼接在堆中建立"123456"
,s1
指向堆中的"123456"
;"123456"
是字符串字面量,此时字符串常量池中不存在"123456"
,因此在字符串常量池
中建立"123456"
, s2
指向字符串常量池
中的"123456"
;"123456"
,因此s3
指向字符串常量池
中的"123456"
。