Top 10 Mistakes in Java and How to Avoid Them (With Examples)

Java is a powerful, object-oriented programming language that is widely used in enterprise applications, web development, and mobile development. However, even experienced developers make common mistakes that lead to bugs, poor performance, and security vulnerabilities.

In this article, we will explore the top 10 most common mistakes in Java and how to avoid them with bad and good code examples.

1️⃣ Using == Instead of .equals() for String Comparison

One of the most common mistakes in Java is comparing Strings using == instead of .equals(). The == operator checks for reference equality, while .equals() checks for value equality.

❌ Bad Code:

String str1 = new String("Java");
String str2 = new String("Java");

if (str1 == str2) { // WRONG: Checks reference, not value
    System.out.println("Strings are equal");
} else {
    System.out.println("Strings are not equal");
}

🔴 Output:

Strings are not equal

✅ Good Code:

String str1 = "Java";
String str2 = "Java";

if (str1.equals(str2)) { // CORRECT: Checks actual value
    System.out.println("Strings are equal");
}

🟢 Output:

Strings are equal

2️⃣ Forgetting to Override hashCode() When Overriding equals()

In Java, when you override equals(), you must also override hashCode(). Otherwise, collections like HashSet, HashMap, and HashTable won’t function properly.

❌ Bad Code (Only Overriding equals()):

class Employee {
    String name;
    
    Employee(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Employee)) return false;
        Employee emp = (Employee) obj;
        return name.equals(emp.name);
    }
}

public class Main {
    public static void main(String[] args) {
        Employee e1 = new Employee("Alice");
        Employee e2 = new Employee("Alice");

        System.out.println(e1.equals(e2)); // true
        
        HashSet<Employee> employees = new HashSet<>();
        employees.add(e1);
        
        System.out.println(employees.contains(e2)); // false ❌ Wrong!
    }
}

✅ Good Code (Override Both equals() and hashCode()):

import java.util.Objects;

class Employee {
    String name;

    Employee(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Employee)) return false;
        Employee emp = (Employee) obj;
        return name.equals(emp.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name); // Correctly override hashCode
    }
}

How Does This Fix the Issue?

  • Now, e1.hashCode() and e2.hashCode() will return the same value because they are based on the name field.
  • When calling contains(e2), the HashSet looks in the correct bucket and finds e1, returning true.

📌 Summary:

  • Always override hashCode() when overriding equals() to ensure correct behavior in hash-based collections.
  • If two objects are considered equal by equals(), they must have the same hashCode().
  • Not overriding hashCode() can cause unexpected behavior, like HashSet.contains() returning false for an object that is logically "equal" to one already in the set.

🚀 Rule of Thumb: If an object will be used as a key in HashSet, HashMap, or similar collections, override both equals() and hashCode() properly.

3️⃣ Not Closing Resources (Memory Leaks!)

Failing to close database connections, file readers, or network sockets can lead to memory leaks and performance issues.

❌ Example 1: Not Closing a File Resource

import java.io.*;

public class FileReaderExample {
    public static void main(String[] args) throws IOException {
        FileReader fr = new FileReader("file.txt");
        BufferedReader br = new BufferedReader(fr);
        
        System.out.println(br.readLine());

        // ❌ Resource not closed! Can cause memory leak
    }
}

✅ Good Code (Using try-with-resources)

import java.io.*;

