我认为这里要理解的要点是String
Java 对象及其内容之间的区别 -char[]
在私有value
字段下。String
基本上是char[]
数组的包装器,将其封装并使其无法修改,因此String
可以保持不可变。该类还String
记住实际使用了该数组的哪些部分(见下文)。这一切都意味着您可以有两个不同的String
对象(非常轻量级)指向同一个char[]
.
我将向您展示几个示例,以及hashCode()
每个String
和hashCode()
内部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
字节码
这是生成的字节码。请注意,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;
}
看?当您String
基于现有对象创建新对象时,它会重用 char[] value
. String
s 是不可变的,因此无需复制已知永远不会修改的数据结构。
我认为这是您问题的线索:即使您有两个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()
工作,这种方法的优点是什么,什么时候会导致大麻烦。