【String、StringBuilder 和 StringBuffer 的 区别】

发布时间:2023年12月22日

在这里插入图片描述

?典型解析


String 是不可变的,StringBuilder 和 StringBuffer 是可变的。


而StringBuffer 是线程安全的,而StringBuilder 是非线程安全的。


?扩展知识仓


?String 的不可变性


我们都知道 String 是不可变的,但是它是怎么实现的呢?


先来看一段 String 的源码(JDK 1.8):


public final class String implements java.io.Serializable, Comparable<String>,CharSequence {

	/** The value is used for character storage. */

	private final char value[];
	
	/** use serialVersionUID from JDK 1.0.2 for interoperability */

	private static final long serialVersionUID = -6849794470754667710L;

	public String substring(int beginIndex) {
		if (beginIndex < 0) {
			throw new StringIndexOutOfBoundsException(beginIndex);
			
		}
		int subLen = value.length - beginIndex;
		if (subLen < 0) {
			throw new StringIndexOutOfBoundsException(subLen);
		}
		return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
	}
	
	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 不可变的主要实现了。




1. String类被声明为final,这意味着它不能被继承。那么他里面的方法就是没办法被覆盖的。


2. 用final修饰字符串内容的char[ ] (从JDK 1.9开始,char [ ] 变成了 byte[ ] ),由于该数组被声明为final,一旦数组被初始化,就不能再指向其他数组。


3. String类没有提供用于修改字符串内容的公共方法。例如,没有提供用于追加、删除或修改字符的方法。如果需要对字符串进行修改,会创建一个新的String对象。


再然后,在他的一些方法中,如substring、concat等,在代码中如果有涉及到字符串的修改,也是通过newString()的方式新建了一个字符串。


所以,通过以上方式,使得一个字符串的内容,一旦被创建出来,就是不可以修改的了。


不可变对象是在完全创建后其内部状态保持不变的对象。这意味着,一旦对象被赋值给变量,我们既不能更新引用,也不能通过任何方式改变内部状态。


可是有人会有疑惑,String为什么不可变,我的代码中经常改变String的值啊,如下:


String s = "abcd";
s = s.concat("ef");

这样,操作,不就将原本的"abcd"的字符串改变成"abcdef"了么?


但是,虽然字符串内容看上去从"abcd"变成了“abcdef",但是实际上,我们得到的已经是一个新的字符串了。


在这里插入图片描述

如上图,在堆中重新创建了一个“ abcdef ”字符串,和 “ abcd ” 并不是同一个对象。


So , 一旦一个 String 对象在内存(堆)中被创建出来,他就无法被修改。而且,String 类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。


如果我们想要一个可修改的字符串,可以选择 StringBuffer 或者 StringBuilder 这两个代替 String。


? 为什么JDK 9 中把String 的char[ ] 改成了 byte[ ] ?


在Java 9之前,字符串内部是由字符数组char[ ] 来表示的。


/** The walue is used for character storage. */
private final char value[];

由于Java内部使用UTF-16,每个char占据两个字节,即使某些字符可以用一个字节(LATIN-1)表示,但是也仍然会占用两个字节。所以,JDK 9就对他做了优化。


这就是Java 9引入了"Compact String"的概念:


每当我们创建一个字符串时,如果它的所有字符都可以用单个字节(Latin-1)表示,那么将会在内部使用字节数组来保存一半所需的空间,但是如果有一个字符需要超过8位来表示,Java将继续使用UTF-16与字符数组。


Latin1(又称ISO 8859-1)是一种字符编码格式,用于表示西欧语言,包括英语、法语、德语、西班牙语、葡萄牙语、意大利语等。它由国际标准化组织(ISO)定义,并涵盖了包括ASCII在内的128个字符。


Latin1编码使用单字节编码方案,也就是说每个字符只占用一个字节,其中第一位固定为0,后面的七位可以表示128个字符。这样,Latin1编码可以很方便地与ASCII兼容。


那么,问题来了,所有字符串操作时,它如何区分到底用Latin-1还是UTF-16表示呢?


为了解决这个问题,对String的内部实现进行了另一个更改。引入了一个名为coder的字段,用于保存这些信息。


/**
 *  The value is used for character storage.
 *
 * @implNote This field is trusted by the VM, and is a subject to
 * constant folding if String instance is constant. Overwriting this
 * field after construction will cause problems.
 * 
 *  Additionally, it is marked with (@link Stable] to trust the contents
 *  of the array. No other facility in JDK provides this functionality (yet).
 *  (@link Stablel is safe here, because value is never null.
 */

@Stable
private final byte[] value;


/**
 * The identifier of the encoding used to encode the bytes in
 * (@code value]. The supported values in this implementation are
 * 
 * LATIN1
 * UTF16
 * 
 *  @implNote This field is trusted by the VM, and is a subject to
 * constant folding if String instance is constant. Overwriting this
 *  field after construction will cause problems.
 */
 private final byte coder;

coder字段的取值可以是以下两种:


static final byte LATIN1 = 0;
static final byte UTF16 = 1;

在很多字符串的相关操作中都需要做一下判断,如:


public int indexOf(int ch, int fromIndex) {
	return islatin1()
	    ? StringLatin1.indexOf(value, ch, fromIndex)
	    :StringUTF16.index0f(value, ch, fromIndex);
}

private boolean isLatin1()  {
	return COMPACT_STRINGS && coder == LATIN1;
}

?为什么String设计成不可变的


为什么要把String设计成不可变的呢? 有什么好处呢?


这个问题,困扰过很多人,甚至有人直接问过Java的创始人James Gosling。

在一次采访中James Gosling被问到什么时候应该使用不可变变量,他给出的回答是:

I would use an immutable whenever I can.

那么,他给出这个答案背后的原因是什么呢? 是基于哪些思考的呢?

其实,主要是从缓存、安全性、线程安全和性能等角度出发的。

?缓存


字符串是使用最广泛的数据结构。大量的字符串的创建是非常耗费资源的,所以,Java提供了对字符串的缓存功能,可以大大的节省堆空间。


JVM中专门开辟了一部分空间来存储Java字符串,那就是字符串池。


通过字符串池,两个内容相同的字符串变量,可以从池中指向同一个字符串对象,从而节省了关键的内存资源。

String s ="abcd";
String s2 = s;

对于这个例子,s和s2都表示”abcd”,所以他们会指向字符串池中的同一个字符串对象:


在这里插入图片描述

但是,之所以可以这么做,主要是因为字符串的不变性。试想一下,如果字符串是可变的,我们一旦修改了s的内容,那必然导致s2的内容也被动的改变了,这显然不是我们想看到的。


?安全性


字符串在Java应用程序中广泛用于存储敏感信息,如用户名、密码、连接url、网络连接等。JVM类加载器在加载类的时也广泛地使用它。


因此,保护String类对于提升整个应用程序的安全性至关重要。


当我们在程序中传递一个字符串的时候,如果这个字符串的内容是不可变的,那么我们就可以相信这个字符串中的内容。


但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。


?线程安全


不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。


因此,一般来说,不可变对象可以在同时运行的多个线程之间共享。它们也是线程安全的,因为如果线程更改了值,那么将在字符串池中创建一个新的字符串,而不是修改相同的值。因此,字符串对于多线程来说是安全的。


?hashcode缓存


由于字符串对象被广泛地用作数据结构,它们也被广泛地用于哈希实现,如HashMap、HashTable、HashSet等。在对这些散列实现进行提作时,经常调用hashCode() 方法。


不可变性保证了字符串的值不会改变。因此,hashCode0方法在String类中被重写,以方便缓存,这样在第一次hashCode调用期间计算和缓存散列,并从那时起返回相同的值。


在String类中,有以下代码:

private int hash;//this is used to cache hash code.

? 性能


前面提到了的字符串池、hashcode缓存等,都是提升性能的体现。

因为字符串不可变,所以可以用字符串池缓存,可以大大节省堆内存。而且还可以提前对hashcode进行缓存,更加高效。

由于字符串是应用最广泛的数据结构,提高字符串的性能对提高整个应用程序的总体性能有相当大的影响。


?String 的 " + " 是如何实现的

使用 + 拼接字符串,其实只是Java提供的一个语法糖,那么,我们就来解一解这个语法糖,看看他的内部原理到底是如何实现的。

还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。

String wechat = "Hollis";
String introduce = "Chuang";
String hollis = wechat + "," + introduce;

我们把这些个代码反编译以下,会得到如下代码:

String wechat ="Hollis";
String introduce = "Chuang";
String hollis = (new Stringbuilder()).append(wechat).append(",").append(introduce).tostring();

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

那么也就是说,Java中的 + 对字符串的拼接,其实现原理是使用StringBuilderappend。


?StringBuffer 和 StringBuilder


接下来我们看看 StringBuffer 和 StringBuilder的实现原理。


和String 类相似,StringBuilder 类也封装了一个字符数组,定义如下:

char [] value;

与String不同的是,它并不是final的,所依它也是可以修改的,另外,与String 不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:

int count;

其append源码如下:

public StringBuilder append(String str);

