java 读取字符串效率_无聊的笔记:之一(Java字符串链接方式效率对比和解析)...

简介

在JAVA开发过程中,经常会遇到字符串操作,对字符串的拼接操作更常见。

拼接字符串主要有以下几种方法:

1. "" + ""

2. "".concat("")

3. new StringBuilder().append()

4. new StringBuffer().append()

还有一个StringUtils工具类的join()方法,这里不做讨论。

时间紧迫的朋友可以跳过分析,直接看结论。

分析

1. 直接使用+拼接字符串

这是最方便的。但是它的性能在大部分情况下也是最低的一个。为什么这么说呢?请看官继续向下:

废话少说,先上个例子:

public class Test1 {

public static void main(String[] args) {

String s = "abc";

String s1 = "123" + s;

String s2 = "efg" + s1;

}

}

我们使用javap工具对字节码文件 Test1.class 反编译一下,瞅瞅+操作符干了些啥。

javap是java字节码反编译工具。-c 参数表示显示反汇编命令。

C:\com\demo> javap -c .\Test1.class

Compiled from "Test1.java"

public class com.demo.Test1 {

public com.demo.Test1();

Code:

0: aload_0

1: invokespecial #1 // Method java/lang/Object."":()V

4: return

public static void main(java.lang.String[]);

Code:

0: ldc #2 // String abc

2: astore_1

3: new #3 // class java/lang/StringBuilder

6: dup

7: invokespecial #4 // Method java/lang/StringBuilder."":()V

10: ldc #5 // String 123

12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

15: aload_1

16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

22: astore_2

23: new #3 // class java/lang/StringBuilder

26: dup

27: invokespecial #4 // Method java/lang/StringBuilder."":()V

30: ldc #8 // String efg

32: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

35: aload_2

36: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

39: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

42: astore_3

43: return

}

从反编译的代码中可以看出,编译器会把+符号优化成StringBuilder类,并使用 StringBuilder.append() 方法对字符拼接。最后调用 StringBuilder.toString() 方法,返回String。

在上面的代码中,我们对字符串进行了两次+操作,在优化后的代码也创建两次StringBuilder对象和调用两次toString()方法。

既然编译器会把+操作优化成StringBuilder方法,那它们效率会一样?其实不然。

2. 为什么+操作符比StringBuilder效率低?

首先看看StringBuilder怎么拼接字符串。

StringBuffer sb = new StringBuffer();

sb.append("abc");

sb.append("123");

sb.append("efg");

sb.toString();

上面的代码,只 new 了一个 StringBuilder 对象,而且只调用了一次 toString()方法。

我们知道在java中实例化对象,其实是很费时的,还要回收什么的。要不就不会搞出 单例模式 这种东西了。

而toString()这个方法更是浪费时间,我们来看下 StringBuilder 的 toString() 方法的源码。

public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{

//省略 n 多代码

private transient char[] toStringCache;

@Override

public synchronized String toString() {

if (toStringCache == null) {

toStringCache = Arrays.copyOfRange(value, 0, count);

}

return new String(toStringCache, true);

}

//省略 n 多代码

}

StringBuilder和StringBuffer 都是 AbstractStringBuilder 的实现类,其底层是用 char[] 数组来储存数据的。StringBuilder默认初始char[]大小是16,当添加的字符大于16个,就会自动扩容。扩容这个操作本质是对数组的复制,也挺费时的,有兴趣的可以看一下源码。而+操作生成的StringBuilder默认大小为16,如果拼接的字符串过大,会频繁的扩容导致效率低下。

所以如果使用StringBuilder的时候,我们可以给一个参数capacity,初始化char[]的大小,这样可以避免频繁的扩容。

toString() 方法就是对 char[] 数组的复制。这可是个挺费时间的操作。

所以,虽然编译器对 + 操作进行了优化,但是由于频繁的实例化StringBuilder、频繁的调用toString()、在添加的字符串较大的情况下还会频繁的扩容,导致其效率极其低下。

但是,+一定比StringBuilder效率低嘛?答案当然是否定的。

3. +什么时候比StringBuilder高效?

我们再上两段代码:

public static void main(String[] args) {

// +

String s1 = "abc"+"123"+"efg";

// StringBuilder

StringBuilder sb = new StringBuilder();

String s2 = sb.append("abc").append("123").append("efg").toString();

}

同样把这段代码反编译一下,你知道,编译器把上面的代码编译成什么样了嘛?

