[MM’s] Tiny Bench Notes — 10 Ways to Validate Integers in Java

A dive into the performance costs of simple code in high-throughput environments.

10 Ways to Validate Integers in Java

If you’ve ever written a quick validation routine if integer, you probably reached for Integer.parseInt() inside a try-catch. It works, it’s simple and the error messages are clear. But when your service processes tens of thousands of strings per second, that innocent line becomes a battlefield of allocations, exceptions and surprising bottlenecks.

⚡ TL;DR (Quick Recap)

  • Manual char-by-char validation: fastest (~6–13 ns/op)
  • parseInt with exceptions: fast for valid inputs, terrible for invalid ones (~537–822 ns/op)
  • Regex: clean, but 3–5× slower than manual approaches
  • String.matches(): slow due to regex compilation every call
  • Scanner: the absolute worst (~4,900 ns/op)

Why This Matters Now

With modern architectures shifting toward event-driven ingestion, log-heavy analytics and streaming pipelines, applications often validate huge amounts of untrusted text data.

Micro-optimizations don’t matter in most code, but validating millions of strings in tight loops absolutely deserves measurement. And integer parsing is a perfect example of how different Java abstractions behave under stress.

The Benchmark Setup

The candidates: 10 ways to check if a string is an integer

  • Try/Catch
  • Try/Catch + pre-check
  • Regex (compiled)
  • String.matches()
  • Manual parsing
  • Manual parsing (optimized charAt)
  • Manual parsing via char[]
  • Java Streams
  • Scanner
  • Apache Commons–style digit checks

All approaches followed the same rules:

  • Accept optional leading sign (+/-)
  • Allow leading zeros
  • Reject decimals, whitespace and empty strings
  • Ensure the number fits exactly in int range

Test data included:

  • Valid small integers
  • Valid large integers
  • Values near Integer.MIN and Integer.MAX
  • Eight classes of invalid input (whitespace, decimals, hex, overflowing values, etc.)

Everything was measured using JMH 1.37 on Java 25.

The benchmarks were run on OpenJDK 25 (Temurin-25+36) with -Xms1g -Xmx1g -XX:+UseG1GC. 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.

The Winners — Manual Parsing

The fastest methods by far were the manual character-inspection variants. They consistently delivered:

  • Valid inputs: ~9–10 ns/op
  • Invalid inputs: ~6–7 ns/op
  • Large inputs: ~13 ns/op

Here’s the winning pseudo approach:

function isInteger(text):
if text is null or empty: return false

index = 0
isNegative = (text[0] == '-')

if text[0] is '+' or '-':
index = 1
if no more characters: return false

value = 0
for each char from index to end:
if char not in '0'-'9': return false
value = value * 10 + char
if value exceeds integer bounds: return false

return true

Why it wins

  • No allocations
  • No exceptions
  • No regex state machines
  • Fail-fast behavior for invalid characters
  • Range checking built directly into the loop

It’s “old school,” but sometimes the classics are classics for a reason.

Try-Catch: Surprisingly Good… Until It Isn’t

The typical approach:

public boolean isInteger(String text) {
if (text == null || text.isEmpty()) return false;
try {
Integer.parseInt(text);
return true;
} catch (NumberFormatException e) {
return false;
}
}

Performance

  • Valid input: ~9–13 ns/op
  • Invalid input: ~537–822 ns/op ❌

Why the huge gap?

Because exceptions in Java aren’t just thrown — they’re constructed, filled with stack traces and allocated, making them about 80× slower than a char check.

Try-catch validation is still great when:

  • You expect almost all inputs to be valid
  • Exceptions are truly exceptional

Regex and Streams

Compiled regex

  • 26–139 ns/op
  • Clean and elegant
  • But: allocates a new Matcher every call

String.matches()

  • 92–203 ns/op
  • Compiles the regex every single time
  • Should never be used in hot paths

Regex is attractive, but performance isn’t its strong suit — especially when range checking still requires a call to parseInt().

The Slowest: Scanner

Scanner.hasNextInt() might look convenient:

  try (var scanner = new java.util.Scanner(text)) {
if (scanner.hasNextInt()) {
scanner.nextInt();
return !scanner.hasNext();
}
return false;
}

Performance

  • ~4,900 ns/op
  • ~500× slower than manual parsing

Scanner is built for tokenization, not simple validation. Lots of internal buffering, charset handling and object creation make it a terrible choice here.

Approaches Not Covered Above: Quick Notes

Java Streams

  • 19–122 ns/op
  • Readable, but adds iteration and lambda overhead

Apache Commons–Style Digit Checking

This approach uses Character.isDigit() inside a loop.

  • Valid integers: ~12 ns/op
  • Invalid: ~110 ns/op
  • It’s predictable and safer than regex, but noticeably slower than charAt-based manual loops.

char[] Conversion Approach

  • Valid: ~18 ns/op
  • Invalid: ~17 ns/op
  • The extra toCharArray() allocation dominates the cost.

Practical Guidance: Choose the Right Tool

Here’s when to use each approach:

Use manual parsing when:

  • You care about raw performance
  • Your input may contain many invalid values
  • You run validation inside a tight loop

Use try-catch when:

  • Data is mostly valid
  • Simplicity matters more than speed
  • You aren’t in a high-throughput scenario

Use regex when:

  • You need very concise code
  • Performance is secondary
  • You don’t mind the matcher overhead

Final Takeaways

Integer validation is a deceptively complex micro-benchmark. What looks clean or idiomatic in everyday Java can behave very differently once scaled into thousands or millions of operations per second.

From this benchmark:

  • Manual character checking is the fastest and most predictable approach.
  • Try-catch is excellent for clean data but disastrous for bad input.
  • Regex is fine for readability, not for speed.
  • Scanner should be avoided entirely for validation.

You can find all the code on GitHub.

Originally posted on marconak-matej.medium.com.