Between your two scenarios, option 1 will be faster every time, unless the JITC does something I don't expect. I really don't expect it to make a difference, though, unless you call those methods extremely frequently.
Why? Because you actually don't create new objects with option 1. The compiler should perform constant folding, turning "Hello" + "World..!"
into "HelloWorld..!"
. And because this is a compile-time constant String
, it's automatically interned into the String
pool at VM startup. So every time that method is called, you're just getting a reference to that "canonical" String
. No object creation is performed.
In option 2, you always create multiple objects -- the StringBuffer
(you should be using StringBuilder
, by the way), the backing char[]
, and the result String
(at the very least). And doing that in a tight loop is not very efficient.
In addition, option 1 is more readable, which is always something you should consider when writing code.
Proof:
Given this test code:
public class Test {
public String getTestMessageA() {
return "Hello" + "World..!";
}
public String getTestMessageB() {
return new StringBuffer("Hello").append("World..!").toString();
}
}
Compiling with javac -XD-printflat
shows us what this code is processed to before compilation to bytecode:
public class Test {
public Test() {
super();
}
public String getTestMessageA() {
return "HelloWorld..!";
}
public String getTestMessageB() {
return new StringBuffer("Hello").append("World..!").toString();
}
}
Notice how "Hello" + "World..!"
was converted at compile time to a single String
. So String
concatenation is not what is happening in the first option.
Now let's look at the bytecode. Here's the constant pool:
Constant pool:
#1 = Methodref #10.#20 // java/lang/Object."<init>":()V
#2 = String #21 // HelloWorld..!
#3 = Class #22 // java/lang/StringBuffer
#4 = String #23 // Hello
#5 = Methodref #3.#24 // java/lang/StringBuffer."<init>":(Ljava/lang/String;)V
#6 = String #25 // World..!
#7 = Methodref #3.#26 // java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
#8 = Methodref #3.#27 // java/lang/StringBuffer.toString:()Ljava/lang/String;
Here's the bytecode for option 1:
public java.lang.String getTestMessageA();
Code:
0: ldc #2 // String HelloWorld..!
2: areturn
Well, that's short. As you can see, the JVM loads a constant (ldc
) from the pool and returns it. No object created directly in the method.
Now here's the bytecode for option 2:
public java.lang.String getTestMessageB();
Code:
0: new #3 // class java/lang/StringBuffer
3: dup
4: ldc #4 // String Hello
6: invokespecial #5 // Method java/lang/StringBuffer."<init>":(Ljava/lang/String;)V
9: ldc #6 // String World..!
11: invokevirtual #7 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
14: invokevirtual #8 // Method java/lang/StringBuffer.toString:()Ljava/lang/String;
17: areturn
So this code creates a new StringBuffer
, loads the appropriate constants from the string pool, calls the append()
method for each of them, then calls the toString()
method on the buffer. The toString()
is implemented as such:
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
So option 2 will create two new objects every time you call it, and also executes more instructions. Thus, option 1 will be faster.