public static void main(String[] args) {

String s1 = "abc123efg";

StringBuilder sb = new StringBuilder();

String s2 = sb.append("abc").append("123").append("efg").toString();

}

编译器直接在编译的时候就把 + 给组合完成了,运行时间为 0 。谁效率高谁效率低显而易见。

所以进行大量的字符串拼接操作 StringBuilder 更合适,而进行少量的,可预知的字符串拼接 + 更合适。(一般 for 循环中用 StringBuilder ,而静态变量等用 +)

4. StringBuilder与StringBuffer的区别。

它们两个的区别主要是 StringBuilder 是线程不安全的,而StringBuffer是线程不安全的。

我们看一下两个类的 append() 方法的源码:

StringBuilder

public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{

// ...

@Override

public StringBuilder append(String str) {

super.append(str);

return this;

}

// ...

}

StringBuffer

public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence{

// ...

@Override

public synchronized StringBuffer append(String str) {

toStringCache = null;

super.append(str);

return this;

}

// ...

}

StringBuilder比StringBuffer少了同步锁。其他基本相同,因为它们都是AbstractStringBuilder的实现类。是双胞胎(其实眼瞎的我莫名感觉他们的名字长得还蛮像的)

所以,在线程安全的情况下,StringBuilder更高效一点(毕竟同步锁也要花时间),而线程不安全就只能用 StringBuffer。

5. String.concat()拼接与StringBuilder的比较

上代码:

String s = "abc".concat("123").concat("efg");

来瞅瞅源码:

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.concat()方法调用了一次Arrays.copyOf(value, len + otherLen)方法,一次性分配了两个个字符串长度的内存空间,只执行了一次空间分配,并对拼接的两个字符各复制了一次,复制了两次。

而StringBuilder的append()在char[]未满的情况下,不会扩容。只会在初始化的时候执行一次空间分配,对两个字符串各复制了一次,复制了两次,但是最后会调用toString(),还会复制一次。

所以,比较之下,在只有两个字符串拼接的情况下,concat的效率要高一点,而大量的字符串拼接,StringBuilder效率会高一点,而且好在初始化的时候,指定char[]容器的大小,这样可以避免过度的扩容。

测试环境

系统:windows10 x64

处理器:i7 9700k

内存:16G

台式电脑

测试代码

public class Demo {

public static void main(String[] args) {

int number = 10000000; //拼接次数

long start ; //开始时间

// concat

String s2 = "";

start = System.currentTimeMillis();

for(int i = 0 ; i < number ; ++i){

s2.concat(String.valueOf(i));

}

System.out.println(" concat 用时:"+ (System.currentTimeMillis() - start) + " 毫秒");

// StringBuilder

StringBuilder stringBuilder = new StringBuilder();

start = System.currentTimeMillis();

for(int i = 0 ; i < number ; ++i){

stringBuilder.append(String.valueOf(i));

}

System.out.println("StringBuilder 用时:"+ (System.currentTimeMillis() - start) + " 毫秒");

//StringBuffer

StringBuffer stringBuffer = new StringBuffer();

start = System.currentTimeMillis();

for(int i = 0 ; i < number ; ++i){

stringBuffer.append(String.valueOf(i));

}

System.out.println(" StringBuffer 用时:"+ (System.currentTimeMillis() - start) + " 毫秒");

// +

String s = "";

start = System.currentTimeMillis();

for(int i = 0 ; i < number ; ++i){

s += String.valueOf(i);

}

System.out.println(" + 用时:"+ (System.currentTimeMillis() - start) + " 毫秒");

}

}

测试结果

测试结果取几次或者1次的平均值整数,不同运行环境会有偏差

+

concat

StringBuilder

StringBuffer

1000次

3ms

1ms

1ms

1ms

100000次

15584ms

10ms

6ms

6ms

1000000次

2212323ms

56ms

34ms

44ms

10000000次

很多很多ms

408ms

353ms

448ms

结论

+最好不要直接大量使用。只有在逻辑较简单的情况下(没有for循环之类)或者字符确定的情况下时候使用。如:String a = "abc"+"123"。

一般情况,在大量字符串拼接操作中。使用StringBuilder和StringBuffer,并尽可能的估算字符串的大小,使用带capacity参数的构造函数,也就是char[]的大小,这样可以避免重复的扩容。

StringBuffer 在线程不安全的情况下使用,其他情况一般使用StringBuilder。

只有两个字符串拼接的时候使用 concat(),这个性能最好。 但是每次拼接操作都会分配内存的操作,而上面两个并不一定每次拼接操作都会分配内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值