public class FileReaderExample {
    public static void main(String[] args) {
        try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
            System.out.println(br.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

✔️ Why Is This Better?

  • The BufferedReader is automatically closed when the try block ends.
  • No need to manually call br.close().
  • Even if an exception occurs, the resource is closed properly.

Example 2: Not Closing a Database Connection

import java.sql.*;

public class DatabaseExample {
    public static void main(String[] args) throws SQLException {
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "password");
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT * FROM employees");

        while (rs.next()) {
            System.out.println(rs.getString("name"));
        }

        // ❌ Connection, Statement, and ResultSet are not closed!
    }
}

🚨 Problem:

  • The database connection is left open, which can exhaust database resources.

Fixed Code: Using try-with-resources

import java.sql.*;

public class DatabaseExample {
    public static void main(String[] args) {
        try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "password");
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT * FROM employees")) {

            while (rs.next()) {
                System.out.println(rs.getString("name"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

✔️ Now, all resources are automatically closed! 🚀

Example 3: Not Closing a Network Socket

import java.io.*;
import java.net.*;

public class SocketExample {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("example.com", 80);
        OutputStream out = socket.getOutputStream();
        out.write("GET / HTTP/1.1\r\n".getBytes());

        // ❌ Socket remains open!
    }
}

🚨 Problem:

  • The socket remains open, leading to network resource exhaustion.

Fixed Code: Using try-with-resources

import java.io.*;
import java.net.*;

public class SocketExample {
    public static void main(String[] args) {
        try (Socket socket = new Socket("example.com", 80);
             OutputStream out = socket.getOutputStream()) {

            out.write("GET / HTTP/1.1\r\n".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

🎯 Key Takeaways

Always close resources like files, database connections, and sockets to prevent memory leaks.
Use try-with-resources (try (...) {}) to ensure automatic closure.
Manually closing resources (.close()) is error-prone and should be avoided.
Unclosed resources can cause memory leaks, performance issues, and system crashes.

🚀 Rule of Thumb:

"If a class implements AutoCloseable, always use try-with-resources!"

4️⃣ Catching Generic Exceptions (Poor Error Handling)

Catching generic exceptions (Exception or Throwable) makes debugging difficult because it can hide real issues.

❌ Bad Code (Catching Everything)

try {
    int result = 10 / 0; // ArithmeticException
} catch (Exception e) { // ❌ Catches all exceptions
    System.out.println("Something went wrong");
}

🔹 Issue: It swallows all exceptions, making debugging hard.

✅ Good Code (Catch Specific Exceptions)

try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("Cannot divide by zero!");
}

✔️ More informative and specific error handling.

5️⃣ Using nextInt() Without Handling Newline Issue in Scanner

When using Scanner to take integer and string inputs together, calling nextInt() without handling the newline (\n) left in the buffer can cause issues.

Bad Code (Skipping Input)

import java.util.Scanner;

public class ScannerExample {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("Enter age: ");
        int age = scanner.nextInt(); // Reads integer

        System.out.print("Enter name: ");
        String name = scanner.nextLine(); // ❌ Skips input

        System.out.println("Name: " + name + ", Age: " + age);
    }
}

🔴 Issue:

  • nextInt() does not consume the newline character (\n) after reading the integer.
  • nextLine() reads the leftover newline instead of waiting for user input.

Good Code (Handling Scanner Issue)

import java.util.Scanner;

public class ScannerExample {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("Enter age: ");
        int age = scanner.nextInt();
        scanner.nextLine(); // ✔️ Consume the newline

        System.out.print("Enter name: ");
        String name = scanner.nextLine(); // ✔️ Now works correctly

        System.out.println("Name: " + name + ", Age: " + age);
    }
}

✔️ Adding scanner.nextLine(); after nextInt() ensures proper input handling.

6️⃣ Using public Fields Instead of Encapsulation

Encapsulation is one of the core principles of Object-Oriented Programming (OOP). It helps protect the internal state of an object and makes the class easier to maintain and modify in the future.

When you declare fields as public, any part of the code can modify them directly, breaking encapsulation.

Bad Code (Using Public Fields)

class User {
    public String name;
}

public class Main {
    public static void main(String[] args) {
        User user = new User();
        user.name = "Amit"; // ❌ Direct access to the field
        System.out.println(user.name);
    }
}

🔴 Issues:

  1. No control over modifications – Anyone can change the field without any validation.
  2. Difficult to add extra logic later – If you need to validate or log changes, you have to modify all usages of the field across the entire codebase.
  3. Breaks encapsulation – The internal details of the class are exposed, making it harder to maintain.

🟢 Good Code: Using Encapsulation

Encapsulation means:

  • Making fields private so they cannot be accessed directly.
  • Providing controlled access through getter and setter methods.

Encapsulated Code (Using Getters and Setters)

class User {
    private String name; // 🔒 Private field

    public String getName() { // ✅ Getter method
        return name;
    }

    public void setName(String name) { // ✅ Setter method
        this.name = name;
    }
}

public class Main {
    public static void main(String[] args) {
        User user = new User();
        user.setName("Amit"); // ✅ Controlled modification
        System.out.println(user.getName()); // ✅ Controlled access
    }
}

✔️ Why Is This Better?

  • Protects data – Other classes cannot modify name directly.
  • Allows validation – You can add rules inside the setter, e.g., prevent empty names.
  • Improves maintainability – Future modifications (e.g., logging, format checks) can be added without breaking existing code.

Example: Adding Validation in Setter

Encapsulation allows us to add validation logic easily.

Encapsulation with Validation

class User {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty!"); // ✔️ Validation logic
        }
        this.name = name;
    }
}

public class Main {
    public static void main(String[] args) {
        User user = new User();
        
        user.setName("Amit"); // ✅ Valid name
        System.out.println(user.getName());

        user.setName(""); // ❌ Throws exception (Prevents invalid data)
    }
}

✔️ Now, invalid names are not allowed!

Example: Read-Only Fields (No Setter)

If a field should not be modified after initialization, you can omit the setter.

Encapsulation with Read-Only Property

class User {
    private final String name; // 🔒 Read-only field

    public User(String name) { // ✅ Set only once via constructor
        this.name = name;
    }

    public String getName() { // ✅ Getter only
        return name;
    }
}

public class Main {
    public static void main(String[] args) {
        User user = new User("Amit");
        System.out.println(user.getName()); // ✅ Allowed
        // user.setName("Ravi"); ❌ Not allowed (No setter)
    }
}

✔️ Now, name cannot be changed after the object is created.

🎯 Key Takeaways

Always make fields private and use getters and setters for controlled access.
Encapsulation allows validation, ensuring that only valid data is stored.
Read-only properties can be created by not providing a setter.
Encapsulation improves security, maintainability, and flexibility in the code.

🚀 Rule of Thumb:
"Never expose class fields directly. Always use encapsulation to control data access!"

7️⃣ Using Iterator Incorrectly (Concurrent Modification Exception)

Modifying a collection while iterating over it can lead to ConcurrentModificationException.

❌ Bad Code (Direct Modification)

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");

for (String item : list) {
    if (item.equals("A")) {
        list.remove(item); // ❌ ConcurrentModificationException
    }
}
🔴 Issue:
  • for-each loop or for loop does not allow modification of the list while iterating.
  • Results in ConcurrentModificationException at runtime.

✅ Good Code (Using Iterator)

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    if (iterator.next().equals("A")) {
        iterator.remove(); // ✔️ Safe removal
    }
}
✔️ Using Iterator.remove() ensures safe modification during iteration.

8️⃣ Hardcoding Values Instead of Using Constants

Hardcoding values make the code harder to maintain.

❌ Bad Code (Hardcoded Values)

if (userRole == 1) { // ❌ What does 1 mean?
    System.out.println("Admin Access");
}

✅ Good Code (Using Constants)

public static final int ROLE_ADMIN = 1;

if (userRole == ROLE_ADMIN) {
    System.out.println("Admin Access");
}

✔️ Makes the code more readable and maintainable.

9️⃣ Not Using Generics (Type Safety Issues)

Using raw types instead of generics can cause ClassCastException.

❌ Bad Code (Without Generics)

List list = new ArrayList();
list.add("Java");
String s = (String) list.get(0); // ❌ Explicit cast needed

✅ Good Code (With Generics)

List<String> list = new ArrayList<>();
list.add("Java");
String s = list.get(0); // ✔️ No cast needed

✔️ Generics improve type safety and remove unnecessary casting.

🔟 Using String for Heavy String Manipulation Instead of StringBuilder

Using String in loops leads to unnecessary object creation.

❌ Bad Code (String in Loop)

String result = "";
for (int i = 0; i < 1000; i++) {
    result += i; // ❌ Creates a new object in each iteration
}

✅ Good Code (StringBuilder)

StringBuilder result = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    result.append(i);
}

✔️ StringBuilder is much more efficient for modifications.

🎯 Conclusion: Write Java Like a Pro!

Avoiding common Java mistakes is crucial for writing efficient, maintainable, and bug-free code. Whether you are a beginner or an experienced Java developer, keeping these best practices in mind will enhance performance, improve code readability, and prevent costly debugging efforts.

✅ Key Takeaways:

✔️ Use .equals() instead of == for string comparison.
✔️ Override both equals() and hashCode() to avoid unexpected behavior in collections.
✔️ Always close resources properly using try-with-resources to prevent memory leaks.
✔️ Catch specific exceptions instead of using generic ones.
✔️ Using nextInt() Without Handling Newline Issue in Scanner
✔️ Use encapsulation to protect data and maintain code structure.
✔️ Avoid modifying collections while iterating without using an Iterator.
✔️ Use constants instead of hardcoded values for better readability.
✔️ Leverage generics to improve type safety and remove unnecessary casting.
✔️ Use StringBuilder instead of String for efficient string manipulation.

By following these best practices, you will write better Java code, prevent common pitfalls, and create robust applications. Adopt these habits today and see an improvement in your Java programming skills! 🚀

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare