Different implementations, one benchmark and several surprising JVM optimizations.

What’s the Fastest Way to Reverse a String in Java?

Reversing a string is one of those problems every Java developer has solved dozens of times. It’s common in coding interviews, utility methods, parsers and text-processing pipelines.

Java offers multiple ways to reverse a string, ranging from the built-in StringBuilder.reverse() to manual array manipulation and even streams.

While they all produce the same output, they certainly don't deliver the same performance and the implementation most developers assume is fastest (manual char[] swapping) only wins in one narrow case.

⚡ TL;DR (Quick Recap)

  • StringBuilder.reverse() is the best default choice for almost every application.
  • A manual char[] swap wins only on very short strings and it isn't Unicode-safe for surrogate pairs (e.g. many emoji).
  • Streams, recursion, stacks and collection-based implementations are considerably slower and are better suited for educational purposes than production code.

Benchmark Setup

It measures the average execution time per operation in nanoseconds, running in a shared benchmark state, with 2 forks (each doing 1 warmup fork), warming up for 5 iterations of 1 second each and then measuring over 10 iterations of 1 second each.

Here’s every implementation tested, as a common interface:

public interface ReverseStrategy {
String reverse(String input);
}

Test parameters:

  • JMH 1.37 on Java 25
  • JVM: -Xms1g -Xmx1g -XX:+UseG1GC

Why compare String reversal?

Most applications won’t spend measurable time reversing strings.

However, string manipulation appears everywhere — serialization, parsing, templating, log processing and data transformation. Understanding the trade-offs between different implementations is a useful exercise because it highlights the cost of allocations, boxing, recursion and abstraction.

StringBuilder.reverse()

The simplest and most idiomatic solution.

public class ReverseStringBuilder implements ReverseStrategy {

@Override
public String reverse(String input) {
if (input == null) return null;
return new StringBuilder(input).reverse().toString();
}
}

Pros

  • Extremely fast for medium/long inputs
  • Handles Unicode surrogate pairs correctly (surrogate-aware)
  • Readable and well-tested

Cons

  • Allocates a StringBuilder (usually negligible)

Two-Pointer char[]

Convert the string into a character array and swap characters from both ends in place.

public class ReverseTwoPointer implements ReverseStrategy {

@Override
public String reverse(String input) {
if (input == null) return null;
char[] c = input.toCharArray();
for (int i = 0, j = c.length - 1; i < j; i++, j--) {
char t = c[i];
c[i] = c[j];
c[j] = t;
}
return new String(c);
}
}

Pros

  • Fastest option for very short strings
  • Minimal overhead

Cons

  • Operates on UTF-16 char units, not Unicode code points
  • Breaks surrogate pairs — a string like "🙂🚀Java" can come out corrupted, since characters outside the Basic Multilingual Plane (many emoji) are represented as two chars in Java

Backwards Loop

Append characters in reverse order into a pre-sized StringBuilder.

public class ReverseLoop implements ReverseStrategy {

@Override
public String reverse(String input) {
if (input == null) return null;
StringBuilder sb = new StringBuilder(input.length());
for (int i = input.length() - 1; i >= 0; i--) {
sb.append(input.charAt(i));
}
return sb.toString();
}
}

Pre-sizing the StringBuilder with input.length() avoids internal array resizing as characters are appended — a small but real win over letting it grow dynamically.

Recursive Solution

public class ReverseRecursive implements ReverseStrategy {

@Override
public String reverse(String input) {
if (input == null || input.length() <= 1) {
return input;
}
return reverse(input.substring(1)) + input.charAt(0);
}
}

Elegant from an academic perspective, but O(n²) — each recursive call allocates a new substring and string concatenation allocates again. It’s also susceptible to StackOverflowError on sufficiently large inputs, since each character adds a stack frame.

Streams

Two variants were benchmarked:

public class ReverseStreamCodePoints implements ReverseStrategy {

@Override
public String reverse(String input) {
if (input == null) return null;
return input.codePoints()
.mapToObj(Character::toString)
.reduce("", (a, b) -> b + a);
}
}

Unlike ReverseTwoPointer and ReverseStreamIndex, this operates on code points (via codePoints()), so it correctly preserves surrogate pairs — the only stream-based option that’s emoji-safe.
public class ReverseStreamIndex implements ReverseStrategy {

@Override
public String reverse(String input) {
if (input == null) return null;
return IntStream.range(0, input.length())
.mapToObj(i -> String.valueOf(input.charAt(input.length() - 1 - i)))
.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
.toString();
}
}

