我认为这里要了解的重点是在私有字段下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;
const #38 = Asciz abc;
需要注意的重要事项是字符串所指向的String
常量对象(#2
)和Unicode编码文本"abc"
(#38
)之间的区别。
字节码
这是生成的字节码。请注意,one
和two
引用均分配有#2
指向的相同常量"abc"
字符串:
ldc #2;
astore_1
ldc #2;
astore_2
输出量
对于每个示例,我将打印以下值:
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。显然,如果one
和two
指向同一对象,则one.value
和two.value
必须相等。
文字和 new String()
Java代码
现在我们都在等待该示例-一个字符串文字和一个String
使用相同文字的新文字。这将如何运作?
String one = "abc";
String two = new String("abc");
事实 "abc"
在源代码中两次使用常量应该给您一些提示...
类常量池
同上。
字节码
ldc #2;
astore_1
new #3;
dup
ldc #2;
invokespecial #4;
astore_2
仔细看!第一个对象的创建方法与上面相同,不足为奇。它只需要对常量池中已创建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);
Java编译器(至少是我的)不够聪明,无法在编译时执行此类操作,请看一下:
类常量池
突然我们以指向两个不同常量文本的两个常量字符串结尾:
const #2 = String #44;
const #3 = String #45;
const #44 = Asciz abc;
const #45 = Asciz ?abc;
字节码
ldc #2;
astore_1
ldc #3;
iconst_1
invokevirtual #4;
astore_2
拳头弦是照常构造的。通过首先加载常量"?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()
工作,这种方法的优点是什么,以及何时会导致大麻烦。