Java中数字转字符串方式哪家强?是它!

Java中数字转字符串搞过吧?四种常用的转换方式,究竟用哪种最优呢,本次对

  • Integer.toString(a)
  • String.valueOf(a)
  • a + “”
  • “” + a

四种数字转字符串的方式进行性能探究和分析,本文稍长,如有谬论和建议,欢迎指出。

测试一波

/**
 * CommonTest
 *
 * @author chenyaqiang
 * @date 2020/8/18 22:59
 */
public class CommonTest {
    public static void main(String[] args) {
 		int a = 123456789;
        long start = System.currentTimeMillis();
        for (int i=0; i<500000000; i++){
            String m = a + "";
        }
        long end = System.currentTimeMillis();
        System.out.println("a+"" = " + (end - start));

        start = System.currentTimeMillis();
        for (int i=0; i<500000000; i++){
            String m = "" + a;
        }
        end = System.currentTimeMillis();
        System.out.println("""+a = " + (end - start));

        start = System.currentTimeMillis();
        for (int i=0; i<500000000; i++){
            String n = String.valueOf(a);
        }
        end = System.currentTimeMillis();
        System.out.println("String.valueOf(a) = " +(end-start));

        start = System.currentTimeMillis();
        for (int i=0; i<500000000; i++){
            String n = Integer.toString(a);
        }
        end = System.currentTimeMillis();
        System.out.println("Integer.toString(a) = " +(end-start));
    }
}

1000000数据测试结果:

a+"" = 80
""+a = 37
String.valueOf(a) = 58
Integer.toString(a) = 47

结果为 a+"" > String.valueOf(a) > Integer.toString(a) > “”+a ,

多次试验结果显示Integer.toString(a)与String.valueOf(a)消耗时间相似,Integer.toString(a)小于String.valueOf(a)的情况较多,a+"“始终大于”"+a,且"" + a 消耗时长最小。

500000000数据测试结果:

a+"" = 10962
""+a = 15039
String.valueOf(a) = 17764
Integer.toString(a) = 17873

Java中数字转字符串方式哪家强?是它!

结果为 Integer.toString(a) > String.valueOf(a) > “” + a > a+""

多次试验结果发现""+a消耗时长始终大于a+“”,Integer.toString(a)和String.valueOf(a)结果相似。

结果探究:

点开String.valueOf(int i)源码不难看出:

String.valueOf(int i)其实是调用的Integer.toString(i),所以String.valueOf(int i)调用时间大于Integer.toString(i)比较正常,两者时间应该非常相似。

Java中数字转字符串方式哪家强?是它!

为了验证字符串相加的编译结果,下面给出探究过程:

测试代码:

package com.bestqiang.commontest;

/**
 * CommonTest
 *
 * @author chenyaqiang
 * @date 2020/8/18 22:59
 */
public class CommonTest {
    public static void main(String[] args) {
        int a = 1;
        String str = "hello" + a;

        String str2 = "hello2" + 1;

        String str3 = a + "hello3";

        String str4 = 1 + "hello4";
    }
}

下面是idea的反编译结果,但这个结果其实不是很准确:

Java中数字转字符串方式哪家强?是它!

首先看 str2, 我们把其他代码注释掉,只留下 String str = “hello” + a;

使用javap解析可得:

"C:Program FilesJavajdk1.8.0_221binjavap.exe" -v com.bestqiang.commontest.CommonTest
Classfile /D:/idea-space2/learning-technology/target/test-classes/com/bestqiang/commontest/CommonTest.class
  Last modified 2020-8-19; size 481 bytes
  MD5 checksum 2d6ee54fb564dc0a7fc51ab0a617cfcc
  Compiled from "CommonTest.java"
public class com.bestqiang.commontest.CommonTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = String             #21            // hello21
   #3 = Class              #22            // com/bestqiang/commontest/CommonTest
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               LocalVariableTable
  #10 = Utf8               this
  #11 = Utf8               Lcom/bestqiang/commontest/CommonTest;
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               args
  #15 = Utf8               [Ljava/lang/String;
  #16 = Utf8               str2
  #17 = Utf8               Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               CommonTest.java
  #20 = NameAndType        #5:#6          // "<init>":()V
  #21 = Utf8               hello21
  #22 = Utf8               com/bestqiang/commontest/CommonTest
  #23 = Utf8               java/lang/Object
{
  public com.bestqiang.commontest.CommonTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/bestqiang/commontest/CommonTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2                  // String hello21
         2: astore_1
         3: return
      LineNumberTable:
        line 14: 0
        line 19: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
            3       1     1  str2   Ljava/lang/String;
}
SourceFile: "CommonTest.java"

Process finished with exit code 0

Java中数字转字符串方式哪家强?是它!

解析结果显示,编译器直接优化为了hello21,没有那种StringBuilder追加的情况发生。

再看idea解析的文件,str1和str3仅仅是因为"" + a 和 a + “” 的 改变,反编译结果还变了?让我们用javap看看吧:

我们把其他代码注释掉,只留下 String str = “hello” + a;

使用javap解析可得:

Classfile /D:/idea-space2/learning-technology/target/test-classes/com/bestqiang/commontest/CommonTest.class
  Last modified 2020-8-19; size 705 bytes
  MD5 checksum 69bc22cd50230bd882b407a17dcff463
  Compiled from "CommonTest.java"
public class com.bestqiang.commontest.CommonTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#27         // java/lang/Object."<init>":()V
   #2 = Class              #28            // java/lang/StringBuilder
   #3 = Methodref          #2.#27         // java/lang/StringBuilder."<init>":()V
   #4 = String             #29            // hello
   #5 = Methodref          #2.#30         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #6 = Methodref          #2.#31         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   #7 = Methodref          #2.#32         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #8 = Class              #33            // com/bestqiang/commontest/CommonTest
   #9 = Class              #34            // java/lang/Object
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/bestqiang/commontest/CommonTest;
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               args
  #20 = Utf8               [Ljava/lang/String;
  #21 = Utf8               a
  #22 = Utf8               I
  #23 = Utf8               str
  #24 = Utf8               Ljava/lang/String;
  #25 = Utf8               SourceFile
  #26 = Utf8               CommonTest.java
  #27 = NameAndType        #10:#11        // "<init>":()V
  #28 = Utf8               java/lang/StringBuilder
  #29 = Utf8               hello
  #30 = NameAndType        #35:#36        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #31 = NameAndType        #35:#37        // append:(I)Ljava/lang/StringBuilder;
  #32 = NameAndType        #38:#39        // toString:()Ljava/lang/String;
  #33 = Utf8               com/bestqiang/commontest/CommonTest
  #34 = Utf8               java/lang/Object
  #35 = Utf8               append
  #36 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #37 = Utf8               (I)Ljava/lang/StringBuilder;
  #38 = Utf8               toString
  #39 = Utf8               ()Ljava/lang/String;
{
  public com.bestqiang.commontest.CommonTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/bestqiang/commontest/CommonTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_1
         1: istore_1
         2: new           #2                  // class java/lang/StringBuilder
         5: dup
         6: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         9: ldc           #4                  // String hello
        11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: iload_1
        15: invokevirtual #6                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        18: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        21: astore_2
        22: return
      LineNumberTable:
        line 11: 0
        line 12: 2
        line 19: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            2      21     1     a   I
           22       1     2   str   Ljava/lang/String;
}
SourceFile: "CommonTest.java"

Process finished with exit code 0

Java中数字转字符串方式哪家强?是它!

如上图所示,其实内部是用stringBuilder进行追加操作的。

我们把其他代码注释掉,只留下 String str = “hello” + a;

使用javap解析可得:

Classfile /D:/idea-space2/learning-technology/target/test-classes/com/bestqiang/commontest/CommonTest.class
  Last modified 2020-8-19; size 707 bytes
  MD5 checksum b767896bc82bc01ac0153354ac6e5886
  Compiled from "CommonTest.java"
public class com.bestqiang.commontest.CommonTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#27         // java/lang/Object."<init>":()V
   #2 = Class              #28            // java/lang/StringBuilder
   #3 = Methodref          #2.#27         // java/lang/StringBuilder."<init>":()V
   #4 = Methodref          #2.#29         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   #5 = String             #30            // hello3
   #6 = Methodref          #2.#31         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = Methodref          #2.#32         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #8 = Class              #33            // com/bestqiang/commontest/CommonTest
   #9 = Class              #34            // java/lang/Object
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/bestqiang/commontest/CommonTest;
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               args
  #20 = Utf8               [Ljava/lang/String;
  #21 = Utf8               a
  #22 = Utf8               I
  #23 = Utf8               str3
  #24 = Utf8               Ljava/lang/String;
  #25 = Utf8               SourceFile
  #26 = Utf8               CommonTest.java
  #27 = NameAndType        #10:#11        // "<init>":()V
  #28 = Utf8               java/lang/StringBuilder
  #29 = NameAndType        #35:#36        // append:(I)Ljava/lang/StringBuilder;
  #30 = Utf8               hello3
  #31 = NameAndType        #35:#37        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #32 = NameAndType        #38:#39        // toString:()Ljava/lang/String;
  #33 = Utf8               com/bestqiang/commontest/CommonTest
  #34 = Utf8               java/lang/Object
  #35 = Utf8               append
  #36 = Utf8               (I)Ljava/lang/StringBuilder;
  #37 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #38 = Utf8               toString
  #39 = Utf8               ()Ljava/lang/String;
{
  public com.bestqiang.commontest.CommonTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/bestqiang/commontest/CommonTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_1
         1: istore_1
         2: new           #2                  // class java/lang/StringBuilder
         5: dup
         6: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         9: iload_1
        10: invokevirtual #4                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        13: ldc           #5                  // String hello3
        15: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        18: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        21: astore_2
        22: return
      LineNumberTable:
        line 11: 0
        line 16: 2
        line 19: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            2      21     1     a   I
           22       1     2  str3   Ljava/lang/String;
}
SourceFile: "CommonTest.java"

Java中数字转字符串方式哪家强?是它!

很明显,同样是使用StringBuilder进行的追加操作。

感觉idea的class文件的反编译显示还是有一定问题的,在数据量低的情况下,a+“”速度是快于“”+a的,考虑到可能与此有关。

问题来了,不同数据量导致了实验结果的偏差,难道实验结果的偏差时由于GC导致的?我们加上GC打印再来进行试验试试:

我们把遍历的数据值设置为5w,此时并没有进行垃圾回收,可得结果:

String.valueOf(a) = 15
Integer.toString(a) = 7
a+"" = 25
""+a = 10
Heap
 par new generation   total 78656K, used 26582K [0x00000006c1e00000, 0x00000006c7350000, 0x00000006eb790000)
  eden space 69952K,  38% used [0x00000006c1e00000, 0x00000006c37f5a28, 0x00000006c6250000)
  from space 8704K,   0% used [0x00000006c6250000, 0x00000006c6250000, 0x00000006c6ad0000)
  to   space 8704K,   0% used [0x00000006c6ad0000, 0x00000006c6ad0000, 0x00000006c7350000)
 concurrent mark-sweep generation total 174784K, used 0K [0x00000006eb790000, 0x00000006f6240000, 0x00000007c0000000)
 Metaspace       used 3101K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 324K, capacity 392K, committed 512K, reserved 1048576K

可得结果:a + “” 与 ”“ + a其实是慢于Integer.toString(a)的,”“ + a 速度优于 a + ”“。

现在是50w,此时进行少量垃圾回收,

[GC (Allocation Failure) [ParNew: 69952K->1087K(78656K), 0.0007377 secs] 69952K->1087K(253440K), 0.0007731 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 71039K->1219K(78656K), 0.0005169 secs] 71039K->1219K(253440K), 0.0005370 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 71171K->1214K(78656K), 0.0005665 secs] 71171K->1214K(253440K), 0.0005977 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
""+a = 145
Heap
 par new generation   total 78656K, used 7423K [0x00000006c1e00000, 0x00000006c7350000, 0x00000006eb790000)
  eden space 69952K,   8% used [0x00000006c1e00000, 0x00000006c2410670, 0x00000006c6250000)
  from space 8704K,  13% used [0x00000006c6ad0000, 0x00000006c6bff870, 0x00000006c7350000)
  to   space 8704K,   0% used [0x00000006c6250000, 0x00000006c6250000, 0x00000006c6ad0000)
 concurrent mark-sweep generation total 174784K, used 0K [0x00000006eb790000, 0x00000006f6240000, 0x00000007c0000000)
 Metaspace       used 3269K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K
[GC (Allocation Failure) [ParNew: 69952K->1110K(78656K), 0.0008434 secs] 69952K->1110K(253440K), 0.0008823 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 71062K->1140K(78656K), 0.0009190 secs] 71062K->1140K(253440K), 0.0009522 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Integer.toString(a) = 222
Heap
 par new generation   total 78656K, used 70900K [0x00000006c1e00000, 0x00000006c7350000, 0x00000006eb790000)
  eden space 69952K,  99% used [0x00000006c1e00000, 0x00000006c6220150, 0x00000006c6250000)
  from space 8704K,  13% used [0x00000006c6250000, 0x00000006c636d198, 0x00000006c6ad0000)
  to   space 8704K,   0% used [0x00000006c6ad0000, 0x00000006c6ad0000, 0x00000006c7350000)
 concurrent mark-sweep generation total 174784K, used 0K [0x00000006eb790000, 0x00000006f6240000, 0x00000007c0000000)
 Metaspace       used 3268K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

初步结论:Integer.valueOf(a)其实更快,在数据量大的情况下系统会进行GC,Integer.valueOf(a)GC的时间会更长,判断它因为是CPU密集型计算,GC时占用的资源较多,导致GC耗时时间会更长?这个推论其实不能令人信服,因为测试电脑CPU和内存的资源完全够用了,可能还有其他原因。

所以再次猜测,是由于JIT对热点代码的优化导致的速度差异,验证方法很简单,把OSR给关闭即可。方法为使用JVM参数:-XX:-UseOnStackReplacement。关闭后,Integer.toString(a)效率无论数据量大小,都始终大于“”+a。

总结:

过程: 对Integer.toString(a)、String.valueOf(a) 、a + “”、"" + a等几种数字转字符串的情况通过javap解析字节码文件分析JVM处理情况,查看它们的源码,并分别进行性能测试,分别使用1w、5w、500w等多组数据在for循环中进行执行时间测试,打印CMS垃圾回收器下GC情况。得出中低数据量情况下,Integer.toString(a)耗时最短,高数据量情况下"" + a与a + ""耗时最短。

最终结论:String.valueOf(a)内部调用Integer.toString(a)在源码中可以看出。a+"“与“”+a在javap解析后内部均使用StringBuilder进行相加,时间耗时测试也无特殊差异,另外对于非变量的相加如1+“hello”,使用javap分析可以看出直接优化为“1hello”放入常量池。关键在于Integer.toString(a)与“”+a在不同数据量循环的速度差异问题,考虑到低数据量与高数量结果迥然不同,所以怀疑是热点数据触发OSR编译导致的结果差异,考虑到JVM主要为解释执行+JIT即时编译,JIT检测热点数据的方法为基于计数器的热点探测,调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)的阈值Server模式下默认为1w,这里直接用-XX:-UseOnStackReplacement直接关闭OSR,测试结果为无论数据量大还是小Integer.toString(a)耗时均小于”"+a,得出结论为OSR对热点数据优化为机器指令后“”+a效率大于Integer.toString(a),否则Integer.toString(a)效率大于“”+a。使用JunitPerf开源工具进行接口性能测试可以发现日常使用中两者差距几乎可以忽略,结合OOM思想,建议大家日常使用Integer.toString(a)进行转化操作。

分类:未分类
匿名

发表评论

匿名网友