是string文字池汇集对string对象或对象集合的引用
SCJP Tip Line的作者Corey McGlone写了一篇关于javaranch网站的文章,我感到很困惑。 命名为Strings, Kathy Sierra (co-founder of javaranch) and Bert Bates
由Kathy Sierra (co-founder of javaranch) and Bert Bates
编写的SCJP Java 6程序员指南。
我会试着引用科里先生和凯西·谢拉女士所引用的关于弦乐文字池的内容。
1.按照科里·麦克隆先生的说法:
-
string文字池是指向string对象的引用的集合。
-
String s = "Hello";
(假设堆上没有名为“Hello”的对象),将在堆上创build一个String对象"Hello"
,并将该对象的引用放置在String Literal Pool(常量表) -
String a = new String("Bye");
(假设堆上没有名为“Bye”的对象,new
运算符将使JVM在堆上创build一个对象。
现在对于创build一个String的"new"
运算符的解释和它的引用在本文中有点混乱,所以我把这个文章本身的代码和解释放在下面。
public class ImmutableStrings { public static void main(String[] args) { String one = "someString"; String two = new String("someString"); System.out.println(one.equals(two)); System.out.println(one == two); } }
在这种情况下,由于关键字"new."
,我们实际上最终会有一个稍微不同的行为"new."
在这种情况下,对这两个string的引用仍然被放到常量表(string文字池)中,但是当您使用关键字"new,"
,JVM必须在运行时创build一个新的String对象,时间,而不是使用常数表中的一个。
这是解释它的图。
那么这是否意味着,string文字池也有这个对象的引用?
以下是Corey McGlone撰写的文章的链接
http://www.javaranch.com/journal/200409/Journal200409.jsp#a1
2.根据SCJP书中的Kathy Sierra和Bert Bates的说法:
-
为了使Java更高效地存储内存,JVM在编译器遇到string文字时,留出一个称为“string常量池”的特殊区域,检查该池是否已经存在。 如果不是那么它会创build一个新的string文字对象。
-
String s = "abc";
//创build一个String对象和一个引用variables….这很好,但现在下面的声明让我感到困惑。
-
String s = new String("abc")
//创build两个对象和一个引用variables。它在书中说,….在正常(非池)内存中的一个新的String对象,“s”将引用它…而额外的文字“abc”将被放置在池中。
本书中的上述内容与Corey McGlone的文章中的内容相冲突。
-
如果String Literal Pool是Corey McGlone提到的String对象的引用的集合,那么如何将字面值对象“abc”放置在池中,如本书所述。
-
而这个string文字池驻留在哪里。
-
请明白这个疑问,虽然编写代码并不重要,但是从内存pipe理的angular度来看非常重要,这就是我想要澄清这个问题的原因。
我认为这里要理解的重点是String
Java对象和它的内容之间的区别 – char[]
在私有value
字段下 。 String
基本上是char[]
数组的封装,封装它,使其不可能修改,所以String
可以保持不变。 而且String
类还记得这个数组的哪个部分被实际使用(见下文)。 这一切都意味着你可以有两个不同的String
对象(相当轻量)指向同一个char[]
。
我将向您展示几个示例,以及每个String
hashCode()
和内部char[] value
字段(我将其称为text来区分string)的hashCode()
)。 最后,我将显示javap -c -verbose
输出,以及用于testing类的常量池。 请不要将类常量池与string文字池混淆。 他们不完全一样。 另请参见了解常量池的javap输出 。
先决条件
为了testing的目的,我创build了一个打破String
封装的实用方法:
private int showInternalCharArrayHashCode(String s) { final Field value = String.class.getDeclaredField("value"); value.setAccessible(true); return value.get(s).hashCode(); }
它将打印char[] value
hashCode()
,有效地帮助我们理解这个特定的String
是否指向相同的char[]
文本。
两个string文字在一个类中
我们从最简单的例子开始。
Java代码
String one = "abc"; String two = "abc";
顺便说一句,如果你只是写"ab" + "c"
,Java编译器将在编译时执行连接,生成的代码将完全相同。 这只适用于所有string在编译时已知的情况。
类常量池
每个类都有自己的常量池 – 一个常量值列表,如果它们在源代码中出现多次,可以重用。 它包括常用string,数字,方法名称等
这里是我们上面例子中常量池的内容。
const #2 = String #38; // abc //... const #38 = Asciz abc;
重要的是要注意的是String
常量对象( #2
)和string指向的Unicode编码文本"abc"
( #38
)之间的区别。
字节码
这里是生成的字节码。 请注意, one
和two
引用都分配了相同的#2
常量,指向"abc"
string:
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[]
(下面的文本),所以equals()
testing将会通过。 但更多的是, one
和two
是完全相同的参考! 所以one == two
也是如此。 显然,如果one
和two
指向同一个对象,那么one.value
和two.value
必须相等。
文字和new String()
Java代码
现在我们都等待的例子 – 一个string文字和一个新的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."<init>":(Ljava/lang/String;)V astore_2 //two
仔细地看! 第一个对象的创build方式与上面相同,不足为奇。 它只是对常量池中的已经创build的String
( #2
)进行常量引用。 然而,第二个对象是通过正常的构造函数调用创build的。 但! 第一个String
作为parameter passing。 这可以反编译为:
String two = new String(one);
产量
输出有点令人惊讶。 表示对String
对象的引用的第二对是可以理解的 – 我们创build了两个String
对象 – 一个是在常量池中为我们创build的,第二对是为two
手动创build的。 但为什么在地球上第一对表明这两个String
对象指向相同的char[] value
数组?
41771 41771 8388097 16585653
当你看看String(String)
构造函数是如何工作的 (这里大大简化了)
public String(String original) { this.offset = original.offset; this.count = original.count; this.value = original.value; }
看到? 当你创build一个新的String
对象的时候,它会重用 char[] value
。 String
是不可变的,不需要复制已知从未被修改的数据结构。
我认为这是你的问题的线索:即使你有两个String
对象,他们仍然可能指向相同的内容。 正如你所看到的, String
对象本身是相当小的。
运行时修改和intern()
Java代码
假设您最初使用了两个不同的string,但经过一些修改后,它们都是一样的:
String one = "abc"; String two = "?abc".substring(1); //also two = "abc"
Java编译器(至less是我的)在编译时并不够聪明,无法执行这样的操作,看看:
类常量池
突然之间,我们结束了两个常量string指向两个不同的常量文本:
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
第一个string像往常一样构build。 第二个是通过首先加载常量"?abc"
string,然后调用substring(1)
。
产量
这里没有什么惊喜 – 我们有两个不同的string,指向内存中的两个不同的char[]
文本:
27379847 7615385 8388097 16585653
那么,文本是不是真的不同 , equals()
方法仍然会产生true
。 我们有两个不必要的同一文本的副本。
现在我们应该进行两个练习。 首先,尝试运行:
two = two.intern();
打印哈希码之前。 不仅one
, two
指向相同的文字,但它们是相同的参考!
11108810 11108810 15184449 15184449
这意味着one.equals(two)
和one == two
testing都会通过。 我们还保存了一些内存,因为"abc"
文本在内存中只出现一次(第二个副本将被垃圾回收)。
第二个练习略有不同,看看这个:
String one = "abc"; String two = "abc".substring(1);
显而易见, one
和two
是两个不同的对象,指向两个不同的文本。 但是输出如何表明它们都指向相同的char[]
数组?
23583040 23583040 11108810 8918249
我会把答案留给你。 它会告诉你substring()
是如何工作的,这种方法的优点是什么,什么时候会导致很大的麻烦 。
我回头阅读CodeRanch文章,我不认为你所说的内容是精确的。 例如,你已经错过了在类加载时分配"Hello"
的String对象的关键点,而不是在执行语句的时候。 我怀疑你对SCJP文本的总结同样是不准确的……而且由于你的阅读/总结不准确,你的“疑惑”(很大一部分)。
不幸的是,由于我们只能依靠你在SCJP案中的总结,所以很难回答你的问题是哪个文本是正确的。
我期望答案是两个都是对的,而且他们用不同的方式大致说了同样的话。
In Java, there is no operator overloading whatsoever, and that's why the comparison operators are only overloaded for the primitive types. The 'String' class is not a primitive, thus it does not have an overloading for '==' and uses the default of comparing the address of the object in the computer's memory. I'm not sure, but I think that in Java 7 or 8 oracle made an exception in the compiler to recognize str1 == str2 as str1.equals(str2)