String字符串常量池

2018-01-13 11:14:18来源:oschina作者:lopssh人点击

分享

理解这个概念的关键所在就是理解String对象和其内容之间的关系,这里的内容也就是String对象的私有成员`value`,它是一个char数组。


String类只是个简单的外封装,它包装了该char数组并提供了一系列方法来操作这个数组,同时还将保持这个数组的不变性(即不会修改数组本身,而会对修改产生新的String对象),同时String对象还会记住数组的哪一部分是有被实际使用到的。


那么这一切便就意味着你可以拥有两个不同的String对象(相当轻量级的)来指向同一个char数组。


(PS:个人批注,除了基本类型外,其它的类型均是引用类型,包括数组也是一样,所以String对象内的`value`成员,实际上是对char数组的一个引用)。


接下来我将会带你看几个例子,以及观察每个String对象的hashCode()和它们对应的char数组的hashCode() ,然后我还会贴出 javap -c -verbose 的输出结果,以及展示我所提供的测试类的常量池。请不要把常量池和字面量池混淆,它们会有些许的不同。


预先准备

为了达到我们的测试目的,我创建了一个用于解开String对象包装的实用方法:


private int showInternalCharArrayHashCode(String s) {
final Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
return value.get(s).hashCode();
}

这个方法将打印出char数组的hashCode(),这将有助于我们理解特定的String是否指向了同样的char数组。

两个字面量存在同一个class文件中
String one = "abc";
String two = "abc";

顺带一提,如果你简单的写出"ab" + "c",java编译器将会在编译期进行串联,生成的最终代码将会与上面生成的一毛一样,当然这种情况也只限于编译器知道你的所有字符串的情况下才是等效的。(ps:个人批注,例如 "a" + 变量b + "c" 中有不确定的变量b,因此是无法自动串联在一起的)

类常量池

每个class文件有一个自己的常量池 ,常量池维护了一个在代码中多次出现的常量的集合。它包含了经常出现的字符串,数值,方法名,以及其它的内容。


这里贴出上面的示例代码的常量池内容 :


const #2 = String #38;//abc
//...
const #38 = Asciz abc;

重要的是要注意的是字符串常量对象(#2)和字符串指向的Unicode编码文本“abc”(#38)之间的区别。

字节码

这里是生成的字节码。 请注意,一个和两个引用都分配了相同的#2常量,指向“abc”字符串:


ldc #2; //String abc
astore_1//one
ldc #2; //String abc
astore_2//two 输出

我将为每一条测试语句使用如下的代码打印输出:


System.out.println(showInternalCharArrayHashCode(one));
System.out.println(showInternalCharArrayHashCode(two));
System.out.println(System.identityHashCode(one));
System.out.println(System.identityHashCode(two));

没有发生任何奇怪的地方,这一次的两对输出是相等的:


23583040
23583040
8918249
8918249

这也就意味着不仅仅是每个对象指向了相同的char数组所以它们相等,还意味着变量 one 和 two 是两个相同的引用,因此 one == two 的结果必然会是true, 很显然,如果变量 one 和 变量two 指向了同一个String对象,那么 one.value 和 two.value 也一定会相等。

字面量 以及 new String()

现在,我将这个例子调整成我们最想看到的样子 —— 一个 普通字面量 以及 一个new String 将使用相同的字面量,那么它将会如何工作?


String one = "abc";
String two = new String("abc");

实际上这个"abc"字面量在源码当中被使用了两次,这将会给你些许暗示。

类常量池

同样的,也与先前我们所看到的没有什么区别。


字节码
ldc #2; //String abc
astore_1//one
new #3; //class java/lang/String
dup
ldc #2; //String abc
invokespecial #4; //Method java/lang/String."":(Ljava/lang/String;)V
astore_2//two

这里一定要注意看,第一个对象的创建与先前所展示的一毛一样,它只是获取了常量池当中已被创建的String (#2) 的一个 常量引用。而第二个对象的创建则通过常规的构造函数进行。但是!第一个String对象将作为参数传入第二个对象的构造函数,你可以理解为:


String two = new String(one); 输出

这次的输出有点让人惊讶,第二对表示String对象的引用输出是可以理解的 —— 因为我们创建了两个String对象,一个String对象是在常量池为我们创建的(PS:个人批注,我理解为所谓的 运行时常量池,就是把常量池中的部分东西实例化后 所存放的地方 ,例如 字符串字面量实例化成一个String对象,而该对象的引用则被虚拟机所持有,并在代码需要该常量String的时候 赋值给代码中使用到的地方 ),而第二个String对象则是我们手工进行创建的。但是为什么,但为什么 第一对输出 表明这两个字符串对象指向相同的char [] 数组?!


41771
41771
8388097
16585653

然而当你看了String(String)构造函数的源码后你就会一目了然(因为这非常容易解释这一现象):


public String(String original) {
this.offset = original.offset;
this.count = original.count;
this.value = original.value;
}

看到了吗?当你基于一个已存在的String对象 来创建一个新的String对象的时候,将会重复使用char 数组,String对象是不会变的,在不涉及对String对象发起变更操作的时候,根本不会拷贝内存,只是简单的复制了引用。

运行时修改 及 intern()

接下来再让我们来修改String看看会发生什么变化:


String one = "abc";
String two = "?abc".substring(1);//also two = "abc"

java 编译器并不够聪明,在编译期间不会求出第二句的值,因而你将会看到如下的情况:

类常量池

我们看到了两个常量字符串引用引向了两个不同的常量字面量:


const #2 = String #44;//abc
const #3 = String #45;//?abc
const #44 = Asciz abc;
const #45 = Asciz ?abc; 字节码
ldc #2; //String abc
astore_1//one
ldc #3; //String ?abc
iconst_1
invokevirtual #4; //Method String.substring:(I)Ljava/lang/String;
astore_2//two

第一个字符串同先前一样。第二个字符串通过"?abc"进行创建,并调用了substring(1)。

输出

这里没有什么特别的地方 —— 因为我们有两个不同的字符串,而且指向了不同的 char[] 数组:


27379847
7615385
8388097
16585653

嗯,当然这两个char数组并非是实际意义上不同的,equals() 方法仍然会返回true。只是我们现在有了同一个字符串的两个副本。


现在我们将要做一件事情,试着运行如下代码:


two = two.intern();

执行后 再来看看 hashCode。 现在不仅仅是 one 和 two 指向了相同的 char数组,就连它们两个也是相同的引用了!


11108810
11108810
15184449
15184449

这也意味着one.equals(two) 和 one == two 会通过测试。于此同时 我们还节省了部分的内存空间,因为字符串 "abc" 这个char数组 现在只在内存中仅有一份拷贝 (而先前的那一个将会因为没有引用而被垃圾回收器回收)

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台