对于JAVA的字符串连接操作符(+)相信大家都十分熟悉 ,它的作用是把多个字符串合并为一个字符串,当然我们使用它是非常方便的 ,但它确不适合运用在大规模的场景中 。
下面我们通过程序说明一下:
假设我们有一个需求会对字符串进行数量很大连接操作
如果我们使用String进行操作,由于 String是不可变的,每次进行用(+)连接时都相当于重新创建了一个对象
这无疑是相当耗时的,以下程序进行了10万次的字符连接操作
- public static void main(String[] args) {
- long start=System.currentTimeMillis();
- String string="aa";
- for (int i = 0; i <100000; i++) {
- string+="a";
- }
- long end=System.currentTimeMillis();
- System.out.println(end-start);
- }
第一个程序输出8425(具体因计算机处理速度不定)
由于我们每次连接两个字符串时,它们的内容都要被拷贝 ,所以程序的执行时候是呈几何增长的。
为了获得可以接受的性能,我们可以用StringBuilder代替String
- public static void main(String[] args) {
- long start=System.currentTimeMillis();
- StringBuilder sb=new StringBuilder("aa");
- for (int i = 0; i <100000; i++) {
- sb.append("a");
- }
- long end=System.currentTimeMillis();
- System.out.println(end-start);
- }
第二个程序输出15(具体因计算机处理速度不定)
由程序的执行结果不难看出 ,二者的性能差十分的大,操作的数量越多 ,差距就越明显 。
结论:当我们的程序需要性能的时候,不要使用字符串连接操作符(+)来合并多个字符串 。最好使用StringBuilder的append方法。
直接看示例1:
- public class StringTest{
- void stringReplace(String strTemp){
- strTemp=strTemp.replace('l','i');
- }
- void stringBufferAppend(StringBuffer sbTemp){
- sbTemp=sbTemp.append('c');
- }
- public static void main(String[] args){
- StringTest st=new StringTest();
- String str=new String("hello");
- StringBuffer sb=new StringBuffer("hello");
- // String str1=new String("hello");
- // StringBuffer sb1=new StringBuffer("hello");
- // System.out.println("str.equals(sb)= "+str.equals(sb));
- // System.out.println("str.equals(str1 )= "+str.equals(str1));
- // System.out.println("sb.equals(str)= "+sb.equals(str));
- // System.out.println("sb.equals(sb1)= "+sb.equals(sb1));
- st.stringReplace(str);
- st.stringBufferAppend(sb);
- System.out.println("str= "+str+"\tsb= "+sb);
- }
- }
输出结果:
- str= hello sb= helloc
上面的示例无非就是方法传值与传址的问题,因为形参为引用类型,因此实参传递过来的是对象的地址值,问题来了:改变该地址所对应的内容,实参应该也会跟着发生变化,如sb最终结果那样,但str却没有,为何?
这就是String与StringBuilder的区别:
字符串的可变与不可变
JDK_1.8中这样解释:
- Strings are constant; their values cannot be changed after they are created.
- String buffers support mutable strings. Because String objects are immutable
- they can be shared.
String创建的字符串是不可变的,而StringBuilder(或StringBuffer)通过字符缓冲区创建字符串,可变;
先来解析下String的不可变:
- String s="hello";
- s="world";
对象s在创建的时候先查看常量池中是否有"hello",有则指向它,没有就创建一个"hello",再指向它,但当重新对s赋值为"world"时,常量池中的"hello"并没有改变,java会重新开辟一个内存存储新值"world",并令s指向"world";
因此在示例1中,当调用stringReplace()方法时,实参str所存储的地址值传给了形参strTemp后,strTemp指向了常量池中"hello",当对其进行字符替换时,java会重新创建"heiio",并让形参strTemp指向它,因此形参和实参指向了不同的区域,结果自然显而易见;
另外,String的不可变是由于:
- public final class String
- implements java.io.Serializable, Comparable<String>, CharSequence {
- private final char value[];//final的作用,使之初始化后不可改变
- ……
- }
(P.S.虽然value是用final来修饰的,但仍有办法可以直接改变其值,具体可以参见 反射 )
再来对比下StringBuilder的可变:
- StringBuilder sb=new StringBuilder("hello");
- sb.append("world");
在创建StringBuilder对象时,java实际上在堆中创建了字符类型数组char[],存储完"hello"后通过append方法在增加"world",并未开辟新的内存区,形参和实参指向同一区域,因此形参对对象内容进行变化,实参也会跟着变化;
来看下StringBuilder构造器的底层代码:
- public StringBuilder() {
- super(16);
- }
- public StringBuilder(int capacity) {
- super(capacity);
- }
- public StringBuilder(String str) {
- super(str.length() + 16);
- append(str);
- }
- public StringBuilder(CharSequence seq) {
- this(seq.length() + 16);
- append(seq);
- }
其基类AbstractStringBuilder的构造方法:
- char[] value;
- AbstractStringBuilder(int capacity) {
- value = new char[capacity];
- }
……
可以发现StringBuilder实际创建了一个默认16字符长的char型数组(亦可指定长度):char[] value;
若为对象赋值时,所存储的字符串长度未超过所定义的char数组长度,则按顺序存储相应字符,若超过所定义的数组长度,则自动扩充其长度,因此说StringBuffer是可变的;
我们还是继续看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;
- }
- private void ensureCapacityInternal(int minimumCapacity) {
- if (minimumCapacity - value.length > 0)
- expandCapacity(minimumCapacity);
- }
- void expandCapacity(int minimumCapacity) {
- int newCapacity = value.length * 2 + 2;
- if (newCapacity - minimumCapacity < 0)
- newCapacity = minimumCapacity;
- if (newCapacity < 0) {
- if (minimumCapacity < 0) // overflow
- throw new OutOfMemoryError();
- newCapacity = Integer.MAX_VALUE;
- }
- value = Arrays.copyOf(value, newCapacity);
- }
当为StringBuilder对象增加内容时,会先计算所需最小空间:minimumCapacity=count+len;并与原数组长度的2倍+2即newCapacity进行比较,取较大值并为数组value重新赋值,赋值采用的是数组复制的方式进行;
常见的数组复制有两种方法:一为:Arrays.copyOf(),另一为:System.arraycopy(),差别在于后者可能存在下标越界的问题,其实前者也是通过调用后者的方法来实现的:
- public static char[] copyOf(char[] original, int newLength) {
- char[] copy = new char[newLength];
- System.arraycopy(original, 0, copy, 0,
- Math.min(original.length, newLength));
- return copy;
- }
由于重新为临时数组copy定义了长度newLength,因此复制的时候不会出现越界问题;在数组复制结束后,将临时数组copy赋值给value,使之指向新的区域,完成append()操作 (这里注意:引用变量value的指向发生了变化,但StirngBuilder的指向没有变,只不过其成员变量(恰巧是个引用变量value)的值发生了改变,因此形参sbTemp和sb仍指向同一区域);
equals方法
示例1中注释掉的几个equals()语句,结果只有String与String比较时才返回true,这是String与StringBuilder的另一个区别;
我们要直到equals()方法的返回结果取决于你如何重写的,如直接继承Object的方法,则比较的是变量的值,对于StringBuilder,它并未重写该方法,因此对两个引用变量进行比较自然返回false;而String类重写了该方法,具体如下:
- public boolean equals(Object anObject) {
- if (this == anObject) {
- return true;
- }
- if (anObject instanceof String) {
- String anotherString = (String)anObject;
- int n = value.length;
- if (n == anotherString.value.length) {
- char v1[] = value;
- char v2[] = anotherString.value;
- int i = 0;
- while (n-- != 0) {
- if (v1[i] != v2[i])
- return false;
- i++;
- }
- return true;
- }
- }
- return false;
- }
可知,若两个引用变量指向同一区域,则自然返回true,若非,则判断anObject是否是String的实例,若是则进行值比较,否则直接返回false;
效率的差别
- public class StringBuilderTest2{
- public static void main(String[] args){
- StringBuilder sb=new StringBuilder();
- StringBuilder sb1=new StringBuilder(Integer.MAX_VALUE/7);
- String s=null;
- System.out.println(Integer.MAX_VALUE+"\n"+sb1.capacity());
- long start=System.currentTimeMillis(),end;
- for(int i=0;i<1000000;i++){
- sb.append(1);
- }
- end=System.currentTimeMillis();
- System.out.println("默认初始化SB时循环100w次耗时:"+(end-start));
- for(int i=0;i<1000000;i++){
- sb1.append(1);
- }
- start=System.currentTimeMillis();
- System.out.println("给定值初始化SB1时循环100w次耗时:"+(start-end));
- for(int i=0;i<100000;i++){
- s+=1;
- }
- end=System.currentTimeMillis();
- System.out.println("String“+”运算10W次耗时:"+(end-start));
- }
- }
输出:
- 2147483647
- 306783378//初始化值
- 默认初始化SB时循环100w次耗时:35
- 给定值初始化SB1时循环100w次耗时:16
- String“+”运算10W次耗时:3211
可以发现在循环“+”/append()运算时,String类的效率明显低于StringBuilder,为何?
这里继续使用javap来分析:
- String s=null;
- s+=1;
- 0: aconst_null
- 1: astore_1
- 2: new #2 // class java/lang/StringBuilder
- 5: dup
- 6: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
- 9: aload_1
- 10: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
- 13: iconst_1
- 14: invokevirtual #5 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
- 17: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
- 20: astore_1
- 21: return
可以看到编译器在解析String"+"运算时,会转换成StringBuilder,调用append()方法,再通过toString()赋值,因此循环运算时,就不断发生创建对象和产生垃圾,这过程消耗了资源,造成了效率的下降;
另外,我们也可以发现调用StringBuilder构造方法时若给它传入较大整数作为参数,则运算效率也会有明显的提升,这跟前面所提扩容时数组复制有关,给定较大初始缓存区,自然不需要频繁扩容;
当然,这里顺带提一下,如果直接把String的"+"运算写成形如:s=""+1+1+1……,而不是通过循环来不断调用变量,则编译器会直接求出其字面量,运行时不会再转换为StringBuilder,效率当然就比后者高了;
相关推荐
本文将深入探讨字符串、String类以及StringBuilder类,帮助你更好地理解和应用这些基础知识。 首先,我们关注的是String类。在C#中,String类是不可变的,这意味着一旦一个字符串对象被创建,它的内容就不能被改变...
- **StringBuilder**:由于 `StringBuilder` 允许在原有对象上进行修改,因此在进行字符串拼接等操作时,其性能通常优于 `String`。特别是当字符串拼接操作非常频繁时,这种性能优势尤为明显。 #### 3. 使用场景 -...
在Java编程语言中,String、StringBuilder和StringBuffer都是用来处理字符串的类,它们之间存在一些重要的区别,主要涉及到性能和线程安全性。 首先,`String`类代表的是字符串常量,一旦创建,其内容就不能改变。...
"String StringBuffer和StringBuilder区别之源码解析" 在Java中,字符串是我们经常使用的数据类型,而String、StringBuffer和StringBuilder是Java中三种常用的字符串类。在这篇文章中,我们将从源码角度对String、...
在编程领域,尤其是在使用C++、Java或C#等面向对象的语言时,经常需要将字符串数组转换为单一的string类型。这种操作在处理数据输入、输出或者格式化时非常常见。下面我们将详细讨论如何在不同语言中实现这个过程,...
本测试着重探讨了三种常用的字符串连接方法:`+`运算符、`String.Format()`以及`StringBuilder.Append()`,并分析了它们在性能上的差异。 1. **字符串连接:+ 运算符** 在C#中,`+`运算符可以用于连接两个或多个...
主要生成StringBuilder 字符串 类似 StringBuilder builder = new StringBuilder(); builder.AppendFormat("<span class=\"navSep\"></span>\r\n"); builder.AppendFormat("机构看盘</a>\r\n"); builder....
String、StringBuffer 和 StringBuilder 是 Java 语言中三种不同类型的字符串处理方式,它们之间存在着明显的性能和线程安全性差异。 String String 类型是不可变的对象,每次对 String 对象进行改变时都会生成一...
C# 拼接字符串的几种方式和性能 C# 拼接字符串的方式有多种,每种方式都有其优缺,今天我们将讨论三种常用的方式:简单“+=”拼接法、String.Format()和StringBuilder.Append()。 1. 简单“+=”拼接法 简单“+=”...
在C#编程语言中,处理字符串是常见的任务之一,其中包括删除字符串中的特定部分或子字符串。本篇文章将详细探讨如何在C#中实现这一功能,包括多种方法和实用技巧。 首先,C#提供了多种内置方法来操作字符串,比如`...
在处理大量字符串连接时,相比直接使用`+`运算符或`String`对象,`StringBuilder`能提供更高的性能。这是因为每次使用`+`进行字符串连接时,Java都会创建新的`String`对象,这在内存管理和效率上都是不理想的。而`...
C# StringBuilder 拼接字符串 字符串转换工具 StringBuilder比StringBuffer运行速度要快,因为StringBuilder是针对于单线程的,所这它是非线程安全的。普通情况下建议使用StringBuilder。
在Java编程语言中,`String`和`StringBuilder`都是用于处理字符串的重要类,但它们在处理方式和效率上有显著的区别。本资源中的代码是针对这两种类的效率进行测试的实例,旨在帮助开发者理解它们在不同场景下的性能...
C#中有多种连接字符串的方法,例如+、$、StringBuilder、Concat等,这里对前三种方法的性能进行了粗略比较,结果表明在字符串较小、使用频率很低的场合,使用“+”或"$"都行,如果连接的数据类型都是字符串,则推荐...
在Java编程语言中,`String`、`StringBuffer`和`StringBuilder`是处理字符串的三个重要类,它们各自有特定的使用场景和优缺点。理解它们的差异对于编写高效的代码至关重要。 **String类** `String`是不可变的类,...
在这个例子中,`join()`方法使用了`StringBuilder`来避免创建大量的中间字符串对象,从而提高了性能。`join(Iterable, String)`方法适用于任何实现了`Iterable`接口的集合,包括List、Set等。`join(Map, ?>, String)...
在内部,JavaScript会创建新的字符串对象来存储连接结果,因此这种方式对于少量字符串连接效率尚可,但随着字符串数量增加,性能会下降,因为每次连接都会产生新的字符串对象。 2. 使用`Array.prototype.join()`:...
StringBuilder 方法可以将多个字符串连接起来生成最终的字符串,具有高效的性能和良好的可扩展性。 2. 使用内置方法 内置方法可以将多个字符串连接起来生成最终的字符串,具有高效的性能和良好的可扩展性。 3. ...
在 Java 中,String, StringBuffer 和 StringBuilder 三个类都是用于字符操作的,但它们之间有着很大的区别。 首先,String 是不可变类,意味着一旦创建了 String 对象,就不能修改它的值。每次对 String 对象的...