String可以说是Java中用的最广的一个类了。虽然大家平时经常使用String,但对于String中有些比较奇怪的行为表现可能会比较费解。

通常我们定义String有2种方式,分为字面量定义和对象定义,即:

String str = "hello" 和 String str = new String("hello")

那么这2种定义方式有什么区别呢?我们先来看一段代码,

猜猜打印的3行结果是什么?

答案是:false,true,false,不知道你猜对了吗。下面我们来一起分析内部的原因。

使用字面量和对象定义String的区别就在于,字面量定义的String,如果定义的String是第一次出现的,会被存放在字符串常量池,然后返回字符串常量池里的引用,否则则直接返回字符串常量池里的引用。而对象定义的String会被存放在堆里。

而字符串常量池和堆是2个不同的区域。在JDK6之前,字符串常量池位于永久代(Perm Gen)中,在JDK7之后,被移到了堆中。注意,虽然在JDK7中字符串常量池在堆中,它跟存放对象的堆仍然属于不同的区域,不能搞混。

最后在讲一下String.intern()吧。源码里的解释是,intern方法会检查字符串常量池中有无和原字符串相等的字符串(这里的相等指的是equals的调用结果是true),如果有,那么返回字符串常量池中的引用,如果没有,那么把原字符串放进字符串常量池中,同时返回引用。

因此我们便可以解释上面的代码运行结果。s1是通过字面量定义的,又abc是第一次出现,所以abc被存放在字符串常量池中,此时s1是字符串常量池中的引用。s2是通过对象定义的,因此直接在堆中生成一个字符串对象。因为s1和s2的引用地址不同,因此第一个比较为false。由于abc已经在字符串常量池中出现,因此s1.intern()直接返回了字符串常量池中的引用,因此第二个比较为true。def在字符串常量池中不存在(因为对象定义的,忘记了可以回顾上面的内容),因此s2.intern()会把def放进常量池,并返回常量池中的引用,而s2是堆中的引用,因此第三个比较是false。我画一张图展示一下。

为了进一步验证我们的想法,我们使用一些工具查看Java堆和字符串常量池中的信息。

首先使用Class文件解析器解析生成的Class文件,得到如下信息

图中用红色的线圈出来的就是字符串常量池中的字符串。然后通过jmap查看堆中对象信息

num 1那一行就是堆中的字符串对象,为什么会是[C呢,因为Java的String底层存储就是用的Char[],如果你还不理解,强烈推荐《深入理解JVM虚拟机》这本书。

总结一下:字面量定义的String总是会在字符串常量池,对象定义的String总是会在堆中,String.intern方法只会跟字符串常量池发生关系。因此在工作中强烈建议比较2个字符串是否相等使用equals而不是==,因为你并不知道比较的2个字符串是如何定义的(这里让我吹一下kotlin,kotlin里==等价于Java的equals)。