JDK 9/JEP 280: String Concatenations Will Never Be the Same
Since JDK 9, string concatenations have undergone a significant change.
Join the DZone community and get the full member experience.
Join For FreeJEP 280 ("Indify String Concatenation") was implemented in conjunction with JDK 9 and, according to its "Summary" section, "Change[s] the static String
-concatenation bytecode sequence generated by javac to use invokedynamic calls to JDK library functions." The impact this has on string concatenation in Java is most easily seen by looking at the javap output of classes using string concatenation that is compiled in pre-JDK 9 and post-JDK 9 JDKs.
The following simple Java class named "HelloWorldStringConcat" will be used for the first demonstration.
package dustin.examples;
import static java.lang.System.out;
public class HelloWorldStringConcat
{
public static void main(final String[] arguments)
{
out.println("Hello, " + arguments[0]);
}
}
Contrasting the differences in -verbose output from javap
for the HelloWorldStringConcat
class's main(String)
method when compiled with JDK 8 (AdoptOpenJDK) and JDK 11 (Oracle OpenJDK) is shown next. I have highlighted some key differences.
JDK 8 javap
Output
Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringConcat.class
Last modified Jan 28, 2019; size 625 bytes
MD5 checksum 3e270bafc795b47dbc2d42a41c8956af
Compiled from "HelloWorldStringConcat.java"
public class dustin.examples.HelloWorldStringConcat
minor version: 0
major version: 52
. . .
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."10: ldc #5 // String Hello,
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_0
16: iconst_0
17: aaload
":()V 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 27: return
JDK 11 javap
Output
Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringConcat.class
Last modified Jan 28, 2019; size 908 bytes
MD5 checksum 0e20fe09f6967ba96124abca10d3e36d
Compiled from "HelloWorldStringConcat.java"
public class dustin.examples.HelloWorldStringConcat
minor version: 0
major version: 55
. . .
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: iconst_0
5: aaload
6: invokedynamic #3, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
11: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: return
The "Description" section of JEP 280 describes this difference: "The idea is to replace the entire StringBuilder
append dance with a simple invokedynamic
call to java.lang.invoke.StringConcatFactory that will accept the values in the need of concatenation." This same section shows a similar comparison of compiled output for a similar string concatenation example.
The compiled output from JDK 11 for the simple string concatenation is not just fewer lines than its JDK 8 counterpart; it also has fewer "expensive" operations. Potential performance improvement can be gained from not needing to wrap primitive types and not needing to instantiate a bunch of extra objects. One of the primary motivations for this change was to "lay the groundwork for building optimized String
concatenation handlers, implementable without the need to change the Java-to-bytecode compiler" and to "enable future optimizations of String
concatenation without requiring further changes to the bytecode emitted by javac
."
There's an interesting implication of this in terms of using StringBuffer (which I have a difficult time finding a good use for anyway) and StringBuilder. It was a stated "Non-Goal" of JEP 280 to not "introduce any new String
and/or StringBuilder
APIs that might help to build better translation strategies." Related to this, for simple string concatenations like that shown in the code example at the beginning of this post, explicit use of StringBuilder
and StringBuffer
will actually preclude the ability for the compiler to make use of the JEP 280-introduced feature discussed in this post.
The next two code listings show similar implementations to the simple application shown above, but these use StringBuilder
and StringBuffer
respectively instead of string concatenation. When javap -verbose
is executed against these classes after they are compiled with JDK 8 and with JDK 11, there are no significant differences in the main(String[])
methods.
Explicit StringBuilder
Use in JDK 8 and JDK 11 Are the Same
package dustin.examples;
import static java.lang.System.out;
public class HelloWorldStringBuilder
{
public static void main(final String[] arguments)
{
out.println(new StringBuilder().append("Hello, ").append(arguments[0]).toString());
}
}
Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringBuilder.class
Last modified Jan 28, 2019; size 627 bytes
MD5 checksum e7acc3bf0ff5220ba5142aed7a34070f
Compiled from "HelloWorldStringBuilder.java"
public class dustin.examples.HelloWorldStringBuilder
minor version: 0
major version: 52
. . .
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."":()V 10: ldc #5 // String Hello, 12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15: aload_0 16: iconst_0 17: aaload 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 27: return
Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringBuilder.class
Last modified Jan 28, 2019; size 627 bytes
MD5 checksum d04ee3735ce98eb6237885fac86620b4
Compiled from "HelloWorldStringBuilder.java"
public class dustin.examples.HelloWorldStringBuilder
minor version: 0
major version: 55
. . .
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."":()V 10: ldc #5 // String Hello, 12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15: aload_0 16: iconst_0 17: aaload 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 27: return
Explicit StringBuffer
Use in JDK 8 and JDK 11 Are the Same
package dustin.examples;
import static java.lang.System.out;
public class HelloWorldStringBuffer
{
public static void main(final String[] arguments)
{
out.println(new StringBuffer().append("Hello, ").append(arguments[0]).toString());
}
}
Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringBuffer.class
Last modified Jan 28, 2019; size 623 bytes
MD5 checksum fdfb90497db6a3494289f2866b9a3a8b
Compiled from "HelloWorldStringBuffer.java"
public class dustin.examples.HelloWorldStringBuffer
minor version: 0
major version: 52
. . .
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuffer
6: dup
7: invokespecial #4 // Method java/lang/StringBuffer."":()V 10: ldc #5 // String Hello, 12: invokevirtual #6 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 15: aload_0 16: iconst_0 17: aaload 18: invokevirtual #6 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 21: invokevirtual #7 // Method java/lang/StringBuffer.toString:()Ljava/lang/String; 24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 27: return
Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringBuffer.class
Last modified Jan 28, 2019; size 623 bytes
MD5 checksum e4a83b6bb799fd5478a65bc43e9af437
Compiled from "HelloWorldStringBuffer.java"
public class dustin.examples.HelloWorldStringBuffer
minor version: 0
major version: 55
. . .
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuffer
6: dup
7: invokespecial #4 // Method java/lang/StringBuffer."":()V 10: ldc #5 // String Hello, 12: invokevirtual #6 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 15: aload_0 16: iconst_0 17: aaload 18: invokevirtual #6 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 21: invokevirtual #7 // Method java/lang/StringBuffer.toString:()Ljava/lang/String; 24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 27: return
JDK 8 and JDK 11 Handling of Looped String Concatenation
For my last example of JEP 280 changes in action, I use a code sample that may offend some Java developers' sensibilities and perform a string concatenation within a loop. Keep in mind this is just an illustrative example and all will be okay, but don't try this at home.
package dustin.examples;
import static java.lang.System.out;
public class HelloWorldStringConcatComplex
{
public static void main(final String[] arguments)
{
String message = "Hello";
for (int i=0; i<25; i++)
{
message += i;
}
out.println(message);
}
}
Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringConcatComplex.class
Last modified Jan 30, 2019; size 766 bytes
MD5 checksum 772c4a283c812d49451b5b756aef55f1
Compiled from "HelloWorldStringConcatComplex.java"
public class dustin.examples.HelloWorldStringConcatComplex
minor version: 0
major version: 52
. . .
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: ldc #2 // String Hello
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: bipush 25
8: if_icmpge 36
11: new #3 // class java/lang/StringBuilder
14: dup
15: invokespecial #4 // Method java/lang/StringBuilder."19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: iload_2
23: invokevirtual #6 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
26: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
29: astore_1
30: iinc 2, 1
":()V 18: aload_1 33: goto 5 36: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 39: aload_1 40: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 43: return
Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringConcatComplex.class
Last modified Jan 30, 2019; size 1018 bytes
MD5 checksum 967fef3e7625965ef060a831edb2a874
Compiled from "HelloWorldStringConcatComplex.java"
public class dustin.examples.HelloWorldStringConcatComplex
minor version: 0
major version: 55
. . .
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // String Hello
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: bipush 25
8: if_icmpge 25
11: aload_1
12: iload_2
13: invokedynamic #3, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
18: astore_1
19: iinc 2, 1
22: goto 5
25: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
28: aload_1
29: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
32: return
In the presentation "Enough java.lang.String to Hang Ourselves ...," Dr. Heinz M. Kabutz and Dmitry Vyazelenko discuss the JEP 280-introduced changes to Java string concatenation and summarize it succinctly, "+
is no longer compiled to StringBuilder
." In their "Lessons from Today" slide, they state, "Use +
instead of StringBuilder
where possible" and "recompile classes for Java 9+."
The changes implemented in JDK 9 for JEP 280 "will enable future optimizations of String
concatenation without requiring further changes to the bytecode emitted by javac
." Interestingly, it was recently announced that JEP 348 ("Java Compiler Intrinsics for JDK APIs") is now a Candidate JEP and it aims to use a similar approach for compiling methods String::format
and Objects::hash
.
Published at DZone with permission of Dustin Marx, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments