[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.

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.