我最近看了挺多关于String相加的博文,总觉得都在告诉大家一些结果,或者公认的表面东西,没有人讲为什么这样。所以这篇博文旨在由浅入深的讲讲String类型的相加的一些知识。
对String类型做一个基本的介绍:
String 是典型的Immutable类,被声明成为final class,所有的属性也是final的。由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的String对象。
String原生的保证了基础线程安全,由于可不变,对象在拷贝时不需要额外复制数据。
这些大家在平常的使用过程中都有自己清楚的认识。
其实字符串拼接无非总体来说就是俩种方式:
字符串相加,其实就是字符串的拼接,在字符串拼接时,编译器会先检查拼接字符串是否都是常量,如果是常量,会直接应用常量,编译器并创建一个新的对象来存放对于拼接的字符串的引用。
如果不是常量,其实在编译器中执行的就是StringBuilder.append()方法对字符串进行拼接,然后将拼接好的StringBuilder进行.toString(),得到字符串后返回。
先来说说创建字符串在堆栈中创建的对象是多少个,我先举几个例子:
String s1 = "a";
String s2 = "b";
String s3 = new String("123");
String s4 = "1" + "2";
String s5 = new String("123") + new String("321");
String s6 = s1 + s2;
在上面的代码中一共有六行代码,其中行、行2在内存中分别创建了一个对象,这个容易理解,也是基础。接下来看行3,行3创建了几个对象呢?看着代码表面来说,应该是一个对象,但是真的是这样吗?
先给大家贴一下new String()的构造代码(运行环境是jdk1.8),如下:
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
很简单,就是一个有参构造器,将传入的值构造一个新的String对象,并且根据传入的值做hash
,好这段源代码告诉我们new String是只创建一个对象的。但是,我们看看这段代码反编译后的结果:
这是行3进行编译,然后反编译的结果,从结果中我们可以看到操作顺序,和创建的对象,一个是 new 出来的对象,也就是new String(),另一个是 String 123,从这里不难看出,在jdk的运行操作中,底层是出现了俩个String对象的,没有额外的对象生成出来(当然不能算String底层的 char[] 数组)。那么也就可以看出来,在编译器运行行3的代码时,实际上是创建了俩个对象的,一个是s3,一个是“123”。
好,我们接着看行4,行4是一个字符串的拼接,如果按照目前为止的理解方式,那么应该在内存中创建的是3个对象,“1,”,“2”和“12”,但是并不是这样子的。这里涉及到一个重要的知识点(个人的理解总结):
如果在创建String的时候引用的是字符串常量(jdk1.8之前字符串常量是单独的一块内存空间,java 9之后,字符串常量池被移动到了堆中),那么只会创建当前对象
该怎么理解这句话呢,首先“1”,和“2”在java中,类似于这样的,我们统一称为字符串常量,在创建新的字符串时,编译器会先去访问常量池,因为“1”,“2”在字符串常量池中存在,所以,在创建s4时,会直接引用字符串常量池中的“1”和“2”,并将合并的结果创建一个新的对象s4,如下图反编译后的结果:
可以看到反编译后只是创建了一个String对象 “12”。
那么有部分“彭于晏”(同学)会问,那么对于行3你怎么解释?这里要说一下,行3 执行的是new String操作,在java8中,注意一下这个操作的顺序,还有反编译后的结果显示。
接下来要说的是行6,(不是行5哦),行6是两个字符串的拼接,这是咱们常用的拼接方式(以后在实际开发中尽量直接使用StringBuilder去拼接字符串),那么这是创建了几个对象呢?首先s1对象已经创建了,s2对象也已经创建了,当已经创建好字符串对象时,那么再做字符串拼接时,编译器会直接饮用创建好的对象地址,也就是s1和s2的对象地址,将s1和s2对象都拿出来时,此时还没有创建新的对象,接下来是字符串拼接,将俩个对象合成一个新的对象,那么这个新的对象就是创建出来的对象。那这步操作只是引用后生成了一个对象吗?这样描述是不准确的。我们先来看看反编译之后的结果:
从图中不难看出,当编译器监测到俩个String对象要进行拼接时,首先会创建一个StringBuilder,然后会逐步按照顺序将俩个对象append到StringBuilder中,最后再执行toString方法。
那么这其中的整个流程到底创建了多少个对象呢?我按照执行顺序将源代码放出来。
首先StringBuilder的类的继承和实现
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
......//此处省略一完字
}
从反编译的结果中看出是逐步append(),而且参数是String,所以调用的数StringBuilder的append(String str)方法,源代码如下:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
调用了父类的append方法,代码如下:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
按照执行顺序,先查看appendNull()方法,源代码如下:
private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}
内部方法 ensureCapacityInternal(c + 4),源代码如下:
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
内部方法newCapacity(minimumCapacity),源代码如下:
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
因为父类的方法又调用了一遍ensureCapacityInternal(count + len)方法,所以从源代码中看到,至此appen方法是没有新增对象的(Arrays.copyOf()方法是不会复制对象的,复制的是引用地址)。再接着看反编译后的结果,两个append()方法都没有新增对象,那么最后一步toString()方法会吗?我们看源代码,如下:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
从代码里看到是有个String的构造器的,那么这里会创建一个String的对象,其实从代码注释Create a copy, don't share the array
也能看出来。
执行完toString()方法后,程序也就执行完这一部分的代码了,所以这整个过程一共创建了两个对象,一个是StringBuilder,一个是String,所以行6 一共创建的是俩个对象,一个String对象。
接下来要说的就是行5,行5 略有些复杂,首先看表面,按照之前的分析,应该是6个对象“123”、”321“、俩个new String()动作、s5,还有一个StringBuilder对象,这样才能验证上面所说的。我将反编译后的结果放上来:
按照步骤来看,首先创建一个StringBuilder对象,然后创建“123”String对象,再创建new String(),执行append(),接着创建“321”String对象,再创建new String(),执行append(),最后执行toString方法。结合上面的经验,最后得出创建了6个对象,分别是StringBuilder对象,两个new String()对象,”123“,”321“,toString()方法创建的String对象。
还有几个冷知识,有兴趣的可以了解一下:
字符串的hashcode值取决于内容,而不是地址;
string.intern()方法,会将一个字符串的引用放到常量池里。
例如:
String s3 = new String("12") + new String("34");
s3.intern();
String s4 = "1234";
System.out.println(s3 == s4);
如果有行2代码,结果是true,如果不加行2代码结果就是false。
综上所述,对于String我们有了一个大概的认识,String在创建时一共创建了 几个对象,几个String对象,我们大概心理有了清晰的认识,关于字符串的拼接,其实就是StringBuilder的append()
,所以在日后的工作中也好,学习中也好,如果能使用的StringBuilder尽量使用StringBuilder,String的字符串直接拼接会造成多余内存的消耗(一般来说使用+号也没什么大碍)。