Java——String类详解

引言

这里主要从虚拟机层面完整的讲解一下String类的对象存储位置。以及看似相同的String类的对象的比较。在此,针对一个对象的初始化的过程,我就不细讲了,其实可以参考我的另外一篇博客,虽然没有讲得很细,但是大体的逻辑过程是没有问题,待以后有时间再去细化和完善。随便提一下,在jdk1.6及以下版本,字符串常量池是放在方法区的运行时常量池中的,静态变量也是存储于方法区的,但是在jdk1.7及以上版本,字符串常量池和静态变量被放入了Java堆内存区域中,所以,我以下的讲解中,会两种版本对比着讲解。我会尽可能去注意下,我的每一句话是否有歧义,以保证读者能准确的理解。

字符串常量池

字符串常量池,我主要从两个角度去阐释,第一、字符串常量池存放的是什么内容?我准确的告知一下,存放的是字符串对象。有人会疑惑,Java堆区不就是用来存对象的吗?字符串常量池又存对象是怎么回事?我可以先告诉一下,Java堆中和Java字符串常量池中的会存在着,Java代码看起来一模一样,但理论上不等,实际却可能相等的两个对象。不懂没关系,待我下面细讲完之后,可再回来看着一句话,如果很清晰,那么说明,这个知识点,你就基本掌握了。第二,放入字符串常量池的字符串对象是长什么样子的?

String s = "test";
String s1 = new String("test1");

上面代码块的第一行代码,创建了一个字符串对象,存放在字符串常量池中。而第二行代码,存放了两个对象,一个是new出来的String对象,放在堆区,这个对象存放的内容是字符串test,另一个是存放于字符串常量池的字符串对象。 所以,可以明显的看出,当出现双引号包裹字面量的时候,就会生成一个字符串对象,存入字符串常量池。

String类的不可变性

String类是一个final类,所以是不可继承的,所以可操作对象的方法,就仅限于String类下所定义的方法,没有一个是对于现存的String类的对象,内容上进行修改的。也就是说,String类的对象,一旦创建,就是不可修改内容的,当我们想要修改其内容时,均是创建一个新的String类的对象,而不是再原有的对象上进行内容的更改。

String类的对象是否相等的问题

我们知道,在类加载得过程中的加载阶段,类加载子系统会完成这么一个动作,将class文件中的二进制字节流所表示的静态存储结构,转化为方法区的运行时数据结构。此时,就会在常量池加载各种显示的字面量和符号引用。
而当运行方法的时候,在栈帧中的动态链接部分,则会将符号引用转化为直接应用,也就是此时在字符串常量池中生成字符串对象。

String s = "test";
String s1 = new String("test1");

还是上面的代码,像变量s,可以说,在编译期,我就能确定它的值了,而变量s1则是运行期,我才能确定s1的值。当然,是在除开他们的域信息,也就是访问权限不是final和static的情况下。

String s = "test";
String s1 = "test1";
String s2 = "test" + "test1";
String s3 = s + "test1";
String s4 = s + s1;
String s5 = "testtest1";
System.out.println(s2 == s5);
System.out.println(s2 == s3);
System.out.println(s2 == s4);
System.out.println(s4 == s3);

上面的代码的输出,除了第一行是true,全部都是false,这里讲解的内容主要是关于字符串的拼接的,其实变量s,s1,s2,s5,由于编译期已知,所以s2的字符串的拼接操作直接就给优化拼在一起了,所以第一行就返回的是true,而s3,s4由于存在着变量,对于jvm来说,可不管你的变量是否已有确定的引用,我就是一个机器,我只知道你现在呈现给我的是一个变量而已,我翻译不了,我只是被设计为,将二进制字节流对应位置的静态数据结构翻译为运行时数据结构然后存储而已,自此,也能理解了s2为什么能直接拼接在一起,因为数据都已知的,而且数据在静态数据中的位置肯定是相邻的,都是字符串,所以就直接翻译转化为一个整体咯,且类加载子系统没有那么冗长的被设计成去增加判断是不是一个变量,然后再判断引用什么的。所以s3,s4都是运行时才能知道,到底是个啥玩意。
那么当String类型的s3和s4是运行期才知道的时候,它们的字符串的拼接又是另一种情况了,此时,会先在创建一个StringBuffer对象,因为String类型我们知道是一个不可变类型,我们是没办法进行拼接操作的,但每次操作都去创建新对象的话,无论是内存还是性能都会大打折扣,所以出现了StringBuffer这样的类,而StringBuffer对象就可以进行字符串拼接操作而不需要生成新对象,调用的是append方法进行拼接,拼接完成后又调用一下toString方法将这个StringBuffer对象转化为一个在堆中new出来的String对象。所以s3,s4都是堆中new出来的String对象,那自然是不等的。

String中的intern()方法

最后说一下,String中比较重要的intern()方法,不得不提到,那道经典的intern()方法的面试题。相比在我上述的讲解下,应该很好理解了。这里我就不完整的展现了,说说主要的逻辑就行,也当做上面内容的一个小检验。
intern方法的作用,简单来说就是,如果常量池中存在当前的字符串对象,则返回字符串常量池中该字符串对象的引用,也就是地址。如果常量池中不存在,在jdk1.6及以下版本中,会先在常量池中创建,然后返回其应用,而在jdk1.7及以上版本中,会将当前堆中已经存在的new String()对象的内容的引用,也就是堆中new String()对象的地址返回给字符串常量池。

String s1 = new String("test1");
s1.intern();
String s2 = "test1";
System.out.println(s1 == s2);

jdk1.6及以下版本,必然不等,因为s1的地址肯定在Java堆中,s2则是指向的方法区的常量池部分。
jdk1.7及以上版本,此时s1的地址是堆区new的一个String类的对象的地址,而这个地址的内容是字符串常量池的地址,此时s1调用intern方法发现,常量池中已经存在了"test1"的字符串对象,所以会返回常量池中"test1"对象的引用,也就是地址给当前堆区的new String()对象作为内容。所以s1的地址还是当前new String()对象的地址,只不过它的内容是常量池中"test1"对象的地址,而s2的地址是常量池中"test1"的地址,所以也不等。

String s1 = new String("test1")+new String("test1");
s1.intern();
String s2 = "test1test1";
System.out.println(s1 == s2);

jdk1.6及以下版本中,还是不等,还是因为一个是堆的地址,一个是方法区的地址。
jdk1.7及以上版本中,此时是相等的,因为我们知道当前s1为堆区的一个new String()对象,其内容为"test1test1",当调用s1.intern()方法的时候,常量池中是不存在这样的对象的,所以此时常量池将返回堆区的"test1test1"的引用,即这个内容为"test1test1"的new String()对象的地址,当s2去获取常量池中"test1test1"的地址的时候,其实返回的是一个引用,也就是堆区的new String()这个对象的地址,所以此时的s1和s2是相等的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值