Skip to content

How to Convert int to String in Java – Which Way Is Fastest?

Stacked skeins of yarn in pink, blue and green

Converting an int to a String – Java gives you four ways to do it. Which one is the fastest?

I measured all four with JMH microbenchmarks: on Java 8 to 25, on an x86 and an arm64 machine. The result is clear – and it is a different one than the first version of this article claimed in 2019. I’ll show you why the old answer was wrong, too.

If you only want the recommendation: Jump straight to the summary.

Four ways to convert an int to a String

Leaving aside deliberately convoluted constructions, these are the four options:

  • Option 1: Integer.toString(i)
  • Option 2: String.valueOf(i)
  • Option 3: String.format("%d", i)
  • Option 4: "" + i

String.valueOf(i) calls Integer.toString(i) internally. String.format() sends the number through the Formatter. And "" + i is the variant where it gets interesting: since Java 9, it is no longer the compiler that decides what this becomes, but the JVM. More on that later.

The benchmark setup

For the measurements, I use the Java Microbenchmark Harness, JMH for short – a framework that runs short pieces of code millions of times, starts measuring only after a warm-up phase for the JIT compiler, and finally reports throughput along with an error interval.

You can find a good beginner’s tutorial by Jakob Jenkov. For IntelliJ, there is a JMH plugin that lets you start benchmarks straight from the IDE.

The benchmark

You can find the complete source code in my GitHub repository. The benchmark itself is short:

public class IntToStringBenchmark {

  @Benchmark
  public void option1(RandomInts state, Blackhole blackhole) {
    String s = Integer.toString(state.next());
    blackhole.consume(s);
  }

  @Benchmark
  public void option2(RandomInts state, Blackhole blackhole) {
    String s = String.valueOf(state.next());
    blackhole.consume(s);
  }

  @Benchmark
  public void option3(RandomInts state, Blackhole blackhole) {
    String s = String.format("%d", state.next());
    blackhole.consume(s);
  }

  @Benchmark
  public void option4(RandomInts state, Blackhole blackhole) {
    String s = "" + state.next();
    blackhole.consume(s);
  }

}

The numbers come from a JMH state. It initializes an array of 1,024 random numbers once, up front, and every call of the benchmark method picks one of them through the next() method.

@State(Scope.Thread)
public class RandomInts {

  private static final int SIZE = 1024;

  private final int[] values = new int[SIZE];
  private int index;

  @Setup(Level.Trial)
  public void doSetup() {
    ThreadLocalRandom random = ThreadLocalRandom.current();
    for (int k = 0; k < SIZE; k++) {
      values[k] = 1_000_000 + random.nextInt(9_000_000);
    }
  }

  public int next() {
    return values[index++ & (SIZE - 1)];
  }

}

Three things about this are deliberate:

  • The numbers are random, so that the JIT compiler cannot optimize the conversion away – converting a constant to a String, it would simply replace with the result of that conversion.
  • All numbers have seven digits, so that every String has the same length.
  • The result goes into a Blackhole – for the same reason as the first point.

And a fourth detail is the reason for the new version of this article: the 1,024 numbers are drawn once per trial (Level.Trial), not before every single invocation.

The test environment

I measured on two machines:

  • a Dell XPS 17 with an Intel Core i7-12700H (six performance and eight efficiency cores), under WSL2, and
  • a Mac with an Apple M5 Pro (18 cores).

The Java versions used were 8, 11, 17, 21, 22, 23, 24, and 25, each a current OpenJDK build. JMH ran with three forks of five warm-up and five measurement iterations of five seconds each – and with the GC profiler, which reports the bytes allocated per call for every variant.

One more detail: the bytecode for "" + i depends on the compiler. That is why I built a separate JAR for each Java version, with that version’s javac compiler. So every column of the tables shows you what you get when you compile and run your code with exactly that Java version. What happens when bytecode compiled with Java 8 runs on a new JVM, I’ll show you in the section “Under the hood”.

The results

The tables show how long one conversion takes, in nanoseconds – the fewer, the better. The error intervals (99.9%) are below 2% almost everywhere and are left out for readability; you can find the raw data with all iterations, error intervals, and bytes per call as JMH JSON in the results/ directory of the benchmark repository.

The last row of each table – new StringBuilder().append("").append(i).toString() – is not one of the four variants but a fifth measurement. This is exactly the code the compiler turns "" + i into up to Java 8. I measured it because it explains why "" + i is the odd one out on Java 8: you can see it in the Java 8 numbers – identical.

Intel Core i7-12700H (x86)

