剖析new String()创建几个对象?及不同的方式创建的string对象有什么区别?

今天看到一个很有意思的高频面试题,其中的内容确确实实很有意思,下面我们来聊聊,先来看几道面试题,试试水平;

目录

面试题类型1:

面试题类型2:

 解释:

目前来看,关于 new String("xxx") 创建对象个数的答案有 3 种:

  首先回顾「字符串常量池」:

下面我们看看JVM是如何处理面试题类型一中的前两个代码的:

新知识引入:

使用+拼接字符串的实现原理


面试题类型1:

分别执行下面代码:请回答执行他们的过程中总共创建了几个字符串对象?

代码一:new String("xxx");//一个或两个

代码二:String str = "abc" + "def";//0个或一个

代码三:String s=new String(“xxx”);//一个或两个

代码四:String str = "abc" + new String("xxx");//5个

面试题类型2:

回答如果执行下列代码,分别返回的结果都是什么?

String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2);//true
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s==s1);//false
String s1 = new String("javaer-wang");
String s2 = new String("javaer-wang");
System.out.println(s1 == s2);//false
String s1 = "abc";
String s2 = new String("def");
String s3 = s1 + s2;
String s4 = "abcdef";
System.out.println(s3==s4); //false

 解释:

接下来我们一边解释面试题,一边总结相关知识点;

目前来看,关于 new String("xxx") 创建对象个数的答案有 3 种:

  1. 有人说创建了 1 个对象;
  2. 有人说创建了 2 个对象;
  3. 有人说创建了 1 个或 2 个对象。

而出现多个答案的关键争议点在 【字符串常量池 】上,有的说 new 字符串的方式会在常量池创建一个字符串对象,有人说 new 字符串的时候并不会去字符串常量池创建对象,而是在调用 intern() 方法时,才会去字符串常量池检测并创建字符串,那么实际情况是怎样的呢?我们接下来慢慢聊。。。。

  首先回顾「字符串常量池」:

  • 字符串的分配和其他的对象分配一样,需要耗费高昂的时间和空间为代价,如果需要大量频繁的创建字符串,会极大程度地影响程序的性能,因此 JVM 为了提高性能和减少内存开销引入了字符串常量池(Constant Pool Table)的概念。
  • 字符串常量池相当于给字符串开辟一个常量池空间类似于缓存区,对于直接赋值的字符串(String s="xxx")来说,在每次创建字符串时优先使用已经存在字符串常量池的字符串,如果字符串常量池没有相关的字符串,会先在字符串常量池中创建该字符串,然后将引用地址返回变量,如下图:


下面我们看看JVM是如何处理面试题类型一中的前两个代码的:

JVM如何执行String s="abc";
        JVM在执行String s="abc"时,会先在常量池中查找是否存在"abc"这个字符串,如果存在,则将s1指向该字符串;如果不存在,则在常量池中创建一个新的字符串"abc",然后将s指向该字符串。如果在后续的代码中使用了相同的字符串字面量,JVM会重用之前创建的String对象,而不是创建新的对象。

JVM如何执行new String("abc");
        当执行 new String("abc") 时,JVM 会首先在常量池中查找是否已经存在 "abc" 这个字符串,如果存在,则直接返回该字符串的引用;如果不存在,则在堆中创建一个新的 String 对象,并将 "abc" 这个字符串的引用赋值给该对象。同时,该对象会被添加到常量池中,以便下次使用时直接返回该对象的引用。

        你也可以这样理解;new String("abc")相当于new String(String s1="abc"),即先要执行String s1="abc",然后再在堆区new一个String对象。

因此,现在可以解答本文的标题了,String s=new String("abc")创建了1或2个对象,String s="abc"创建了0或1个对象。

我在博客上看到一位博主对以上执行过程分析为:

String s="abc"会先从字符串常量池(下文简称常量池)中查找,如果常量池中已经存在"abc",而"abc"必定指向堆区的某个String对象,那么直接将s指向这个String对象即可;

我并不认可这句话(仅仅代表我的观点),下面我们用例子来证明我的观点; 

按照以上说法;下列代码返回的值应该为true,我们实际执行看结果;

String s2="abc";
String s1=new String("abc");
String s3=new String("abc");
System.out.println(s1==s2);//false
System.out.println(s1==s3);//false

 我们再次通过一个例子更加贴切的理解一下该过程(我们在看一个例子):

