我认为这里要理解的要点是Java对象及其内容之间的区别 - 在私有值
字段下。 基本上是数组的包装器,封装它并使其无法修改,因此可以保持不可变。此外,该类还会记住实际使用此数组的哪些部分(见下文)。这一切都意味着您可以有两个不同的对象(非常轻量级)指向同一个 。String
char[]
String
char[]
String
String
String
char[]
我将向您展示几个示例,以及每个示例和内部字段(我将称之为文本以将其与字符串区分开来)。最后,我将显示输出,以及我的测试类的常量池。请不要将类常量池与字符串文本池混淆。它们并不完全相同。另请参阅了解 Javap 的常量池输出。hashCode()
String
hashCode()
char[] value
javap -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[] value
String
char[]
类中的两个字符串文本
让我们从最简单的例子开始。
Java 代码
String one = "abc";
String two = "abc";
顺便说一句,如果你只是简单地编写,Java编译器将在编译时执行串联,生成的代码将完全相同。这仅在编译时所有字符串都已知时才有效。"ab" + "c"
类常数池
每个类都有自己的常量池 - 常量值的列表,如果它们在源代码中多次出现,则可以重用这些值。它包括常见的字符串,数字,方法名称等。
以下是上面示例中常量池的内容。
const #2 = String #38; // abc
//...
const #38 = Asciz abc;
需要注意的重要一点是字符串指向的常量对象 () 和 Unicode 编码文本 () 之间的区别。String
#2
"abc"
#38
字节码
下面是生成的字节码。请注意,和引用都分配有相同的常量,指向字符串:one
two
#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
one
two
one.value
two.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;
}
看?当您基于现有对象创建新对象时,它会重用 。s 是不可变的,无需复制已知从不修改的数据结构。String
char[] value
String
我认为这是你问题的线索:即使你有两个对象,它们可能仍然指向相同的内容。正如你所看到的,物体本身非常小。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);
显然,是两个不同的对象,指向两个不同的文本。但是,为什么输出表明它们都指向同一个数组?!?one
two
char[]
23583040
23583040
11108810
8918249
我会把答案留给你。它将教你如何工作,这种方法的优点是什么,以及何时会导致大麻烦。substring()