Java 8Java 11Java 17Java 21Java 22Java 23Java 24Java 25
Integer.toString(i)14.59.09.48.68.18.18.07.1
String.valueOf(i)15.08.79.28.68.18.18.07.1
"" + i19.28.39.28.68.28.18.07.1
String.format("%d", i)183.0157.654.358.857.754.654.954.7
explicit StringBuilder19.315.615.714.811.213.311.19.0

The chart for it leaves out String.format(): at 183 nanoseconds, that bar would squeeze the other four into stripes. This variant gets a chart of its own further down. And the chart shows only the LTS line plus the current GA release – Java 8, 11, 17, 21, and 25; the in-between versions 22 to 24 are in the table above.

int to String – nanoseconds per conversion on the Intel Core i7-12700H, Java 8 to 25
Nanoseconds per conversion on the Intel Core i7-12700H (x86)

Apple M5 Pro (arm64)

Java 8Java 11Java 17Java 21Java 22Java 23Java 24Java 25
Integer.toString(i)9.66.86.77.26.46.36.36.3
String.valueOf(i)9.66.86.77.16.46.26.36.3
"" + i14.76.56.87.26.46.36.26.2
String.format("%d", i)147.6150.3154.344.743.843.342.441.8
explicit StringBuilder14.79.29.38.57.812.47.67.4

Here, too, the chart is without String.format() and without the versions 22 to 24, for the same reasons.

int to String – nanoseconds per conversion on the Apple M5 Pro, Java 8 to 25
Nanoseconds per conversion on the Apple M5 Pro (arm64)

What the numbers say

Three variants, one operation

Since Java 17, Integer.toString(i), String.valueOf(i), and "" + i are on par – on both machines, in every version. The largest difference is two percent, on Java 17 under x86; from Java 21 on it is less than one percent. The GC profiler confirms that this is no coincidence: all three allocate 48 bytes per conversion, namely exactly one String with a byte array for seven digits.

It is the same work. Integer.toString() computes the number of digits first, allocates an array of exactly that length, and writes the digits into it. And since Java 9, "" + i does the same – by way of a detour I’ll show you in a moment.

On Java 11, "" + i is even 4% ahead. On Java 8, however, it is clearly behind: 28% slower on x86, 53% slower on the M5 Pro. The reason is the StringBuilder chain that javac 8 turns it into – with a buffer for 16 characters that is copied into an array of the right length at the end. The last table row shows that this chain remains the slower option on new JVMs too, and it allocates 80 instead of 48 bytes.

Bytes allocated per conversion on Java 25 – 48 bytes for Integer.toString(), String.valueOf() and "" + i, 80 for the explicit StringBuilder, 360 for String.format()
Bytes allocated per conversion (Java 25)

String.format() is the outlier

On Java 25, String.format("%d", i) takes about 55 nanoseconds on the i7 and 42 on the M5 Pro – about seven times as long as the other variants – and allocates 360 bytes per call: a Formatter, a StringBuilder, the parsed format specification, and the result String.

String.format("%d", i) – nanoseconds per conversion on both machines, Java 8 to 25
String.format() – nanoseconds per conversion

Up to Java 11, String.format() was even slower: 150 to 180 ns, 13 to 22 times the other variants. Since Java 17, the Formatter has a fast path for simple specifications like %d and %s, which are parsed without a regular expression (JDK-8263038). On x86, you can see the jump exactly there. On the M5 Pro, however, only from Java 21 on: there, Java 17 formats in 154 ns even with the current build 17.0.20 and allocates 1,152 bytes per call, three times as much as on x86. The fast path is the same Java code on both platforms – so the cause lies in the JIT compiler for aarch64 in Java 17. Which optimization is missing there, I did not investigate.

My recommendation: use String.format() only when you actually format – zero padding, thousands separators, fixed width. For a plain conversion, it is the wrong method.

The outlier on Java 23

A curiosity on the side: on Java 23, the explicit StringBuilder is slower on both machines than on 22 and 24, and it allocates 104 instead of 80 bytes. The three main variants (Integer.toString(i), String.valueOf(i), and "" + i) are unaffected. I did not investigate the cause – Java 23 has been out of support since March 2025, and from 24 on, time and memory consumption are back where they were on Java 22.

Under the hood: what "" + i becomes

To understand why "" + i has been just as fast as Integer.toString() since Java 9, let’s look at the bytecode. For that, we need a file IntToStringFast.java with the following content:

import java.util.Random;

public class IntToStringFast {
  public static void main(String[] args) {
    int i = new Random().nextInt();
    System.out.println("" + i);
  }
}

Compile it:

