73

阅读 SCJP Tip Line 的作者 Corey McGlone 在 javaranch 网站上的文章后,我感到很困惑。由 Kathy Sierra(javaranch 的联合创始人)和 Bert Bates 命名的 Strings、Literally 和 SCJP Java 6 程序员指南。

我将尝试引用 Corey 先生和 Kathy Sierra 女士所引用的关于字符串文字池的内容。

1.根据 Corey McGlone 先生的说法:

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

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

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

现在 "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,"JVM 必须在运行时创建一个新的 String 对象,而不是使用常量表中的那个。

这是解释它的图表..

在此处输入图像描述

那么这是否意味着 String Literal Pool 也有对这个 Object 的引用?

这是 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 s = new String("abc")// 创建两个对象和一个引用变量。

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

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

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

    • 这个字符串文字池在哪里?

请清除这个疑问,虽然在写代码的时候并没有太大的关系,但是从内存管理的角度来看是非常重要的,这就是我要清除这个基础的原因。

4

1 回答 1

112

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

我将向您展示几个示例,以及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。显然,如果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[] value. Strings 是不可变的,因此无需复制已知永远不会修改的数据结构。

我认为这是您问题的线索:即使您有两个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();

在打印哈希码之前。不仅两者都one指向two同一个文本,而且它们是同一个引用!

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()工作,这种方法的优点是什么,什么时候会导致大麻烦

于 2012-07-28T12:27:32.883 回答