The Common Mistakes That Java Developers Make while app Development
The most common Java development mistakes are comparing objects with ==, mishandling null, leaking resources, swallowing exceptions, guessing at performance instead of measuring it, and writing tightly coupled code. Most are easy to avoid once you have seen them fail.
Below are ten mistakes that appear constantly in real codebases, each with a short explanation, its practical impact, and a working fix. Every code example here was compiled and executed on a Java 11 runtime before publication.
1. Comparing objects with == instead of equals()
The mistake: using == on objects. For reference types, == compares whether two variables point to the same object, not whether their contents match.
Why it bites: small values often appear to work, which makes the bug worse. Java caches boxed Integer values in a small range, so == can return true for small numbers and false for large ones.
Integer a = 1000, b = 1000;
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
Integer c = 100, d = 100; // inside the cache range
System.out.println(c == d); // true <-- the trap
Those outputs are from an actual run. Code tested with small values passes; the same code fails in production with larger ones.
The fix: use .equals() for content comparison, or Objects.equals(a, b) when either side may be null. Reserve == for primitives and deliberate identity checks.
2. Letting null spread through the codebase
The mistake: chaining method calls without considering that any link might return null.
// Throws NullPointerException if any step is null
return order.getCustomer().getAddress().getCity();
Impact: NullPointerException remains one of the most frequent runtime failures in Java applications, and the stack trace often points at the chain rather than at whichever method actually returned null.
The fix: Optional for values that may legitimately be absent, and Objects.requireNonNull() at method boundaries to fail fast where the bug actually is.
return Optional.ofNullable(order)
.map(Order::getCustomer)
.map(Customer::getAddress)
.map(Address::getCity)
.orElse("Unknown");
A useful discipline: decide for each method whether null is a valid return value, then document and enforce that decision. Most null bugs come from ambiguity, not from null itself.
3. Leaking resources
The mistake: opening a file, socket or database connection and closing it manually — which silently fails to happen when an exception is thrown first.
// If an exception occurs before close(), the reader leaks
BufferedReader r = Files.newBufferedReader(path);
long n = r.lines().count();
r.close();
return n;
Impact: leaked file handles and connections accumulate. The application runs fine in testing and exhausts the connection pool under sustained load — a failure mode that is difficult to reproduce locally.
The fix: try-with-resources, which closes anything implementing AutoCloseable whether the block exits normally or not.
try (BufferedReader r = Files.newBufferedReader(path)) {
return r.lines().count();
}
4. Swallowing exceptions
The mistake: catching broadly and discarding the error.
try {
return Integer.parseInt(raw);
} catch (Exception e) {
return 0; // the failure is now invisible
}
Impact: this is worse than crashing. The application continues with wrong data, and the eventual symptom appears far from the cause. Catching Exception also captures runtime errors you never intended to handle.
The fix: catch the specific exception, and either handle it meaningfully or wrap it with context and rethrow. Always preserve the original as the cause.
try {
return Integer.parseInt(raw);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Quantity is not a number: " + raw, e);
}
An empty catch block is almost always a defect. If you genuinely intend to ignore something, write a comment saying why — your reviewer cannot read your mind, and neither can you in six months.
5. Deeply nested conditional checks
The mistake: validating with nested if blocks, pushing the real logic several levels deep.
if (order != null) {
if (order.getCustomer() != null) {
if (order.getTotal() > 0) {
return "ok";
} else { return "zero total"; }
} else { return "no customer"; }
} else { return "no order"; }
The fix: guard clauses. Handle each failure case and return immediately, leaving the main path flat and unindented.
if (order == null) return "no order";
if (order.getCustomer() == null) return "no customer";
if (order.getTotal() <= 0) return "zero total";
return "ok";
Both versions behave identically. The second is substantially easier to read, and adding a fourth condition does not add a fourth level of indentation.
Where the same checks repeat across several methods, extract them into one validated method rather than copying the block.
6. Guessing at performance instead of measuring it
The mistake: deciding which code is slow by reading it.
This is the single most expensive habit on this list, because developers routinely optimise code that was never the bottleneck while the actual problem — frequently an N+1 query or an unindexed database lookup — goes untouched.
The JVM makes intuition particularly unreliable. The just-in-time compiler optimises hot paths at runtime, so code that looks inefficient may be optimised away entirely, and code that looks tight may dominate your profile.
The fix: measure before changing anything.
- Profilers — such as the JDK’s built-in Java Flight Recorder with JDK Mission Control, or VisualVM — show where time and memory actually go in a running application.
- JMH (Java Microbenchmark Harness) is the standard tool for benchmarking small pieces of code. It handles JVM warm-up and dead-code elimination, which naive timing loops using
System.currentTimeMillis()do not — such loops routinely produce results that are wrong by orders of magnitude.
Measure, change one thing, measure again. Any other sequence tells you nothing.
7. Building strings in a loop with +
The mistake: concatenating with += inside a loop. Java strings are immutable, so each iteration creates a new object and copies the previous contents.
String out = "";
for (String p : parts) {
out += p + ","; // allocates a new String every iteration
}
The fix: StringBuilder.
StringBuilder sb = new StringBuilder();
for (String p : parts) {
sb.append(p).append(',');
}
return sb.toString();
One honest caveat, consistent with mistake 6: for a handful of items this difference is irrelevant, and modern compilers optimise simple concatenation outside loops. It matters when the loop count is large. Do not treat it as a universal rule — profile if it is on a hot path.
8. Tight coupling between classes
The mistake: a class constructing its own dependencies.
class OrderService {
private final MySqlOrderRepository repo = new MySqlOrderRepository();
// Now permanently tied to MySQL, and untestable without a database
}
Impact: you cannot unit test this class without a real database, and swapping the implementation means editing every class that uses it. This is how codebases become expensive to change.
The fix: depend on an interface and accept the implementation through the constructor.
interface OrderRepository {
Order find(int id);
}
class OrderService {
private final OrderRepository repo;
OrderService(OrderRepository repo) { // injected, not constructed
this.repo = repo;
}
}
The test can now pass a simple stub. In verification for this article, the decoupled version accepted a one-line lambda stub in place of the database — which is the entire point.
9. Unsafe shared state across threads
The mistake: treating count++ as a single operation. It is three — read, increment, write — and two threads can interleave between them.
Running eight threads incrementing a plain int 50,000 times each — expected total 400,000 — produced 390,889 on one run while an AtomicInteger performing the same work returned exactly 400,000.
// Unsafe: lost updates under concurrency
int count = 0;
count++;
// Safe: atomic read-modify-write
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();
The exact shortfall varies between runs, which is what makes concurrency bugs so hard to catch — the same code may pass a hundred test runs and fail in production.
The fix: use the java.util.concurrent classes rather than writing your own synchronisation. AtomicInteger for counters, ConcurrentHashMap instead of a HashMap shared between threads, and ExecutorService rather than managing threads directly.
10. Building SQL by string concatenation
The mistake: putting user input directly into a query string.
// SQL injection vulnerability
Statement s = conn.createStatement();
ResultSet rs = s.executeQuery(
"SELECT * FROM users WHERE name = '" + name + "'");
Impact: an attacker who controls name can alter the query’s meaning. Injection has remained among the most serious and most common web application vulnerabilities for two decades.
The fix: PreparedStatement with bound parameters. The input is sent separately from the query structure, so it cannot change what the query does.
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE name = ?");
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
This applies regardless of how trusted the input seems. Validation elsewhere is not a substitute for parameterised queries.
The practices that catch these automatically
Knowing these mistakes helps. Having tooling that catches them helps more, because it works when you are tired.
| Practice | What it catches |
|---|---|
| Static analysis — SpotBugs, PMD, SonarQube, Error Prone | == on objects, resource leaks, empty catch blocks, likely null dereferences |
| Automated tests — JUnit, with Mockito for dependencies | Logic errors, regressions. Also makes tight coupling immediately obvious. |
| Code review | Design problems and unclear naming that no tool can judge |
| Consistent formatting — google-java-format, Spotless | Style arguments in review, and noisy diffs in version control |
| Version control discipline | Small, reviewable commits with messages explaining why |
| Javadoc on public APIs | Ambiguity about nullability, thread safety and valid inputs |
Run static analysis in your build pipeline rather than locally. A check that only runs when someone remembers is not a check.
Frequently asked questions
What is the most common mistake Java developers make?
Comparing objects with == instead of .equals(). It is especially deceptive because Java caches small boxed Integer values, so the same code can work with small numbers and fail with larger ones.
How do I find performance problems in a Java application?
Profile it. Use Java Flight Recorder with JDK Mission Control, or VisualVM, to see where time and memory actually go. For benchmarking small code sections, use JMH — it handles JVM warm-up, which naive timing loops do not.
Does Java have memory leaks if it has garbage collection?
Yes. Garbage collection reclaims unreachable objects, but objects your code still references are never collected. Common causes include static collections that only grow, unremoved listeners, and unclosed resources holding native memory.
Should I use Optional everywhere instead of null?
No. Optional is designed primarily for return values that may legitimately be absent. Using it for fields and method parameters adds overhead without much benefit. For parameters, Objects.requireNonNull() to fail fast is usually clearer.
Is catching Exception ever acceptable?
Rarely, and only at the outermost boundary of an application — a request handler or top-level loop — where you log the failure and return a controlled error. Inside business logic, catch the specific exception you can actually handle.
Where to start
If you fix only two things this week, make them these: add a static analysis tool to your build, and search your codebase for empty catch blocks. Those two steps will surface several of the mistakes above without you having to look for them.
Then adopt the habit behind mistake 6 — measure before you optimise. More Java performance work is wasted on the wrong code than on the wrong technique.
All code examples were compiled and executed on OpenJDK 11 before publication; the Integer caching and concurrency outputs shown are from actual runs. Concurrency results are non-deterministic and will vary between runs and machines. Language behaviour described here should be verified against the official Java SE documentation for your target version. No performance benchmarks are quoted, as results depend heavily on JVM version, hardware and workload. Last reviewed: October 2026.



