用三种编程方法实现字符串的反转(大厂面试系列-字符串的拼接方式有几种)
字符串作为Java开发中比较常用的一种数据类型。在上篇文章中我们介绍了String类型的不可变的特性。既然是不可变的,那么我们如何对于字符串进行拼接呢?下面我们就来看一下关于字符串拼接相关的内容。
字符串拼接什么是拼接,拼接的意思就是将两个或者多个字符串合成一个大的字符串的过程,但是由于String类型在Java中是不可变的也就是说它一旦被创建初始化之后,就是不可被修改的,那么这个时候,我们如何去实现字符串的拼接效果呢?
实际上,在我们学习String类型数据的时候,我们用到了如下的一个内存模型。
也就是说如果有一个字符串A = "123",字符串B="345",将这两个字符串拼接成一个大的字符串的操作实际上是创建了一个新的字符串C="123456",只不过在我们完成,这一系列操作的时候,其实字符串“123”,“456”还存在于我们的字符串常量池中,等待垃圾回收机制进行回收,我们新的引用则是指向了新的字符串“123456”。
那么在Java中是如何实现字符串的拼接呢?
使用 进行拼接在一般情况下,我们会有如下的操作
System.out.println("Hello" joinPoint.getClass());
其实这个操作就是使用了加号对两个字符串进行了拼接,其内存模型与上面的内存模型是一样的。
使用String类型的concat()方法这个方法也是用来完成字符串拼接的。其源码如下
public String concat(String str) {
if (str.isEmpty()) {
return this;
}
int len = value.length;
int otherLen = str.length();
char buf[] = Arrays.copyOf(value, len otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
会看到,在这个操作中,使用了capy的方式进行了字符串的拼接。并且很明显地使用了new String(buf, true)操作来创建了一个新的字符串。同时也证明了字符串的不可变性。
StringBufferStringBuffer是Java中用来定义字符串变量的类,通过StringBuffer定义的字符串可以实现扩展、修改等操作。例如通过下面这个操作可以将Hello 和World 字符串组成Hello World字符串。
public class Test {
public static void main(String[] args) {
StringBuffer stringBUffer = new StringBuffer("Hello");
stringBUffer.append("World");
System.out.println(stringBUffer.toString());
}
}
当然除了上面这种方式之外,在有些框架提供的工具类中还是用join()的方式来进行字符串的添加操作,如apache.commons中提供的StringUtils类,其中的join方法可以拼接字符串。
在Java8中String类也提供了一个join方法,其功能与StringUtils类中的join方法类似,代码如下。
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();
}
在上面的内容中我们介绍了几种字符串拼接的方式,在我们学习String类型的时候一定知道,在开发中是不建议在循环中使用 操作来进行字符串的拼接的,结合上面的内存模型结构图读者可以回想一下这是为什么?
在上面的描述中我们看到在StringBuffer中,我们采用的是append方法进行字符串的拼接的。其代码如下
/**
* The value is used for character storage.
*/
char[] value;
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;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
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;
}
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
我们会看到在上面代码中使用了一个字符数组char[] value,来对字符串进行存储,但是与String类型不同的是它并不是final的,所以说对于这个数组来说它是支持修改的。在StringBuffer中还定义了一个变量count来标记大当前数组的使用位置。
在我们使用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;
}
结合上下两段代码可以知道,它的操作只是将字符串数组内部的内容进行了拷贝,当字符数组长度不够的时候会对字符数组进行扩展操作。
StringBuffer与StringBuilder最大的不同也就是append方法的不同如下
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuilder
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
上面的代码也解释了为什么StringBuffer是线程安全的,而StringBuilder是线程不安全的。
而在StringUtils的join方法中则是通过如下的方式来实现字符串的拼接操作的。
public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
if (array == null) {
return null;
}
if (separator == null) {
separator = EMPTY;
}
// endIndex - startIndex > 0: Len = NofStrings *(len(firstString) len(separator))
// (Assuming that all Strings are roughly equally long)
final int noOfItems = endIndex - startIndex;
if (noOfItems <= 0) {
return EMPTY;
}
final StringBuilder buf = new StringBuilder(noOfItems * 16);
for (int i = startIndex; i < endIndex; i ) {
if (i > startIndex) {
buf.append(separator);
}
if (array[i] != null) {
buf.append(array[i]);
}
}
return buf.toString();
}
会发现在代码中采用的StringBuilder来进行字符串的拼接操作。这里为什么不考虑线程安全问题呢?这是一个值得读者思考的问题。
总结通过上面的分析,我们可以知道,虽然字符串是不可变的,但是我们可以通过新建字符串的操作来进行字符串的拼接操作。字符串的拼接总共有五种分别是 、concat、StringBuffer、StringBuilder以及join()方法。
在使用String循环创建字符串的过程中一直在创建新的String对象,所以在循环中不建议使用String 类型 通过 的方式进行拼接。
综合上面的的分析内容,拼接字符串效率最高的应该是StringBuilder,因为StringBuilder设计之初就是用来扩展字符串操作的,只不过是没有考虑到线程安全问题。所以只要不是在并发场景下,最好使用StringBuilder来进行字符串操作。
,