JVM使用+
优化字符串连接,并将其替换为StringBuilder
。在Scala中应该也是这样的。但是,如果使用++=
连接字符串会发生什么?
var x = "x"
x ++= "y"
x ++= "z"
据我所知,这种方法把字符串当作字符序列来处理,因此即使JVM创建了一个StringBuilder,也会导致许多方法调用,对吗?使用StringBuilder是否更好?字符串会隐式转换为什么类型?
JVM使用+
优化字符串连接,并将其替换为StringBuilder
。在Scala中应该也是这样的。但是,如果使用++=
连接字符串会发生什么?
var x = "x"
x ++= "y"
x ++= "z"
据我所知,这种方法把字符串当作字符序列来处理,因此即使JVM创建了一个StringBuilder,也会导致许多方法调用,对吗?使用StringBuilder是否更好?时间消耗方面有巨大的差异。
如果你使用 +=
反复连接字符串,你无法优化掉创建逐渐变长的字符串所带来的 O(n^2)
的代价。因此,当你需要连接数百个(较短的)字符串时,使用 StringBuilder
的速度会比使用 +=
快 20 倍以上。 (精确数据:将数字 0 到 100 的字符串表示相加,+=
耗时 1.3 微秒,而 StringBuilder
耗时 27.1 微秒;计时误差约为 5%,当然是基于我的机器测试的。)
对于一个 var
String
使用 ++=
更糟糕,因为这样你就让 Scala 将字符串视为一个字符集合进行处理,从而需要所有种类的包装器来使字符串看起来像一个集合(其中包括使用 ++
的通用版本进行盒装的字符级加法!)。现在,在 100 次添加时,你的速度又慢了 16 倍!(精确数据:使用 ++=
对 var 字符串操作需要 428.8 微秒,而 +=
只需要 26.7 微秒。)
如果你写一个有许多个 +
的单独语句,那么 Scala 编译器会使用 StringBuilder 并得出高效的结果(数据:对于从数组中提取的非常量字符串,耗时为 1.8 微秒)。
因此,如果你需要连接字符串但不是使用内联的 +
,而且你关心效率,那么请使用 StringBuilder
。绝对不要使用 ++=
将另一个 String
添加到 var
String
中;这没有任何理由去这样做,而且会导致运行时间变长。
(注意 - 很多时候,您并不关心字符串的添加效率!除非您有理由怀疑该特定代码路径正在频繁调用,否则不要使用额外的StringBuilder使代码变得混乱。)
++=
是逐字符的;@om-nom-nom 关于“普通的追加”是正确的,因为 TraversableLike 的 ++ 委托给了 builder。也许以前不是这样的;就像在 2.10 之前一样。 - som-snytt大规模重构,以便:scala>“hi”==“hi”.reverse.reverse
。这听起来几乎像是paulp的样式。(我应该说TL总是使用builder的++=,而StringBuilder在2.10中被覆盖了。) - som-snyttStringBuilder
仅为 String
重写了 ++=
,但在此处它被定义为 Builder[Char, String]
,因此使用通用路径--无论如何,它都会传递一个参数,该参数仅被认为是 GenTraversableOnce[Char]
,因此它将通过整个未优化的路径。 - Rex KerrStringOps
通常仍然需要进行分配:scala> :pa
// Entering paste mode (ctrl-D to finish)
class Concat {
var x = "x"
x ++= "y"
x ++= "z"
}
// Exiting paste mode, now interpreting.
defined class Concat
scala> :javap -prv Concat
Binary file Concat contains $line3.$read$$iw$$iw$Concat
Size 1211 bytes
MD5 checksum 1900522728cbb0ed0b1d3f8b962667ad
Compiled from "<console>"
public class $line3.$read$$iw$$iw$Concat
SourceFile: "<console>"
[snip]
public $line3.$read$$iw$$iw$Concat();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=6, locals=1, args_size=1
0: aload_0
1: invokespecial #19 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #20 // String x
7: putfield #10 // Field x:Ljava/lang/String;
10: aload_0
11: new #22 // class scala/collection/immutable/StringOps
14: dup
15: getstatic #28 // Field scala/Predef$.MODULE$:Lscala/Predef$;
18: aload_0
19: invokevirtual #30 // Method x:()Ljava/lang/String;
22: invokevirtual #34 // Method scala/Predef$.augmentString:(Ljava/lang/String;)Ljava/lang/String;
25: invokespecial #36 // Method scala/collection/immutable/StringOps."<init>":(Ljava/lang/String;)V
28: new #22 // class scala/collection/immutable/StringOps
31: dup
32: getstatic #28 // Field scala/Predef$.MODULE$:Lscala/Predef$;
35: ldc #38 // String y
37: invokevirtual #34 // Method scala/Predef$.augmentString:(Ljava/lang/String;)Ljava/lang/String;
40: invokespecial #36 // Method scala/collection/immutable/StringOps."<init>":(Ljava/lang/String;)V
43: getstatic #28 // Field scala/Predef$.MODULE$:Lscala/Predef$;
46: invokevirtual #42 // Method scala/Predef$.StringCanBuildFrom:()Lscala/collection/generic/CanBuildFrom;
49: invokevirtual #46 // Method scala/collection/immutable/StringOps.$plus$plus:(Lscala/collection/GenTraversableOnce;Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;
52: checkcast #48 // class java/lang/String
55: invokevirtual #50 // Method x_$eq:(Ljava/lang/String;)V
请查看此答案以获取更多演示。
补充一下,每次重新分配时都会构建字符串,因此您并没有使用单个StringBuilder
。
然而,优化是由javac
而不是JIT编译器完成的,因此为了比较同类产品的结果:
public class Strcat {
public String strcat(String s) {
String t = " hi ";
String u = " by ";
return s + t + u; // OK
}
public String strcat2(String s) {
String t = s + " hi ";
String u = t + " by ";
return u; // bad
}
}
而
$ scala
Welcome to Scala version 2.11.2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_11).
Type in expressions to have them evaluated.
Type :help for more information.
scala> :se -Xprint:typer
scala> class K { def f(s: String, t: String, u: String) = s ++ t ++ u }
[[syntax trees at end of typer]] // <console>
def f(s: String, t: String, u: String): String = scala.this.Predef.augmentString(scala.this.Predef.augmentString(s).++[Char, String](scala.this.Predef.augmentString(t))(scala.this.Predef.StringCanBuildFrom)).++[Char, String](scala.this.Predef.augmentString(u))(scala.this.Predef.StringCanBuildFrom)
很糟糕。甚至更糟糕的是,解释Rex的话可能会让情况更加复杂。
"abc" ++ "def"
augmentString("abc").++[Char, String](augmentString("def"))(StringCanBuildFrom)
collection.mutable.StringBuilder.newBuilder ++= new WrappedString(augmentString("def"))
val b = collection.mutable.StringBuilder.newBuilder
new WrappedString(augmentString("def")) foreach b.+=
StringBuilder
重写了 ++=(String)
但没有重写Growable.++=(Traversable[Char])
。unaugmentString
的作用: 28: invokevirtual #40 // Method scala/Predef$.augmentString:(Ljava/lang/String;)Ljava/lang/String;
31: invokevirtual #43 // Method scala/Predef$.unaugmentString:(Ljava/lang/String;)Ljava/lang/String;
34: invokespecial #46 // Method scala/collection/immutable/WrappedString."<init>":(Ljava/lang/String;)V
为了说明你最终会调用未装饰的 +=(Char)
,但在装箱和拆箱之后:
public final scala.collection.mutable.StringBuilder apply(char);
flags: ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: getfield #19 // Field b$1:Lscala/collection/mutable/StringBuilder;
4: iload_1
5: invokevirtual #24 // Method scala/collection/mutable/StringBuilder.$plus$eq:(C)Lscala/collection/mutable/StringBuilder;
8: areturn
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this L$line10/$read$$iw$$iw$$anonfun$1;
0 9 1 x C
LineNumberTable:
line 9: 0
public final java.lang.Object apply(java.lang.Object);
flags: ACC_PUBLIC, ACC_FINAL, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokestatic #35 // Method scala/runtime/BoxesRunTime.unboxToChar:(Ljava/lang/Object;)C
5: invokevirtual #37 // Method apply:(C)Lscala/collection/mutable/StringBuilder;
8: areturn
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this L$line10/$read$$iw$$iw$$anonfun$1;
0 9 1 v1 Ljava/lang/Object;
LineNumberTable:
line 9: 0
开怀大笑确实能够让氧气进入血液中。