In this article, I show you how to convert an int into a String in Java the fastest way. The answer is probably surprising for some. I present four variants and measure and compare their speed using JMH microbenchmarks. I analyze the measurement results looking at the Java source code and also the generated bytecode. If you want to skip the details, you can use this link to scroll down directly to the result.
Java int-to-String conversion variants
The following four options are available (apart from intentionally more complicated variants):
- Option 1:
Integer.toString(i)
- Option 2:
String.valueOf(i)
- Option 3:
String.format("%d", i)
- Option 4:
"" + i
In the following sections, I first perform detailed benchmarks and then interpret the results based on the Java source code and the generated bytecode.
Performance measurements of the int-to-String conversion
To find out which of the options is the fastest, I ran several benchmarks with the Java Microbenchmark Harness – short: JMH.
JMH is a framework that facilitates benchmark tests for short code sections and provides meaningful measurements in milli-, micro- and nano-second ranges. Tests are repeated hundreds of thousands of times, and the actual measurement process is only started after a warm-up phase to give the just-in-time compiler sufficient lead time for code optimization.
A good tutorial for beginners can be found on tutorials.jenkov.com.
IntelliJ comes with a JMH plugin by default so that you can run the benchmark tests directly in your IDE.
Source code of the microbenchmarks
Below you find the complete source code of the int-to-String benchmark. You can copy the code directly into your IDE or clone it as a Maven project from my GitHub repository. When you create a project yourself, you need to add the following two dependencies:
- JMH core: org.openjdk.jmh:jmh-core
- JMH annotation processor: org.openjdk.jmh:jmh-generator-annprocess (in "provided" scope )
package eu.happycoders.int2string;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import java.util.concurrent.ThreadLocalRandom;
public class IntToStringBenchmark {
@State(Scope.Thread)
public static class MyState {
public int i;
@Setup(Level.Invocation)
public void doSetup() {
// always 7-digits, so that the String always has the same length
i = 1_000_000 + ThreadLocalRandom.current().nextInt(9_000_000);
}
}
@Benchmark
public void option1(MyState state, Blackhole blackhole) {
String s = Integer.toString(state.i);
blackhole.consume(s);
}
@Benchmark
public void option2(MyState state, Blackhole blackhole) {
String s = String.valueOf(state.i);
blackhole.consume(s);
}
@Benchmark
public void option3(MyState state, Blackhole blackhole) {
String s = String.format("%d", state.i);
blackhole.consume(s);
}
@Benchmark
public void option4(MyState state, Blackhole blackhole) {
String s = "" + state.i;
blackhole.consume(s);
}
}
Code language: Java (java)
A few remarks about the source code:
- We assign a random number to the int-variable
i
so that it is not replaced by a constant, which would result in the complete String conversion being optimized away. - The random number is generated in the
setup()
method of a so-called "state" so that the execution time for it is not measured. - The
@Setup(Level.Invocation)
annotation causes thesetup()
method to be executed before each invocation of the test method; thus, each invocation receives a new random number. - The conversion result is always passed to the
Blackhole
– again so that the compiler does not optimize away the conversion.
Microbenchmark results
In the following sections, you find the measurement results on my Dell XPS 15 9570 with an Intel Core i7-8750H. Detailed results (including all single tests, minimums, maximums, and standard deviations) can be found in the results/
directory of my GitHub repository.
Measurement results int-to-String on Java 7
Method | Operations per second | Confidence interval (99.9%) |
Integer.toString(i) | 20,365,947 | 20,276,015 – 20,455,879 |
String.valueOf(i) | 20,318,316 | 20,251,621 – 20,385,011 |
String.format("%d", i) | 2,107,397 | 2,075,553 – 2,139,240 |
"" + i | 23,358,668 | 23,178,506 – 23,538,831 |
The first two variants can be regarded as equally fast, which is to be expected since String.valueOf(i)
simply calls Integer.toString(i)
, and this call is inlined by the HotSpot compiler.
As expected, the third variant is slower, since the format string must be parsed here.
The fourth variant ("" + i
) is significantly faster under Java 7 (almost 15%) than the first two variants.
Measurement results int-to-string on Java 8
Method | Operations per second | Confidence interval (99.9%) |
Integer.toString(i) | 20,939,910 | 20,699,671 – 21,180,149 |
String.valueOf(i) | 20,920,359 | 20,737,898 – 21,102,821 |
String.format("%d", i) | 2,284,027 | 2,218,004 – 2,350,050 |
"" + i | 23,777,738 | 23,651,239 – 23,904,237 |
Java 8 shows a very similar result as Java 7, with all variants having increased in speed between 2% and 8%.
Measurement results int-to-string on Java 9
Method | Operations per second | Confidence interval (99.9%) |
Integer.toString(i) | 28,025,700 | 27,829,430 – 28,221,969 |
String.valueOf(i) | 27,732,474 | 27,646,937 – 27,818,010 |
String.format("%d", i) | 2,718,377 | 2,680,574 – 2,756,179 |
"" + i | 28,354,690 | 28,151,883 – 28,557,497 |
Java 9 makes it interesting: All variants have significantly increased throughput (20 to 30%). However, the margin of variant four ("" + i
) has dropped back to about 2%. To exclude measurement inaccuracies, I repeated the test several times.
Measurement results int-to-string on Java 11
I skip Java 10. Java 11 is the current LTS (Long Term Support) release. Java 9 was the first release after the new release cycle and came three and a half years after Java 8, so I included it.
Method | Operations per second | Confidence interval (99.9%) |
Integer.toString(i) | 27,755,914 | 27,537,830 – 27,973,998 |
String.valueOf(i) | 27,836,735 | 27,676,576 – 27,996,894 |
String.format("%d", i) | 2,717,551 | 2,602,165 – 2,832,937 |
"" + i | 28,237,066 | 27,965,904 – 28,508,227 |
There were no significant changes from Java 9 to Java 11. I attribute the minimal fluctuations to measurement inaccuracies.
Measurement results int-to-string on Java 13
I also skip Java 12 and come directly to the current release, Java 13.
Method | Operations per second | Confidence interval (99.9%) |
Integer.toString(i) | 27,664,976 | 27,591,996 – 27,737,955 |
String.valueOf(i) | 27,718,096 | 27,646,080 – 27,790,112 |
String.format("%d", i) | 1,800,345 | 1,763,017 – 1,837,672 |
"" + i | 28,293,228 | 28,156,241 – 28,430,215 |
The variants one, two, and four are virtually unchanged; variant four is still the front runner. Something interesting happened to variant three (String.format("%d", i)
): compared to Java 11 it is 34% slower.
Measurement results int-to-string on Java 14
For the sake of completeness, I also tested the latest early access build of Java 14 (ea+19).
Method | Operations per second | Confidence interval (99.9%) |
Integer.toString(i) | 27,642,630 | 27,484,253 – 27,801,007 |
String.valueOf(i) | 27,571,938 | 27,456,427 – 27,687,448 |
String.format("%d", i) | 1,828,382 | 1,780,958 – 1,875,807 |
"" + i | 28,226,175 | 28,030,308 – 28,422,042 |
Here we see almost the same result as with Java 13. "" + i
is still the fastest way. The 2% lead over the first two variants has proven to be stable in the last four Java versions measured so that we can rule out measurement uncertainty.
Measurement result overview of all Java versions
Here you can see all measurement results summarized in one diagram:
We can summarize – regardless of the Java version:
"" + i
is the fastest method
to convert an int
into a String
.
… whereby the margin up to Java 8 with almost 15% was clearly more significant than since Java 9 with about 2%.
Analysis of performance differences
The measurements have raised the following questions, which I would like to clarify in this section:
- Why have all variants significantly increased in speed in Java 9?
- Why does
"" + i
perform best throughout all Java versions? - Why has
String.format(i)
become so slow in Java 13?
I will, therefore, analyze the JDK source code and the generated byte code.
Why have all variants significantly increased in speed in Java 9?
My first guess was that "Compact Strings", which are enabled by default in Java 9, are responsible for the increased performance. However, disabling them (VM option "-XX:-CompactStrings") did not result in any relevant speed change.
My second approach was to compare the Java source code of the Integer.toString(i)
methods of Java 8 and Java 9. While the Java 9 code is easy to understand, the Java 8 code looks pretty cryptic and optimized. To see if it's this code change (and not JVM optimizations), I extracted the Java 8 source code from Integer, copied it into a class Integer8
under Java 9, and repeated the benchmark test with it. And indeed: The Integer8.toString(i)
-Method was also much slower under Java 9, but a bit faster than under Java 8. So the main reason for the performance gain lies in code improvements, and besides, there are some JVM optimizations.
Method | Operations per second | Confidence interval (99.9%) |
Java 8 code on Java 8 | 20,939,910 | 20,699,671 – 21,180,149 |
Java 8 code on Java 9 | 21,737,981 | 21,517,415 – 21,958,547 |
Java 9 code on Java 9 | 28,025,700 | 27,829,430 – 28,221,969 |
I didn't put the corresponding source code in my GitHub repository, because I'm not sure to what extent I can publish code from the JDK or even parts of it.
I have not further investigated variants three and four at this point. I assume that also, their algorithms have been massively improved.
Why does "" + i
perform best throughout all Java versions?
To find out why "" + i
is the fastest, let's first look at the generated byte code. We do this as follows:
We create a file IntToStringFast.java
with the following content.
package eu.happycoders.int2string;
import java.util.Random;
public class IntToStringFast {
public static void main(String[] args) {
int i = new Random().nextInt();
System.out.println("" + i);
}
}
Code language: Java (java)
We compile the file as follows:
javac IntToStringFast.java
Code language: plaintext (plaintext)
And look at the byte code with the following command:
javap -c IntToStringFast.class
Code language: plaintext (plaintext)
Variant "" + i
on Java 7 and Java 8
With Java 7 and Java 8 the following byte code is generated (here only the relevant excerpt):
14: new #6 // class java/lang/StringBuilder
17: dup
18: invokespecial #7 // Method java/lang/StringBuilder."":()V
21: ldc #8 // String
23: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
26: iload_1
27: invokevirtual #10 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
30: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
Code language: Java (java)
The bytecode corresponds to the following Java code (interestingly append("")
was not eliminated – this task is apparently left to the HotSpot compiler):
new StringBuilder().append("").append(i).toString();
Code language: Java (java)
To confirm this assumption, I repeat the benchmark with the StringBuilder.append(i)
code and come to the same result as for "" + i
.
Looking at the AbstractStringBuilder.append(int i)
method, we realize that in both Java 7 and Java 8, it contains virtually the same code as Integer.toString(int i)
. The only difference I could see is that StringBuilder
internally first creates a char array of length 16, which is then copied in the toString()
method via System.arraycopy()
into an array of the required length; while Integer.toString()
creates a char array of the final required length from the start. To check if this makes a difference, I create another test in which I pass 7 as capacity when creating the StringBuilder
(only 7-digit random numbers are generated in the test). However, this also leads to the same result.
This is a short interim result of my current test:
Benchmark Mode Cnt Score Error Units
IntToStringBenchmarkStringBuilder.integerToString thrpt 2 21168090.044 ops/s
IntToStringBenchmarkStringBuilder.stringBuilderCapacity7 thrpt 2 23968649.108 ops/s
IntToStringBenchmarkStringBuilder.stringBuilderCapacityDefault thrpt 2 23769306.792 ops/s
IntToStringBenchmarkStringBuilder.stringPlus thrpt 2 23989334.180 ops/s
Code language: plaintext (plaintext)
All StringBuilder
variations are almost equally fast and, still, significantly faster than Integer.toString()
. To get to the bottom of this, I copy the source code from both Integer
and StringBuilder
and run the tests again. I get the following result (the benchmarks with the "8" are the ones with the copied source code):
Benchmark Mode Cnt Score Error Units
IntToStringBenchmarkStringBuilderInline.integerToString thrpt 2 20518228.534 ops/s
IntToStringBenchmarkStringBuilderInline.integer8ToString thrpt 2 19681140.450 ops/s
IntToStringBenchmarkStringBuilderInline.stringBuilder thrpt 2 23873235.183 ops/s
IntToStringBenchmarkStringBuilderInline.stringBuilder8 thrpt 2 19990576.858 ops/s
Code language: plaintext (plaintext)
Interesting: In the copied source code, Integer.toString(i)
and "" + i
are almost equally fast – as I would have expected after viewing the source code. However, both copied classes are slower than the classes from the JDK. So what does this mean?
Do the compiled classes in the JDK not match the source code provided? To check this, I remove the file src.zip from the JDK directory so that IntelliJ does not display the source code when clicking on a class, but decompiles the class. The decompiled class looks exactly like the source code (except that local variable names are generic).
At this point, I stop the analysis for Java 7 and Java 8 for the time being. You could now use the VM options -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
to see how HotSpot optimizes the code in detail, but I don't have enough time to do that.
Variante "" + i
since Java 9
Since Java 9, different byte code is generated from "" + i
– only a single line:
15: invokedynamic #6, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
Code language: Java (java)
The makeConcatWithConstants()
method "facilitate the creation of String concatenation methods, that can be used to efficiently concatenate a known number of arguments of known types" (see StringConcatFactory's JavaDoc). I looked at the source code of StringConcatFactory.makeConcatWithConstants()
. Eventually, it also invokes StringBuilder.append(int)
using a MethodHandle. Therefore also here I cannot see why this variant is faster than Integer.toString()
. Again, sophisticated HotSpot optimizations seem to be at work.
Why has String.format(i)
become so slow in Java 13?
The method String.format()
invokes – in both Java 11 and Java 13 – Formatter().format(format, args).toString()
. To check if it's the int-to-String conversion or the formatter in general, I do a test with the format string "%s". As parameter, I pass a random number again, which I already convert into a String in the "state" and pass it as such to the test method (you can also find this test in the GitHub repository).
Java version | Operations per second | Confidence interval (99.9%) |
Java 11 | 2,978,241 | 2,377,067 – 3,579,416 |
Java 13 | 1,924,183 | 1,624,398 – 2,223,968 |
So the formatter has generally become much slower, not only when converting integers to Strings. Unfortunately, I don't have the time to analyze further details here. This article has already become much longer than originally planned.
Summary
Extensive benchmark tests have shown that across all Java versions, "" + i
is the fastest way to convert an integer to a String. While the margin for Java 7 and Java 8 was still an impressive 15%, it has dropped to about 2% since Java 9, so today it's basically a matter of taste which variant you use.
Unfortunately, I did not succeed in finding out the underlying reason. Do you know the cause? Or do you know other performant ways to convert ints into Strings? Then I look forward to your comment!
In the next article, I'll show you what to consider in the opposite direction, i.e. when converting Strings into ints.