Common Mistakes & Pitfalls
Every Java developer — no matter their experience level — trips over the same handful of gotchas. This page catalogs the most common mistakes, explains exactly why they happen, and shows you how to fix or avoid them.
Comparing Strings with ==
This is arguably the single most common Java mistake for beginners. The == operator checks reference equality (do both variables point to the same object in memory?), not value equality.
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false — different objects
System.out.println(a.equals(b)); // true — same content
Output:
false
true
Always use .equals() to compare string content. For case-insensitive comparisons use .equalsIgnoreCase().
Tip: To avoid
NullPointerExceptionin comparisons, put the known non-null string first:"expected".equals(userInput)instead ofuserInput.equals("expected").
See String Comparison for a full breakdown of ==, equals(), and compareTo().
NullPointerException (NPE)
An NPE is thrown when you try to call a method or access a field on a null reference. It’s the most famous Java runtime error.
String name = null;
System.out.println(name.length()); // throws NullPointerException
Common causes:
- Forgetting to initialize a variable
- A method returns
nulland you don’t check it - Auto-unboxing a
nullwrapper object (see below)
Fix: Always validate before use, or use Optional (Java 8+) to make nullability explicit.
String name = getName(); // might return null
if (name != null) {
System.out.println(name.length());
}
// Or with Optional
Optional.ofNullable(getName())
.ifPresent(n -> System.out.println(n.length()));
Note: Java 14+ prints helpful NPE messages that name the exact variable that was null — a huge debugging improvement.
Integer Cache Trap (== on Boxed Numbers)
Java caches Integer objects in the range -128 to 127. Outside that range, == on boxed integers gives surprising results.
Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true — cached, same object
Integer p = 128;
Integer q = 128;
System.out.println(p == q); // false — new objects each time
Output:
true
false
Always use .equals() when comparing Integer, Long, Double, and other wrapper objects. See Wrapper Classes and Autoboxing & Unboxing for details.
Accidental Auto-Unboxing NPE
When Java auto-unboxes a null wrapper, you get an NPE with no obvious cause.
Map<String, Integer> scores = new HashMap<>();
int score = scores.get("Alice"); // NPE! get() returns null, unboxing null throws
Fix: Check for null before unboxing, or use getOrDefault().
int score = scores.getOrDefault("Alice", 0); // safe default
Off-By-One Errors in Loops
A classic mistake in for loops is using <= instead of <, or iterating one step too many or too few.
int[] nums = {10, 20, 30};
// Wrong — ArrayIndexOutOfBoundsException
for (int i = 0; i <= nums.length; i++) {
System.out.println(nums[i]);
}
// Correct
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
Tip: Prefer the for-each loop when you don’t need the index — it eliminates this class of error entirely.
Modifying a Collection While Iterating
Removing elements from a List inside a regular for-each loop throws ConcurrentModificationException.
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
// Wrong
for (String name : names) {
if (name.startsWith("B")) {
names.remove(name); // throws ConcurrentModificationException
}
}
// Correct — use Iterator
Iterator<String> it = names.iterator();
while (it.hasNext()) {
if (it.next().startsWith("B")) {
it.remove(); // safe
}
}
// Also correct (Java 8+)
names.removeIf(name -> name.startsWith("B"));
See Iterator for more on safe iteration patterns.
String += in a Loop (Performance Pitfall)
Strings are immutable in Java. Each += inside a loop creates a brand-new String object, making the operation O(n²).
// Wrong — O(n²) allocations
String result = "";
for (int i = 0; i < 10_000; i++) {
result += i; // creates new String every iteration
}
// Correct — O(n) with StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
sb.append(i);
}
String result2 = sb.toString();
Warning: For large loops (thousands of iterations), the naive
+=approach can be orders of magnitude slower. Always useStringBuilderinside loops.
See String Immutability and String vs StringBuffer for more context.
Not Overriding Both equals() and hashCode()
If you override equals() but not hashCode(), your objects will behave incorrectly when used as keys in a HashMap or stored in a HashSet.
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
// hashCode() NOT overridden — BUG!
}
Map<Point, String> map = new HashMap<>();
map.put(new Point(1, 2), "origin");
System.out.println(map.get(new Point(1, 2))); // null — wrong bucket!
Fix: Always override both. Modern IDEs and java.util.Objects make this easy.
@Override
public int hashCode() {
return Objects.hash(x, y);
}
See Working of HashMap for why hashCode() determines which bucket an entry lives in.
Catching Exception (or Throwable) Too Broadly
Swallowing all exceptions hides real bugs and makes debugging a nightmare.
// Bad — silences everything, including programming errors
try {
doSomething();
} catch (Exception e) {
// do nothing
}
// Good — catch only what you can handle
try {
int result = Integer.parseInt(userInput);
} catch (NumberFormatException e) {
System.out.println("Please enter a valid number.");
}
Warning: Never catch
Throwableunless you’re writing a top-level framework handler.ThrowableincludesErrorsubclasses likeOutOfMemoryError— catching those rarely makes sense and usually indicates a design problem.
See Exception Handling and Multiple catch Blocks.
Ignoring finally / Not Closing Resources
Failing to close InputStream, Connection, or similar resources causes resource leaks. The finally block helps, but Java 7+ try-with-resources is cleaner and safer.
// Old, risky approach
FileReader reader = null;
try {
reader = new FileReader("data.txt");
// read...
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try { reader.close(); } catch (IOException e) { /* ignore */ }
}
}
// Modern — try-with-resources (Java 7+)
try (FileReader reader = new FileReader("data.txt")) {
// read — reader is automatically closed
} catch (IOException e) {
e.printStackTrace();
}
See finally Block and FileReader.
Static Fields as Instance State
static fields are shared across all instances of a class. Using them as instance-level state is a common source of subtle bugs.
class Counter {
static int count = 0; // shared by ALL Counter objects!
Counter() { count++; }
}
Counter a = new Counter();
Counter b = new Counter();
System.out.println(Counter.count); // 2 — not what you expect if you wanted per-instance
If you want per-instance state, remove the static keyword. See static Keyword for when static is and isn’t appropriate.
Integer Division Losing the Decimal
When both operands of / are int, Java performs integer division and discards the fractional part.
int a = 7, b = 2;
System.out.println(a / b); // 3, not 3.5
System.out.println(a / (double) b); // 3.5 — cast one operand
Cast at least one operand to double or float when you need a decimal result.
Under the Hood
Several of these pitfalls are rooted in JVM internals:
- String pool: String literals are interned in the JVM string pool, so two literal
"hello"values do share the same reference. Butnew String("hello")bypasses the pool. That’s why==sometimes works for literals and fails fornew String(...). See String Pool & intern(). - Integer cache: The JVM spec mandates that
Integer.valueOf()caches values from -128 to 127 (the upper bound can be increased with-XX:AutoBoxCacheMax). Autoboxing usesvalueOf(), so cached values share references. - ConcurrentModificationException:
ArrayListtracks an internalmodCount. The for-each iterator checksmodCounton everynext()call. Directlist.remove()changesmodCountwithout telling the iterator, triggering the exception immediately. - hashCode contract: The JVM does not enforce the
equals/hashCodecontract — you can violate it and the compiler won’t complain. The failure mode only appears at runtime when objects are stored in hash-based structures.
Related Topics
- Java Best Practices — high-level principles for writing clean, maintainable Java
- Clean Code in Java — style and structure guidance to keep your codebase readable
- Exception Handling — how to handle errors the right way
- String Immutability — why strings are immutable and what that means for performance
- Working of HashMap — internals that explain the
equals/hashCoderequirement - Autoboxing & Unboxing — the hidden conversions behind the Integer cache trap