javac IntToStringFast.java

And display the bytecode:

javap -c IntToStringFast.class

Java 8: a StringBuilder chain

With javac 8, the relevant excerpt looks like this:

14: new           #6          // class java/lang/StringBuilder
17: dup
18: invokespecial #7          // Method java/lang/StringBuilder."<init>":()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;

That corresponds to this Java code:

new StringBuilder().append("").append(i).toString();

The compiler did not even optimize the append("") away. The StringBuilder allocates a char buffer for 16 characters, appends an empty String, writes the digits into it, and toString() copies them into a new array of the exact length. Two allocations, one copy – that is how 48 bytes become 80.

Since Java 9: invokedynamic

From javac 9 on, the same line of Java code becomes a single bytecode instruction:

15: invokedynamic #6,  0      // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;

The compiler no longer commits to how the concatenation is done. It only leaves behind a “recipe” – here: a constant followed by an int – and leaves the rest to the JVM. On the first call, StringConcatFactory builds a concatenation method from it, and that method goes about it differently than the StringBuilder: it first computes the length of the result – zero characters for the empty constant plus the number of digits – allocates the target array in exactly that size, and writes the digits straight into it. No buffer, no copy. That is the same work Integer.toString() does, which is why it is the same 48 bytes and the same nanoseconds.

What "" + i becomes: Java 8 emits a StringBuilder chain with an intermediate buffer; since Java 9, the JVM builds a concatenation method via invokedynamic that computes the length first
Since Java 9, the JVM decides how to concatenate, not javac

This mechanism is called indified string concatenation and arrived with JEP 280 in Java 9. The advantage of the detour: the JVM can improve the strategy without you having to recompile your code.

The proof: old bytecode on new JVMs

If the explanation is right, "" + i must be slower than Integer.toString() on a current JVM too – provided it is compiled as a StringBuilder chain. That can be checked: I additionally compiled the benchmark with --release 8. Then even a current javac emits the old StringBuilder chain. I ran this one JAR unchanged on all JVMs. "" + i is then considerably slower than Integer.toString():

Java 11Java 17Java 21Java 22Java 23Java 24Java 25
x86+82%+73%+69%+37%+64%+35%+26%
arm64+35%+36%+20%+25%+98%+21%+20%

The spike on Java 23 under arm64 is the curiosity from above: the same effect the explicit StringBuilder shows there.

If your project still builds with --release 8 or -target 8, or even still runs on Java 8, String.valueOf(i) is the faster choice.

I did the cross-check as well: a JAR built with --release 11, run on all JVMs from Java 17 on. On every JVM version, it delivers the same numbers as the JAR built with the corresponding version’s javac. The compiler has not changed anything about concatenation since Java 11 – the progress came from the JVM.

Summary

Since Java 9, Integer.toString(i), String.valueOf(i), and "" + i are the same operation: they are equally fast, and all three allocate the same 48 bytes per conversion – exactly one String, no intermediate buffer – on x86 as on arm64. Which one you pick is a matter of readability, not speed.

I recommend String.valueOf(i): you can see at first glance that a conversion happens here, and it is fast no matter which compiler produced your bytecode – unlike "" + i, which falls behind clearly on Java 8 and with --release 8. "" + i is equivalent as soon as your bytecode comes from a javac of version 9 or later; some read it as a trick, so it is a matter of taste within your team.

Use String.format("%d", i) only when you actually format. For a plain conversion, it costs seven times as much.

And the 2019 result, according to which "" + i was the fastest variant? It was an artifact of the benchmark, not a property of Java. The info box in the section “The benchmark” explains what went wrong – if you write microbenchmarks yourself, that is the part of this article that will save you the most time.

The next article goes the other way: what you need to watch out for when parsing Strings into ints – and what boxing costs you there.

Did this article save you time? Then I’d be happy if you invested a minute of it in a review on my ProvenExpert profile. Your feedback shows me that the work on these articles pays off.

👉 Leave a review

Would you like to be notified when I take a close look at the next Java release? Then click here to sign up for the HappyCoders newsletter.

👉 Newsletter Sign-up

Want Even More Knowledge?

My blog features many articles on Java, software architecture, and performance — from foundational concepts to advanced patterns.

If you want to go deeper, check out my trainings: hands-on, easy to understand, and directly applicable to your day-to-day project work. Instead of theory, I teach principles that help you write code that is better, more maintainable, and more performant in the long run.

Explore the Java Trainings

Become a Better Java Developer

My free newsletter keeps you ahead. Modern Java: new versions & features, performance, and JVM insights – once a month.

Search