字符串文本池是对字符串对象的引用的集合,还是对象的集合先决条件类中的两个字符串文本文字和new String() 运行时修改和intern()

2022-08-31 13:40:55

在阅读了The SCJP Tip Line的作者Corey McGlone在javaranch网站上的文章后,我都感到困惑。命名为Strings,Literally和SCJP Java 6 Programmer Guide,由Kathy Sierra(javaranch的联合创始人)和Bert Bates撰写。

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

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

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

  • String s = "Hello";(假设堆上没有名为“Hello”的对象),将在堆上创建一个 String 对象,并将在字符串文本池(常量表)中放置对此对象的引用)"Hello"

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

现在,在本文中,用于创建字符串及其引用的运算符的解释有点令人困惑,因此我将文章本身的代码和解释放在下面。"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);
    }
}

在这种情况下,由于关键字,我们实际上最终会得到一个稍微不同的行为 在这种情况下,对两个字符串文本的引用仍然被放入常量表(字符串文本池)中,但是,当您来到关键字时,JVM有义务在运行时创建一个新的String对象, 而不是使用常量表中的那个。"new.""new,"

这是解释它的图表。

enter image description here

那么这是否意味着,字符串文本池也具有对此对象的引用?

这是Corey McGlone的文章链接

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

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

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

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

    这很好,但后来我被这句话弄糊涂了:

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

    书中说...一个新的字符串对象在正常(非池)内存中,“s”将引用它...而额外的文字“abc”将被放置在池中。

    书中的上述几行与科里·麦格隆(Corey McGlone)文章中的行发生了冲突。

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

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

请清除这个疑问,虽然在编写代码时这并不重要,但从内存管理方面非常重要,这就是我想清除这个基础的原因。


答案 1

我认为这里要理解的要点是Java对象及其内容之间的区别 - 在私有字段下。 基本上是数组的包装器,封装它并使其无法修改,因此可以保持不可变。此外,该类还会记住实际使用此数组的哪些部分(见下文)。这一切都意味着您可以有两个不同的对象(非常轻量级)指向同一个 。Stringchar[]Stringchar[]StringStringStringchar[]

我将向您展示几个示例,以及每个示例和内部字段(我将称之为文本以将其与字符串区分开来)。最后,我将显示输出,以及我的测试类的常量池。请不要将类常量池与字符串文本池混淆。它们并不完全相同。另请参阅了解 Javap 的常量池输出hashCode()StringhashCode()char[] valuejavap -c -verbose

先决条件

为了测试的目的,我创建了这样一个打破封装的实用程序方法:String

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

它将打印 ,有效地帮助我们了解此特定文本是否指向相同的文本。hashCode()char[] valueStringchar[]

类中的两个字符串文本

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

Java 代码

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

顺便说一句,如果你只是简单地编写,Java编译器将在编译时执行串联,生成的代码将完全相同。这仅在编译时所有字符串都已知时才有效。"ab" + "c"

类常数池

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

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

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

需要注意的重要一点是字符串指向的常量对象 () 和 Unicode 编码文本 () 之间的区别。String#2"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()onetwoone == twoonetwoone.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#2String

String two = new String(one);

输出

输出有点令人惊讶。第二对,表示对对象的引用是可以理解的 - 我们创建了两个对象 - 一个是在常量池中为我们创建的,第二个是手动创建的。但是,为什么,在地球上,第一对表明两个物体都指向同一个数组?!StringStringtwoStringchar[] value

41771
41771
8388097
16585653

当您查看 String(String) 构造函数的工作原理时,情况就会变得很清楚(此处已大大简化):

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

看?当您基于现有对象创建新对象时,它会重用 。s 是不可变的,无需复制已知从不修改的数据结构。Stringchar[] valueString

我认为这是你问题的线索:即使你有两个对象,它们可能仍然指向相同的内容。正如你所看到的,物体本身非常小。StringString

运行时修改和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);

显然,是两个不同的对象,指向两个不同的文本。但是,为什么输出表明它们都指向同一个数组?!?onetwochar[]

23583040
23583040
11108810
8918249

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


答案 2