Although expressive, both introduce meaningful overhead: reduce with string concatenation reallocates a new String on every step (O(n²) total) and boxing each charcode point into an object adds GC pressure.

Stack / Deque

Push every character onto a stack, then pop them back out.

public class ReverseStack implements ReverseStrategy {

@Override
public String reverse(String input) {
if (input == null) return null;
Deque<Character> stack = new ArrayDeque<>();
for (char c : input.toCharArray()) {
stack.push(c);
}
StringBuilder sb = new StringBuilder(input.length());
while (!stack.isEmpty()) {
sb.append(stack.pop());
}
return sb.toString();
}
}

Conceptually straightforward, but it performs unnecessary boxing (Character) and allocations.

Collections.reverse()

Convert every character into a boxed Character, reverse the list and rebuild the string.

public class ReverseCollections implements ReverseStrategy {

@Override
public String reverse(String input) {
if (input == null) return null;
List<Character> characters = new ArrayList<>(input.length());
for (char character : input.toCharArray()) {
characters.add(character);
}
Collections.reverse(characters);
StringBuilder result = new StringBuilder(input.length());
for (char character : characters) {
result.append(character);
}
return result.toString();
}
}

Interesting as a demonstration of the collections framework — but the boxing overhead of List<Character> makes it one of the slowest options tested, and not something you'd choose for production.

Benchmark Results

The benchmark measured three input sizes using JMH: short, medium and long strings.

Short Strings For very small inputs, the differences are measured in only a few nanoseconds, but the manual two-pointer implementation comes out on top.

  • 🥇 Two Pointer — 6.08 ns/op
  • 🥈 StringBuilder — 7.22 ns/op
  • 🥉 Backwards Loop — 8.05 ns/op
  • Streams (Index) — 23.86 ns/op
  • Recursion — 32.70 ns/op
  • Stack / Deque — 39.40 ns/op
  • Streams (Code Points) — 44.83 ns/op
  • Collections.reverse() — 57.02 ns/op

Medium Strings As the input grows, StringBuilder.reverse() becomes the clear winner.

  • 🥇 StringBuilder — 14.14 ns/op
  • 🥈 Two Pointer — 17.84 ns/op
  • 🥉 Backwards Loop — 63.39 ns/op
  • Streams (Index) — 113.00 ns/op
  • Collections.reverse() — 163.69 ns/op
  • Streams (Code Points) — 338.67 ns/op
  • Stack / Deque — 363.72 ns/op
  • Recursion — 408.71 ns/op

Long Strings The performance gap widens even further for larger inputs.

  • 🥇 StringBuilder — 27.48 ns/op
  • 🥈 Two Pointer — 31.74 ns/op
  • 🥉 Backwards Loop — 128.40 ns/op
  • Streams (Index) — 272.31 ns/op
  • Collections.reverse() — 450.99 ns/op
  • Stack / Deque — 1011.62 ns/op
  • Streams (Code Points) — 1172.19 ns/op
  • Recursion — 1605.49 ns/op

Why Does StringBuilder Win?

Many developers assume that manually swapping a char[] should always be the fastest approach.

The benchmark shows otherwise.

StringBuilder.reverse() has been heavily optimized inside the JDK over many releases — it's surrogate-pair-aware since JDK 5 (this is what fixes the emoji-corruption bug that the manual swap still has today). It performs minimal allocations and benefits from JVM optimizations that a hand-written loop doesn't automatically get.

Unless you have a very specialized use case — extremely short, ASCII-only strings, in a hot loop where every nanosecond genuinely matters — it’s difficult to outperform the JDK implementation.

Final Takeaway

For most production code, the decision tree is short:

  • Need maximum speed and know your input is BMP-only (no emoji)? Use the two-pointer swap.
  • Need correctness and simplicity and a few nanoseconds don’t matter? Use StringBuilder.reverse(). This is the right default for the overwhelming majority of code.
  • Avoid recursive reversal and stream-reduction-with-concatenation in anything performance-sensitive — both degrade badly as input grows.

Measure first. The fastest-looking code on paper isn’t always the code that actually runs fastest.

You can find all the code on GitHub.

Originally posted on marconak-matej.medium.com.