String s=new String("abc")创建了几个对象?
1、引言
本文将围绕字符串常量池与字符串展开介绍。
字符串常量池和运行时常量池是两个不同的概念。运行时常量池存储的是类的字面量,是每个类独有的,而字符串常量池存储的是字符串字面量,是所有类共享的。
JDK1.7字符串常量池在方法区,JDK1.7之后字符串常量池就转移到了堆区。
2、"abc"与newString("abc")的执行原理
2.1、JVM如何执行String s="abc"
String s="abc"会先从字符串常量池(下文简称常量池)中查找,如果常量池中已经存在"abc",而"abc"必定指向堆区的某个String对象,那么直接将s指向这个String对象即可;如果常量池中不存在"abc",则在堆区new一个String对象,然后将"abc"放到常量池中,并将"abc"指向刚new好的String对象。
2.2、JVM如何执行new String("abc")
new String("abc")相当于new String(String s1="abc"),即先要执行String s1="abc"(2.1已经讲过了),然后再在堆区new一个String对象。
因此,现在可以解答本文的标题了,String s=new String("abc")创建了1或2个对象,String s="abc"创建了0或1个对象。
2.3、String类的intern方法
下面介绍一个方法,String类的intern方法,现有如下代码。
String s=new String("abc");
s.intern();
首先会检查常量池中是否存在"abc",如果存在,则s.intern()会返回"abc"指向堆区的对象,如果不存在,则将"abc"放入常量池,并将"abc"指向s指向的对象。
2.4、new String("abc")会将"abc"放到常量池里吗?
答案是会,如何验证呢?
String s1 = new String("abc");
s1.intern();
String s = "abc";
System.out.println(s==s1);// false
下面用反证法证明。
如果不会,那么是.intern()执行之后,常量池中"abc"指向的就是s1指向的对象,s也是指向常量池中"abc"指向的对象,那么s和s1指向的是同一对象,结果就是true,但结果是false,说明会放到常量池。
3、字符串拼接+操作符原理
首先来看String s="hello"+"world"这句代码。
通过javap -c class文件名称可以查看字节码,如下图所示。
可以看到String s="hello"+"world"与String s="helloworld"是一样的。
再看下面一段代码
String s1="hello";
String s2="world";
String s3=s1+s2;
通过javap命令查看字节码,如下图所示。
-
第一步:new一个StringBuilder;
-
第二步:append s1;
-
第三步:append s2;
-
第四步:调用StringBuilder的toString方法;
这下我们知道了,+操作符的原理是StringBuilder的append方法。
下面再看一道经典面试题,如下代码所示。
String s1 = "a";
String s2 = "b";
String s3 = s1+s2;
String s4 = "ab";
System.out.println(s3==s4);
这道题的疑惑点在于:s1+s2拼接之后的"ab"会不会放进常量池里,如果会,那么s3==s4返回true,如果不会,则返回false。
我们已经知道s1+s2会通过StringBuilder的append方法进行拼接,然后通过StringBuilder的toString方法返回"ab",那我们看一下toString方法的源码,如下图所示。
这下我们知道了,这是new了一个String对象,并且new好的String对象不会放进常量池里,因此s3==s4返回false。
4、为什么String是不可变的?
我们知道,用+操作符拼接字符串,会产生中间对象,如果是线程安全的环境,我们会用StringBuffer拼接字符串,线程不安全的环境则使用StringBuilder。
产生中间对象的原因是String是不可变的,我们看看String类的源码,如下图所示。
String类用声明为final的char数组来存储字符串,在拼接的时候由于char数组不可变,因此会产生中间对象;
那到底产生的是哪些中间对象呢?其实上面已经讲到了,+操作符拼接字符串时,每次会new一个StringBuilder对象,然后将+操作符连接的两个字符串append上,最后toString,而toString又会new一个String对象,因此每使用一次+操作符都会产生2个中间对象。
String类的replace方法只是替换原来的值,因此不会产生中间对象;
那StringBuilder和StringBuffer在拼接字符串时为什么效率高呢?StringBuilder和StringBuffer都继承自AbstractStringBuilder,AbstractStringBuilder的源码如下图所示。
可以看到AbstractStringBuilder中的char数组没有声明为final,因此是可变的。