	super.append(str);
	return this; 
	                                                           

该类继承了 ‘AbstractStringBuilder ’ 类,看其下 ‘ append ’ 方法:


 public AbstractStringBuilder append(String str) {
 	if (str == nul1)
 		return appendNul1();
 	int len = str.length();
 	ensureCapacityInternal(count + len);
 	str.getChars(0,en, value, count);
 	count += len;
	return this;
 }

append会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。


StringBuffer 和 StringBuilder 类似,,最大的区别就是 StringBuffer 是线程安的,看一下 StringBuffer 的 ‘append’ 方法。


public synchronized StringBuffer append(String str) {
	toStringCache = null;
	super.append(str);
	return this;
}

该方法使用 synchronized 进行声明,说明是一个线程安全的方法。而 StringBuilder 则不是线程安全的.。


?不要在for循环中使用+拼接字符串


前面我们分析过,其实使用+拼接字符串的实现原理也是使用的StringBuilder,那为什么不建议大家在for循环中使用呢?


//我们把以下代码反编译下:

long t1 = System.currentTimeMillis();
String str = "hollis";

for (int i = 0; i < 50000; i++) {
	String s = String.valueOf(i);
	str += s;
}

long t2 = System.currentTimeMillis();
System.out.println("+ cost:" + (t2 - t1));

反编译后代码如下:


long t1 = System.currentTimeMillis();
String str = "hollis";
for(int i = 0; i < 50000; i++) {
	String s = String.value0f(i);
	str = (new StringBuilder()).append(str).append(s).toString();
long t2 = System.currentTimeMillis();
System.out.println((new StringBuilder()).append("+ cost:").append(t2 - t1).toString());
}

我们可以看到,反编译后的代码,在 for 循环中,每次都是 new 了一 StringBuilder ,然后再把 String 转成 StringBuilder ,再进行 append。


而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费


所以,阿里巴巴Java开发手册建议: 循环体内,字符审的连接方式,使用 StringBuilder 的 append 方法进行扩展。而不要使用 + 。

文章来源:https://blog.csdn.net/Java_Yangxiaoyuan/article/details/135150071
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。