字符串文字池是对字符串对象的引用集合,还是对象的集合


73

阅读了SCJP提示行作者Corey McGlone在javaranch网站上的文章后,我都感到困惑。从字面上看是Strings,由Kathy Sierra(javaranch的联合创始人)和Bert Bates共同编写的《 SCJP Java 6程序员指南》。

我将尝试引用Corey先生和Kathy Sierra女士对String Literal Pool的引用。

1.根据科里·麦格隆先生的说法:

  • 字符串文字池是指向字符串对象的引用的集合。

  • String s = "Hello"; (假设"Hello"堆上没有名为“ Hello”的对象),将在堆上创建一个String对象,并将对该对象的引用放入String Literal Pool(常量表)中。

  • String a = new String("Bye");(假定堆上没有名为“ Bye”new的对象,操作员将要求JVM在堆上创建一个对象。

现在,在本文中"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." 在这种情况下,对两个String文字的引用仍然放在常量表(String Literal Pool)中,但是,当您使用关键字时"new,"JVM必须在运行时创建一个新的String对象,而不是使用常量表中的对象。

这是解释它的图。

在此处输入图片说明

这是否意味着String Literal Pool也具有对此对象的引用?

这是Corey McGlone的文章链接

http://www.javaranch.com/journal/200409/Journal200409.jsp#a1

2.根据SCJP书中的Kathy Sierra和Bert Bates的说法:

  • 为了使Java的内存使用效率更高,JVM预留了一个特殊的内存区域,称为“字符串常量池”,当编译器遇到String Literal时,它将检查该池以查看是否已经存在相同的String。如果没有,它将创建一个新的字符串文字对象。

  • String s = "abc"; //创建一个String对象和一个引用变量。

    很好,但随后我对以下声明感到困惑:

  • String s = new String("abc") //创建两个对象和一个引用变量。

    书中说:...在普通(非池)内存中有一个新的String对象,而“ s”将引用该对象……而在池中将附加一个文字“ abc”。

    本书中的上述几行与Corey McGlone的文章中的那行相冲突。

    • 如果字符串文字池是Corey McGlone提到的对String对象的引用的集合,那么为什么要将文字对象“ abc”放在池中(如书中所述)?

    • 字符串文字池位于何处?

请清除此疑问,尽管在编写代码时并不太重要,但是从内存管理的角度来看这非常重要,这就是我要清除此基础的原因。


1
池的管理方式可能在某种程度上取决于JVM的实现。只要语言规范未解决某些问题,它们就可以自由进行实验。因此,我相信池中是否包含引用或对象可能完全取决于。
MvG 2012年

我最近遇到了与您提到的2种资源完全相同的问题。我认为通过声明对象“ abc”将放置在书中的池中,作者表示对对象“ abc”的引用将存储在池中。对?接受的答案非常有用,但我认为这是要问的问题。
mustafa1993

Answers:


111

我认为这里要了解的重点是在私有字段StringJava对象及其内容之间的区别。基本上是数组的包装器,将其封装并使其无法修改,因此可以保持不变。另外,类记住此阵列的部分被实际使用(见下文)。这一切都意味着您可以拥有两个指向同一个对象(相当轻量)。char[]valueStringchar[]StringStringStringchar[]

我会告诉你一些例子,连同hashCode()每一个StringhashCode()内部的char[] value领域(我将其称之为文本从字符串相区别)。最后,我将显示javap -c -verbose输出以及测试类的常量池。请不要将类常量池与字符串文字池混淆。它们并不完全相同。另请参见了解常量池的javap输出

先决条件

为了进行测试,我创建了一种破坏String封装的实用程序方法:

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

这将打印hashCode()char[] value,有效地帮助我们理解这是否特定String指向相同的char[]文字或没有。

一个类中的两个字符串文字

让我们从最简单的示例开始。

Java代码

String one = "abc";
String two = "abc";

顺便说一句,如果您只是编写"ab" + "c",Java编译器将在编译时执行串联,并且生成的代码将完全相同。仅当在编译时知道所有字符串时,此方法才有效。

类常量池

每个类都有自己的常量池-常量值列表,如果它们在源代码中多次出现,则可以重用。它包括常见的字符串,数字,方法名称等。

这是上面示例中常量池的内容。

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

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

字节码

这是生成的字节码。请注意,onetwo引用均分配有#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[](下面的相同文本),所以equals()测试将通过。但更多,one并且two是完全相同的参考!因此one == two也为true。显然,如果onetwo指向同一对象,则one.valuetwo.value必须相等。

文字和 new String()

Java代码

现在我们都在等待该示例-一个字符串文字和一个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

仔细看!第一个对象的创建方法与上面相同,不足为奇。它只需要对常量池中已创建String#2)的常量引用。但是,第二个对象是通过常规构造函数调用创建的。但!第一个String作为参数传递。可以将其反编译为:

String two = new String(one);

输出量

输出有点令人惊讶。第二对代表String对象的引用是可以理解的-我们创建了两个String对象-一个是在常量池中为我们创建的,第二个是为手动创建的two。但是为什么在地球上第一对暗示两个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;
}

看到?在String基于现有对象创建新对象时,它会重用 char[] valueStrings是不可变的,因此无需复制已知永远不会修改的数据结构。

我认为这就是您问题的线索:即使您有两个String对象,它们可能仍指向相同的内容。如您所见,String对象本身很小。

运行时修改和 intern()

Java代码

假设您最初使用了两个不同的字符串,但是在进行一些修改之后,它们都是相同的:

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

好吧,文字并没有什么不同equals()方法仍然会产生true。我们有两个相同文本的不必要副本。

现在我们应该进行两次练习。首先,尝试运行:

two = two.intern();

在打印哈希码之前。不仅双方onetwo指向相同的文字,但它们是相同的参考!

11108810
11108810
15184449
15184449

这意味着one.equals(two)one == two测试都将通过。我们还节省了一些内存,因为"abc"文本在内存中仅出现一次(第二个副本将被垃圾回收)。

第二个练习略有不同,请查看以下内容:

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

显然,onetwo是两个不同的对象,指向两个不同的文本。但是输出如何表明它们都指向同一个char[]数组?!

23583040
23583040
11108810
8918249

我将答案留给你。它会教您如何substring()工作,这种方法的优点是什么,以及何时会导致大麻烦


2
+1,感谢您对String的深入了解。但是上面提供的信息是我已经有一个想法。.我的问题仍然存在...字符串文字池是对象或引用的集合吗?如果字符串文字池保存对象,那么将使用new运算符将创建两个String对象,一个在内存池外部,一个在内存池内部,并且引用指向一个在内存池外部的对象。
库玛·维维克·米特拉

1
+1个出色的答案。但是我认为在上一个示例的输出中,您正在为String对象而不是vlaue char数组获得类似的hashCode。
Eng.Fouad

6
字符串文字池是对字符串对象的引用的集合,还是对象的集合,但是确切的答案是什么?
smileVann 2013年

3
从Java 7开始,子字符串为数组的一部分创建了一个新副本,该副本并不指向同一数组。
海森堡2014年

4
该答案未提供初始问题的答案。字符串池在内部是什么样的?它是引用的集合还是字符串的集合?
尤金·梅瑟克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.