Skip to content
Java best practices 7 min read

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 NullPointerException in comparisons, put the known non-null string first: "expected".equals(userInput) instead of userInput.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 null and you don’t check it
  • Auto-unboxing a null wrapper 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 use StringBuilder inside 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 Throwable unless you’re writing a top-level framework handler. Throwable includes Error subclasses like OutOfMemoryError — 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. But new String("hello") bypasses the pool. That’s why == sometimes works for literals and fails for new 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 uses valueOf(), so cached values share references.
  • ConcurrentModificationException: ArrayList tracks an internal modCount. The for-each iterator checks modCount on every next() call. Direct list.remove() changes modCount without telling the iterator, triggering the exception immediately.
  • hashCode contract: The JVM does not enforce the equals/hashCode contract — 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.

Last updated June 13, 2026
Was this helpful?