关于String类字符串在JVM的存储地址问题,结合字符串拼接、intern()方法的详细理解。

2 篇文章 0 订阅
1 篇文章 0 订阅

最近看了网上一些关于String的intern()方法的例子,以及字符串拼接的博客,彻底把我的思绪搞乱了,我对String的理解又被打回了原点,所以决定再一探究竟。


在开始正式探讨之前,我们先要回顾这几个知识点:
String直接赋值与使用new的区别
当String类直接赋值时,如果常量池内存在这个字符串,则s1直接指向常量池的地址,若没有,则先在常量池内创建这个字符串对象,s1直接指向常量池这个字符串的内存地址; 当String类使用new实例对象时,首先在堆里创建这个对象,若是常量池内没这个字符串,则也创建一个,然后堆里的对象的value指向常量池内的字符串。
String变量做“+”运算时的编译优化
编译器内部对于String字符串变量拼接,会创建一个StringBuilder,对于每一个要拼接的内容,调用append进行添加,最后在使用toString()方法返回成字符串。
intern()方法
当对象调用该本地方法时,会去找字符串是否存在于常量池内,如若没有则在常量池再创建一个字符串对象(注意:我这里说的是JDK1.6时的这个方法处理)。
JDK1.6和1.7时,常量池的位置
在JDK1.6以及以前版本中,常量池是放在 Perm 区(属于方法区)中的,和堆区完全分开的;但是到JDK1.7之后,字符串常量池被移回到堆里面了。


说完这些知识点,相信大家一定一头雾水,不知我要表达什么,那我就直接以代码来一步步提出问题:

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

这段代码相信很多人都知道,坑点就是s1是在常量池中建立了字符串对象,s2会在堆中创建,然后直接引用常量池的s1字符串(这个点我在上一篇博客也有讲到);
而另外一个面试常问的点是,把上面第一段代码注释后,问s2创建了几个对象?答案是2个,跟之前知识点说的那样,一个在常量池,一个在堆中,很多朋友都明白,但是其实却不明白为什么会这样。因此,我们来看看String的构造方法是怎么写的:

public String(String original) {
    //...构造内容
}

很多朋友可能不信为什么是调用包含String类型参数的构造方法,最简单的验证方式就是在此方法中打断点,调用时就跳进此方法中了。所以是不是明白了什么,当字符串Hello在被此构造方法调用前,已经转化为String对象了,也就是说,当new一个String对象时,这个字符已经先在常量池里建立了对象,如下顺序代码:

String t = "Hello"; //在常量池建立字符串对象
String str = new String(t); //在堆中建立对象,引用指向常量池

其实说到这里,才是本文我想着重说的东西哦,正如上面代码说到的,引用指向常量池,那是什么的引用指向了常量池呢?
我们先看下面的代码:

String str = new String("Hello") + String("World");
String s = "HelloWorld";
System.out.println(str == s);

当执行第一句代码时,我们大多数菜鸟的第一反应是:常量池中存在“Hello”、”World”、“HelloWorld”这三个字符串,所以输出的是true。
其实不然,运行结果是false。我们再看看下面代码:

String str = new String("Hello") + String("World");
str.intern(); 
String s = "HelloWorld";
System.out.println(str == s);

以下结果又和上面形成鲜明对比:但JDK1.6时输出false,JDK1.7时输出true。
究竟是为什么,仅仅是加上这么一行代码,结果就会有差异呢?
不卖关子,我先说结论,两个new出来的对象在做“+“运算时,首先在常量池创建HelloWorld字符是没错的,但是后面拼接后的字符HelloWorld是存在堆中的
为什么这么说,我们回到之前提到的问题,对象中的引用为什么指向常量池?
来看看String类的整个源码,就清楚了:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence
{
    private final char value[];
    private final int offset;
    private int hash; // Default to 0
    ...
}

我们看到String类中私有属性有哈希、偏移量,还有value字符数组,记住这个value,这就是这篇博客要讨论的关键元素。
因为”+“拼接时用到StringBuilder的append()方法,我们再看看其源代码:

public StringBuilder append(String str) {
    super.append(str);
    return this;
}

append()是继承父类AbstractStringBuilder的方法,但是不难看出,方法返回的是StringBuilder类型,所以当转换成String类型时,肯定需要调用toString()的方法:

public String toString() {
    return new String(value, 0, count);
}

直接new一个String类返回,那么再进入String带这三个参数的构造方法:

public String(char value[], int offset, int count) {
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}

我们发现String对象的value就是对StringBuilder的对象的value进行拷贝,StringBuilder的value是同对象一起存放在堆中,那么拼接后的字符HelloWorld字符串value也是在堆中,并没有被添加到常量池;而用new一个String对象时,构造方法String(String original),对象original已经在常量池中创建,那么它的value也是存在常量池中,所以new出来的对象的value是引用常量池中字符对象的value地址
开头知识点说,JDK1.6时,intern()方法检查String对象中字符串是否存在常量池中,不存在会在常量池新创建一个该字符对象,意思就是上面那段例子代码,拼接的字符串value在堆中,当对象调用intern()方法,常量池内也创建一个该字符的String对象,对象调用intern()方法,是指向了常量池中String对象的value地址,所以才会(str == s)为false,而如果(str.intern() == s)就会是true;
那么JDK1.7,(str == s)为什么又是true呢?当JDK1.7时,intern()方法有所改动,当String对象的字符串不在常量池中,intern()方法会在常量池建立对象,将value引用的地址指向堆中的value,省去了再开辟value的空间成本。个人推荐一篇挺受用的博客:https://blog.csdn.net/seu_calvin/article/details/52291082
所以我们常用来做String字符串”==“比较的,其实就是比较字符串对象中value的地址是否相同


和大多数菜鸟程序员一样,之前我对String的理解都是一些皮毛,顶多知道有个字符串常量池就以为很够用了。相信看了这篇博客,大家一定会对String有了新的理解,我们可以从根源去了解这个类的结构情况,而且这些知识点也是大家面试中常考的基础坑点,希望我的这篇博客对大家有所帮助。
其实学习就是这样,从一无所知到一知半解,再茶饭不思要把它搞懂,小编愿与大家一同学习成长,谢谢!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值