底层原理图与原理我们用画图来解释;

 String类型的数据都存放在常量池,堆中的属性只存放常量池中值的地址!

①    String str1 = "abc";语句是直接创建了字符串,是构造字符串最常用的方式。这种直接赋值的方式,并没有用new关键字在堆中开辟新的地址,而是在常量池中开辟了地址。所以引用str1的内容是常量池中abc字符串的地址0x01。

②   String str2 = "abc";也是直接创建了字符串。但是因为String是特殊的引用类型,其存放在常量池的内容是不可变的。在创建一个字符串对象时,会先在常量池中查询是否已存在,若不存在则再开辟新的存储空间。此时str2所引用的对象abc和str1是一样的,所以这是常量池中已经存在的内容。Str2会直接引用常量池中这个已经存在的字符串对象。所以str2的内容也是常量池中abc字符串的地址0x01。

③    String str3 = new String("abc");语句用new在堆中开辟了空间,是通过创建对象来创建字符串的。str3指向堆中一个新的空间的地址0x02,此空间中存放String对象的属性值。同样的,此属性值在赋值为"abc"时,会先在常量池中查询是否已存在此内容。此时常量池中已经存在了abc,所以属性值存储的是常量池中abc的地址0x01

④    String str4 = new String("abc");语句也是用nuw在堆中开辟了空间,利用创建新对象来创建字符串。str4指向堆中一个新的空间的地址0x03,此空间中存放String对象的属性值。同样的,此属性值在赋值为"abc"时,会先在常量池中查询是否已存在此内容。此时常量池中已经存在了abc,所以属性值存储的是常量池中abc的地址0x01。

⑤    如果通过创建新的对象来创建字符串,发现常量池中没有已存在的字符串;那么JVM会继续在常量池中开辟一个新的空间并把值存放在其中,而堆中的属性值为此空间的地址。

以上案例来源:https://www.baidu.com/link?url=xlIOX3u9Gv11cc6XlDpWjEIMaDrhaWtKinWiDEle_GNNz-OVvqntCjlr2o9FDVbj&wd=&eqid=a5c4a04300005cce00000003641ad2c6


然后我们来解释一下一下代码:

代码二:String str = "abc" + "def";//0个或一个

代码四:String str = "abc" + new String("xxx");//5个

        对于代码二中,如果你不知道他的底层实现,你估计会首先想到String str = "abc" + "def"执行过程中首先创建了两个对象“abc”和“def”然后在进行拼接最后构成第三个对象str;然而实际并不是这样,对于我们的JVM频繁的创建对象是很浪费内存和时间的,那么JVM是如何处理使用“+”进行的字符串拼接的呢?

        底层实现:用+操作符拼接字符串,会产生一个中间对象,如果是线程安全的环境,我们会用StringBuffer拼接字符串,线程不安全的环境则使用StringBuilder。

        总结:上面的问题涉及到字符串常量重载“+”的问题,当一个字符串由多个字符串常量拼接成一个字符串时,它自己也肯定是字符串常量。字符串常量的“+”号连接Java虚拟机会在程序编译期将其优化为连接后的值。就上面的示例而言,在编译时已经被合并成“abcdef”字符串,因此,只会创建1个对象(你也可以说是两个,因为还可以加上stringbuffer或stringbuilder)。并没有创建临时字符串对象abc和def,这样减轻了垃圾收集器的压力 ;

引入一个知识点:

        JVM在编译我们自己写的代码时,会将我们写的代码进行优化      

理解了上述例子,那么第四个例子也就好理解了:

        上述的代码Java虚拟机在编译的时候同样会优化,会创建一个StringBuilder来进行字符串的拼接,实际效果类似:

String s = new String("def");
new StringBuilder().append("abc").append(s).toString();

很显然,多出了一个StringBuilder对象,那就应该是5个对象。

        此时,你也或许有这样的疑问,StringBuilder最后toString()之后的“abcdef”难道不在常量池存一份吗?这个还真没有存,我们来看一下这段代码:

    String s1 = "abc";
    String s2 = new String("def");
    String s3 = s1 + s2;
    String s4 = "abcdef";
    System.out.println(s3==s4); // false

        按照上面的分析,如果s1+s2的结果在常量池中存了一份,那么s3中的value引用应该和s4中value的引用是一样的才对。但是结果却大相径庭,他并没有存到常量池中而是存到了堆内存中,上述代码会证明此观点;

