在谈字符串拼接之前,我们首先了解一下字符串常量池
Java7之前,常量池是存放在方法区中的。
Java7,常量池存放到了堆中。
Java8之后,运行时常量池和静态常量池存放在元空间中,而字符串常量池存放在堆中。
String s = "hello"; //直接通过双引号""声明字符串
String s = new String("hello"); //使用new关键字创建字符串
小结
当用new关键字创建字符串对象时, 不会查询字符串常量池; 当用双引号直接声明字符串对象时, 虚拟机将会查询字符串常量池.
字符串常量池提供了字符串的复用功能, 除非我们要显式创建新的字符串对象, 否则对同一个字符串虚拟机只会维护一份拷贝。
String s1 = new String("123");
String s2 = new String("123");
String s3 = "456";
String s4 = "456";
System.out.println(s1==s2); //false
System.out.println(s3==s4); //true
可以用上面的代码去验证我们的结论,==对于对象来说比较的是地址,上面的代码也说明了如果我们不显示创建对象,它将会直接在堆内存中创建一个字符串对象, 并返回给所属变量。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
翻阅源码我们可以看出,对于String类, 凡是涉及到返回参数类型为String类型的方法, 在返回的时候都会通过new关键字创建一个新的字符串对象
如果我们通过 反编译代码 可以看出当用String类拼接字符串时, 每次都会生成一个StringBuilder对象, 然后调用两次append()方法把字符串拼接好, 最后通过StringBuilder的toString()方法new出一个新的字符串对象。
很显然如果拼接的次数过多, 创建对象所带来的时延会降低系统效率, 同时会造成巨大的内存浪费,使JVM不得不进行GC,进一步降低了系统效率。
StringBuilder和StringBuffer都继承自AbstractStringBuilder类, 通过查看该类的源码, 得知StringBuilder和StringBuffer两个类也是通过char类型数组实现的(AbstractStringBuilder类中)
@Override
public StringBuilder append(char[] str) {
super.append(str);
return this;
}
我们通过阅读源码也可以看出, StringBuilder类, 大多数方法都会返回StringBuilder对象自身 。也就是说在拼接字符串的时候StringBuilder只需要调用一次append()方法,显然StringBuilder比String效率高效很多
StringBuffer在方法上添加了 synchronized关键字, 证明它的方法绝大多数方法都是线程同步方法 (insert方法没有),也就是说在多线程的环境下使用StringBuffer保证线程安全, 在单线程环境下使用StringBuilder获得更高的效率。
@Override
public synchronized StringBuffer append(CharSequence s) {
toStringCache = null;
super.append(s);
return this;
}
大家有兴趣也可以去阅读一下AbstractStringBuilder的源码。
public AbstractStringBuilder append(char[] str) {
int len = str.length;
ensureCapacityInternal(count + len);
System.arraycopy(str, 0, value, count, len);
count += len;
return this;
}
这个类是我在阅读String类的时候发现的,附上String的部分源码
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
可以看出这个类和StringBuilder有异曲同工之妙, StringJoiner是Java8新出的一个类,用于构造由分隔符分隔的字符序列,也就是运用了StringBuilder的一个拼接字符串的封装处理。
public StringJoiner(CharSequence delimiter) {
this(delimiter, "", "");
}
public StringJoiner(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix) {
Objects.requireNonNull(prefix, "The prefix must not be null");
Objects.requireNonNull(delimiter, "The delimiter must not be null");
Objects.requireNonNull(suffix, "The suffix must not be null");
// make defensive copies of arguments
this.prefix = prefix.toString();
this.delimiter = delimiter.toString();
this.suffix = suffix.toString();
this.emptyValue = this.prefix + this.suffix;
}
这从它的构造函数也很容易得出,下面来举个简单的例子
StringJoiner sj = new StringJoiner(":", "[", "]");
sj.add("1").add("2").add("3");
System.out.println(sj.toString());
//输出:[1:2:3]
我们再继续阅读源码,会发现它和StringBuilder的联系,部分源码如下
public StringJoiner add(CharSequence newElement) {
prepareBuilder().append(newElement);
return this;
}
private StringBuilder prepareBuilder() {
if (value != null) {
value.append(delimiter);
} else {
value = new StringBuilder().append(prefix);
}
return value;
}
我们发现StringJoiner底层依旧使用的 StringBuilder,第一次添加数据时,会生成StringBuilder对象,并添加 “前缀”,后续添加字符时,追加 “分隔符”,最后调用 append 方法。
本文参考如下:
StringJoiner
从底层彻底搞懂String,StringBuilder,StringBuffer的实现