为什么会这样呢?我们记以下概念:

仅有使用引号包含文本的方式创建的string对象之间使用“+”连接的新对象才会被加入到字符串常量池中,而对于包含new string()或null“+”字符串的连接方式产生的新对象不会被添加到字符串常量池中;

验证上述概念:

        /*
        String s5="qwe"+"asd";
        String s6="qweasd";
        System.out.println(s5==s6);//true
        */
        String s5="qwe"+new String("asd");
        String s6="qweasd";
        System.out.println(s5==s6);//false

继续总结:

String的两种初始化形式是有本质区别的。

String str1 = "abc";  // 在常量池中

String str2 = new String("abc"); // 在堆上

新知识引入:

使用+拼接字符串的实现原理

        前面提到过,使用+拼接字符串,其实只是Java提供的一个语法糖,看看他的内部原理到底是如何实现的。还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。

String a = "Hello";
String b = "world";
String c = a + "," + b;

反编译后的内容如下:

String a = "Hello";
String b = "world";
String c = (new StringBuilder()).append(a).append(",").append(b).toString();

        通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。也就是说 +和StringBuilder的append等价,

也就是说使用+拼接字符串的实现原理就是使用StringBuilder.append。

其实偶尔看看Java知识点的底层实现也是很有意思的,希望大家在学习中能够拥有刨根问底的学习精神; 

我很清楚自己想要什么,想成为什么,该怎么做,如何做。但我无法打败自己,所以迄今为止我还是我;

                                                                ——

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Java中的String a = new String("a")创建了两个对象,一个是存储字符串"a"的String常量池中的对象,另一个则是通过new关键字在堆内存中新创建String对象。 下面我们来深入探讨一下这个问题,需要了解以下几个概念: 1. String对象的不可变性 2. 字符串常量池 3. Java中的内存分配 1. String对象的不可变性 在Java中,String对象是不可变的,一旦被创建,它的值就不能被更改。这是因为Java中的String类是final的,不能被继承。 例如,当我们对String对象进行修改时,实际上是创建了一个新的String对象,而原来的String对象并没有被修改。 例如: ``` String str = "hello"; str = str + "world"; ``` 在这个例子中,"hello"和"world"都是String对象,但是当我们将它们拼接起来时,实际上是创建了一个新的String对象"helloworld",而原来的"hello"和"world"对象并没有被修改。 2. 字符串常量池 字符串常量池是Java中的一种特殊的内存区域,用于存储String常量。在Java中,如果我们使用双引号定义一个字符串,那么这个字符串就会被存储在字符串常量池中。例如: ``` String str1 = "hello"; String str2 = "hello"; ``` 在这个例子中,str1和str2都是指向字符串常量池中的"hello"对象,因此它们在内存中是同一个对象。 需要注意的是,字符串常量池中的对象是不可变的,一旦被创建,它的值就不能被更改。 3. Java中的内存分配 Java中的内存分为栈内存和堆内存两种。栈内存用于存储局部变量和方法调用栈,而堆内存用于存储对象。 当我们使用new关键字创建一个对象时,它会被分配到堆内存中。例如: ``` String str = new String("hello"); ``` 在这个例子中,"hello"字符串被存储在字符串常量池中,而通过new关键字创建String对象则被分配到了堆内存中。 结合以上三个概念,我们可以对题目进行深入分析: ``` String a = new String("a"); ``` 根据第3点,new关键字会在堆内存中创建一个新的String对象,因此这里至少会创建一个对象。 根据第1点和第2点,我们知道字符串常量池中已经有了一个值为"a"的String对象,如果这个对象没有被其他变量引用,那么第一次执行上述代码时,会在堆内存中创建一个新的String对象,并将常量池中的"a"字符串复制到堆内存中的新对象中。因此,此时会创建两个对象,一个在字符串常量池中,一个在堆内存中。 但是,如果常量池中已经有了一个值为"a"的String对象,并且它被其他变量引用了,那么第一次执行上述代码时,会在堆内存中创建一个新的String对象,但是这个对象中的字符串值会直接指向常量池中的"a"字符串。因此,此时只会创建一个对象。 总之,在Java中,创建String对象方式很多,而创建对象数量也会受到多种因素的影响。对于一个简单的语句,可能需要深入分析才能得出准确的答案。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值